diff --git a/fhem/FHEM/70_ZoneMinder.pm b/fhem/FHEM/70_ZoneMinder.pm new file mode 100755 index 000000000..0a7061f11 --- /dev/null +++ b/fhem/FHEM/70_ZoneMinder.pm @@ -0,0 +1,724 @@ +############################################################################## +# +# 70_ZoneMinder.pm +# +# This file is part of Fhem. +# +# Fhem 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. +# +# Fhem 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 Fhem. If not, see . +# +############################################################################## +# +# ZoneMinder (c) Martin Gutenbrunner / https://github.com/delmar43/FHEM +# +# This module enables FHEM to interact with ZoneMinder surveillance system (see https://zoneminder.com) +# +# Discussed in FHEM Forum: https://forum.fhem.de/index.php/topic,91847.0.html +# +# $Id$ +# +############################################################################## + +package main; + +use strict; +use warnings; +use HttpUtils; +use Crypt::MySQL qw(password41); +use DevIo; +use Digest::MD5 qw(md5 md5_hex md5_base64); + +sub ZoneMinder_Initialize { + my ($hash) = @_; + $hash->{NotifyOrderPrefix} = "70-"; + $hash->{Clients} = "ZM_Monitor"; + + $hash->{GetFn} = "ZoneMinder_Get"; + $hash->{SetFn} = "ZoneMinder_Set"; + $hash->{DefFn} = "ZoneMinder_Define"; + $hash->{UndefFn} = "ZoneMinder_Undef"; + $hash->{ReadFn} = "ZoneMinder_Read"; + $hash->{ShutdownFn}= "ZoneMinder_Shutdown"; + $hash->{FW_detailFn} = "ZoneMinder_DetailFn"; + $hash->{WriteFn} = "ZoneMinder_Write"; + $hash->{ReadyFn} = "ZoneMinder_Ready"; + + $hash->{AttrList} = "interval publicAddress webConsoleContext " . $readingFnAttributes; + $hash->{MatchList} = { "1:ZM_Monitor" => "^.*" }; + + Log3 '', 3, "ZoneMinder - Initialize done ..."; +} + +sub ZoneMinder_Define { + my ( $hash, $def ) = @_; + my @a = split( "[ \t][ \t]*", $def ); + $hash->{NOTIFYDEV} = "global"; + + my $name = $a[0]; + $hash->{NAME} = $name; + + my $nrArgs = scalar @a; + if ($nrArgs < 3) { + my $msg = "ZoneMinder ($name) - Wrong syntax: define ZoneMinder "; + Log3 $name, 2, $msg; + return $msg; + } + + my $module = $a[1]; + my $zmHost = $a[2]; + $hash->{helper}{ZM_HOST} = $zmHost; + $zmHost .= ':6802' if (not $zmHost =~ m/:\d+$/); + $hash->{DeviceName} = $zmHost; + + if ($nrArgs == 4 || $nrArgs > 6) { + my $msg = "ZoneMinder ($name) - Wrong syntax: define ZoneMinder [ ]"; + Log3 $name, 2, $msg; + return $msg; + } + + if ($nrArgs == 5 || $nrArgs == 6) { + $hash->{helper}{ZM_USERNAME} = $a[3]; + $hash->{helper}{ZM_PASSWORD} = $a[4]; + } + +# Log3 $name, 3, "ZoneMinder ($name) - Define done ... module=$module, zmHost=$zmHost"; + + DevIo_CloseDev($hash) if (DevIo_IsOpen($hash)); + DevIo_OpenDev($hash, 0, undef); + + ZoneMinder_afterInitialized($hash); + + return undef; +} + +sub ZoneMinder_afterInitialized { + my ($hash) = @_; + + ZoneMinder_API_Login($hash); + + return undef; +} + +# so far only used for generating the link to the ZM Web console +# usePublic 0: zmHost, usePublic 1: publicAddress, usePublic undef: use public if publicAddress defined +sub ZoneMinder_getZmWebUrl { + my ($hash, $usePublic) = @_; + my $name = $hash->{NAME}; + + #use private or public LAN for Web access? + my $publicAddress = ZoneMinder_getPublicAddress($hash); + my $zmHost = ''; +# Log3 $name, 0, "ZoneMinder ($name) - publicAddress: $publicAddress, usePublic: $usePublic"; + if ($publicAddress and $usePublic) { + $zmHost = $publicAddress; + } else { + $zmHost = $hash->{helper}{ZM_HOST}; + } + $zmHost = "http://$zmHost"; + $zmHost .= '/' if (not $zmHost =~ m/\/$/); + + my $zmWebContext = $attr{$name}{webConsoleContext}; + if (not $zmWebContext) { + $zmWebContext = 'zm'; + } + $zmHost .= $zmWebContext; + + return $zmHost; +} + +sub ZoneMinder_getPublicAddress { + my ($hash) = @_; + my $name = $hash->{NAME}; + + return $attr{$name}{publicAddress}; +} + +# is built by using web-url, and adding /api +sub ZoneMinder_getZmApiUrl { + my ($hash) = @_; + + #use private LAN for API access for a start + my $zmWebUrl = ZoneMinder_getZmWebUrl($hash, 0); + return "$zmWebUrl/api"; +} + +sub ZoneMinder_API_Login { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $username = urlEncode($hash->{helper}{ZM_USERNAME}); + my $password = urlEncode($hash->{helper}{ZM_PASSWORD}); + + my $zmWebUrl = ZoneMinder_getZmWebUrl($hash); + my $loginUrl = "$zmWebUrl/index.php?username=$username&password=$password&action=login&view=console"; + + Log3 $name, 4, "ZoneMinder ($name) - loginUrl: $loginUrl"; + my $apiParam = { + url => $loginUrl, + method => "POST", + callback => \&ZoneMinder_API_Login_Callback, + hash => $hash + }; + HttpUtils_NonblockingGet($apiParam); + +# Log3 $name, 3, "ZoneMinder ($name) - ZoneMinder_API_Login err: $apiErr, data: $apiParam->{httpheader}"; + + return undef; +} + +sub ZoneMinder_API_Login_Callback { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + $hash->{APILoginStatus} = $param->{code}; + + if($err ne "") { + Log3 $name, 0, "error while requesting ".$param->{url}." - $err"; + $hash->{APILoginError} = $err; + } elsif($data ne "") { + if ($data =~ m/Invalid username or password/) { + $hash->{APILoginError} = "Invalid username or password."; + } else { + delete($defs{$name}{APILoginError}); + + ZoneMinder_GetCookies($hash, $param->{httpheader}); + + my $isFirst = !$hash->{helper}{apiInitialized}; + if ($isFirst) { + $hash->{helper}{apiInitialized} = 1; + my $zmApiUrl = ZoneMinder_getZmApiUrl($hash); + ZoneMinder_SimpleGet($hash, "$zmApiUrl/host/getVersion.json", \&ZoneMinder_API_ReadHostInfo_Callback); + ZoneMinder_SimpleGet($hash, "$zmApiUrl/configs.json", \&ZoneMinder_API_ReadConfig_Callback); + ZoneMinder_API_getLoad($hash); + } + } + } + + InternalTimer(gettimeofday() + 3600, "ZoneMinder_API_Login", $hash); + + return undef; +} + +sub ZoneMinder_API_getLoad { + my ($hash) = @_; + + my $zmApiUrl = ZoneMinder_getZmApiUrl($hash); + ZoneMinder_SimpleGet($hash, "$zmApiUrl/host/getLoad.json", \&ZoneMinder_API_ReadHostLoad_Callback); +} + +sub ZoneMinder_SimpleGet { + my ($hash, $url, $callback) = @_; + my $name = $hash->{NAME}; + + my $apiParam = { + url => $url, + method => "GET", + callback => $callback, + hash => $hash + }; + + if ($hash->{HTTPCookies}) { + $apiParam->{header} .= "\r\n" if ($apiParam->{header}); + $apiParam->{header} .= "Cookie: " . $hash->{HTTPCookies}; + } + + HttpUtils_NonblockingGet($apiParam); +} + +sub ZoneMinder_API_ReadHostInfo_Callback { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + if($err ne "") { + Log3 $name, 0, "error while requesting ".$param->{url}." - $err"; + $hash->{ZM_VERSION} = 'error'; + $hash->{ZM_API_VERSION} = 'error'; + } elsif($data ne "") { + + my $zmVersion = ZoneMinder_GetConfigValueByKey($hash, $data, 'version'); + if (not $zmVersion) { + $zmVersion = 'unknown'; + } + $hash->{ZM_VERSION} = $zmVersion; + + my $zmApiVersion = ZoneMinder_GetConfigValueByKey($hash, $data, 'apiversion'); + if (not $zmApiVersion) { + $zmApiVersion = 'unknown'; + } + $hash->{ZM_API_VERSION} = $zmApiVersion; + } + + return undef; +} + +sub ZoneMinder_API_ReadHostLoad_Callback { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + if($err ne "") { + Log3 $name, 0, "error while requesting ".$param->{url}." - $err"; + readingsSingleUpdate($hash, 'CPU_Load', 'error', 0); + } elsif($data ne "") { + my $load = ZoneMinder_GetConfigArrayByKey($hash, $data, 'load'); + readingsSingleUpdate($hash, 'CPU_Load', $load, 1); + + InternalTimer(gettimeofday() + 60, "ZoneMinder_API_getLoad", $hash); + } + + return undef; +} + +#this extracts ZM_PATH_ZMS and ZM_AUTH_HASH_SECRET from the ZoneMinder config +sub ZoneMinder_API_ReadConfig_Callback { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + if($err ne "") { + Log3 $name, 0, "error while requesting ".$param->{url}." - $err"; + } elsif($data ne "") { + my $zmPathZms = ZoneMinder_GetConfigValueByName($hash, $data, 'ZM_PATH_ZMS'); + if ($zmPathZms) { + $zmPathZms =~ s/\\//g; + $hash->{helper}{ZM_PATH_ZMS} = $zmPathZms; + } + + my $authHashSecret = ZoneMinder_GetConfigValueByName($hash, $data, 'ZM_AUTH_HASH_SECRET'); + if ($authHashSecret) { + $hash->{helper}{ZM_AUTH_HASH_SECRET} = $authHashSecret; + ZoneMinder_calcAuthHash($hash); + } + } + + return undef; +} + +sub ZoneMinder_GetConfigValueByKey { + my ($hash, $config, $key) = @_; + my $searchString = '"'.$key.'":"'; + return ZoneMinder_GetFromJson($hash, $config, $searchString, '"'); +} + +sub ZoneMinder_GetConfigArrayByKey { + my ($hash, $config, $key) = @_; + my $searchString = '"'.$key.'":['; + return ZoneMinder_GetFromJson($hash, $config, $searchString, ']'); +} + +sub ZoneMinder_GetConfigValueByName { + my ($hash, $config, $key) = @_; + my $searchString = '"Name":"'.$key.'","Value":"'; + return ZoneMinder_GetFromJson($hash, $config, $searchString, '"'); +} + +sub ZoneMinder_GetFromJson { + my ($hash, $config, $searchString, $endChar) = @_; + my $name = $hash->{NAME}; + +# Log3 $name, 5, "json: $config"; + my $searchLength = length($searchString); + my $startIdx = index($config, $searchString); + Log3 $name, 5, "$searchString found at $startIdx"; + $startIdx += $searchLength; + my $endIdx = index($config, $endChar, $startIdx); + my $frame = $endIdx - $startIdx; + my $searchResult = substr $config, $startIdx, $frame; + + Log3 $name, 5, "looking for $searchString - length: $searchLength. start: $startIdx. end: $endIdx. result: $searchResult"; + + return $searchResult; +} + +sub ZoneMinder_API_UpdateMonitors_Callback { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + my @monitors = split(/\{"Monitor"\:\{/, $data); + + foreach my $monitorData (@monitors) { + my $monitorId = ZoneMinder_GetConfigValueByKey($hash, $monitorData, 'Id'); + + if ( $monitorId =~ /^[0-9]+$/ ) { + ZoneMinder_UpdateMonitorAttributes($hash, $monitorData, $monitorId); + } else { + Log3 $name, 0, "Invalid monitorId: $monitorId" unless ('itors' eq $monitorId); + } + } + + return undef; +} + +sub ZoneMinder_UpdateMonitorAttributes { + my ( $hash, $monitorData, $monitorId ) = @_; + + my $function = ZoneMinder_GetConfigValueByKey($hash, $monitorData, 'Function'); + my $enabled = ZoneMinder_GetConfigValueByKey($hash, $monitorData, 'Enabled'); + my $streamReplayBuffer = ZoneMinder_GetConfigValueByKey($hash, $monitorData, 'StreamReplayBuffer'); + + my $msg = "monitor:$monitorId|$function|$enabled|$streamReplayBuffer"; + + my $dispatchResult = Dispatch($hash, $msg, undef); +} + +sub ZoneMinder_API_CreateMonitors_Callback { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + my @monitors = split(/\{"Monitor"\:\{/, $data); + + foreach my $monitorData (@monitors) { + my $monitorId = ZoneMinder_GetConfigValueByKey($hash, $monitorData, 'Id'); + + if ( $monitorId =~ /^[0-9]+$/ ) { + my $dispatchResult = Dispatch($hash, "createMonitor:$monitorId", undef); + } + } + my $zmApiUrl = ZoneMinder_getZmApiUrl($hash); + ZoneMinder_SimpleGet($hash, "$zmApiUrl/monitors.json", \&ZoneMinder_API_UpdateMonitors_Callback); + + return undef; +} + +sub ZoneMinder_GetCookies { + my ($hash, $header) = @_; + my $name = $hash->{NAME}; + foreach my $cookie ($header =~ m/set-cookie: ?(.*)/gi) { + $cookie =~ /([^,; ]+)=([^,; ]+)[;, ]*(.*)/; + $hash->{HTTPCookieHash}{$1}{Value} = $2; + $hash->{HTTPCookieHash}{$1}{Options} = ($3 ? $3 : ""); + } + $hash->{HTTPCookies} = join ("; ", map ($_ . "=".$hash->{HTTPCookieHash}{$_}{Value}, + sort keys %{$hash->{HTTPCookieHash}})); +} + +sub ZoneMinder_Write { + my ( $hash, $arguments) = @_; + my $method = $arguments->{method}; + + if ($method eq 'changeMonitorFunction') { + + my $zmMonitorId = $arguments->{zmMonitorId}; + my $zmFunction = $arguments->{zmFunction}; + Log3 $hash->{NAME}, 4, "method: $method, monitorId:$zmMonitorId, Function:$zmFunction"; + return ZoneMinder_API_ChangeMonitorState($hash, $zmMonitorId, $zmFunction, undef); + + } elsif ($method eq 'changeMonitorEnabled') { + + my $zmMonitorId = $arguments->{zmMonitorId}; + my $zmEnabled = $arguments->{zmEnabled}; + Log3 $hash->{NAME}, 4, "method: $method, monitorId:$zmMonitorId, Enabled:$zmEnabled"; + return ZoneMinder_API_ChangeMonitorState($hash, $zmMonitorId, undef, $zmEnabled); + + } elsif ($method eq 'changeMonitorAlarm') { + + my $zmMonitorId = $arguments->{zmMonitorId}; + my $zmAlarm = $arguments->{zmAlarm}; + Log3 $hash->{NAME}, 4, "method: $method, monitorId:$zmMonitorId, Alarm:$zmAlarm"; + return ZoneMinder_Trigger_ChangeAlarmState($hash, $zmMonitorId, $zmAlarm); + + } elsif ($method eq 'changeMonitorText') { + + my $zmMonitorId = $arguments->{zmMonitorId}; + my $zmText = $arguments->{text}; + Log3 $hash->{NAME}, 4, "method: $method, monitorId:$zmMonitorId, Text:$zmText"; + return ZoneMinder_Trigger_ChangeText($hash, $zmMonitorId, $zmText); + + } + + return undef; +} + +sub ZoneMinder_API_ChangeMonitorState { + my ( $hash, $zmMonitorId, $zmFunction, $zmEnabled ) = @_; + my $name = $hash->{NAME}; + + my $zmApiUrl = ZoneMinder_getZmApiUrl($hash); + my $apiParam = { + url => "$zmApiUrl/monitors/$zmMonitorId.json", + method => "POST", + callback => \&ZoneMinder_API_ChangeMonitorState_Callback, + hash => $hash, + zmMonitorId => $zmMonitorId, + zmFunction => $zmFunction, + zmEnabled => $zmEnabled + }; + + if ( $zmFunction ) { + $apiParam->{data} = "Monitor[Function]=$zmFunction"; + } elsif ( $zmEnabled || $zmEnabled eq '0' ) { + $apiParam->{data} = "Monitor[Enabled]=$zmEnabled"; + } + + if ($hash->{HTTPCookies}) { + $apiParam->{header} .= "\r\n" if ($apiParam->{header}); + $apiParam->{header} .= "Cookie: " . $hash->{HTTPCookies}; + } + + HttpUtils_NonblockingGet($apiParam); + + return undef; +} + +sub ZoneMinder_API_ChangeMonitorState_Callback { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + if ($data) { + my $monitorId = $param->{zmMonitorId}; + my $logDevHash = $modules{ZM_Monitor}{defptr}{$name.'_'.$monitorId}; + my $function = $param->{zmFunction}; + my $enabled = $param->{zmEnabled}; + Log3 $name, 4, "ZM_Monitor ($name) - ChangeMonitorState callback data: $data, enabled: $enabled"; + + if ($function) { + readingsSingleUpdate($logDevHash, 'monitorFunction', $function, 1); + } elsif ($enabled || $enabled eq '0') { + readingsSingleUpdate($logDevHash, 'motionDetectionEnabled', $enabled, 1); + } + + } else { + Log3 $name, 2, "ZoneMinder ($name) - ChangeMonitorState callback err: $err"; + } + + return undef; +} + +sub ZoneMinder_Trigger_ChangeAlarmState { + my ( $hash, $zmMonitorId, $zmAlarm ) = @_; + my $name = $hash->{NAME}; + + my $msg = "$zmMonitorId|"; + if ( 'on' eq $zmAlarm ) { + DevIo_SimpleWrite( $hash, $msg.'on|1|fhem', 2 ); + } elsif ( 'off' eq $zmAlarm ) { + DevIo_SimpleWrite( $hash, $msg.'off|1|fhem', 2); + } elsif ( $zmAlarm =~ /^on\-for\-timer/ ) { + my $duration = $zmAlarm =~ s/on\-for\-timer\ /on\ /r; + DevIo_SimpleWrite( $hash, $msg.$duration.'|1|fhem', 2); + } + + return undef; +} + +sub ZoneMinder_Trigger_ChangeText { + my ( $hash, $zmMonitorId, $zmText ) = @_; + my $name = $hash->{NAME}; + + my $msg = "$zmMonitorId|show||||$zmText"; + Log3 $name, 4, "ZoneMinder ($name) - Change Text $msg"; + DevIo_SimpleWrite( $hash, $msg, 2 ); + + return undef; +} + +sub ZoneMinder_calcAuthHash { + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 4, "ZoneMinder ($name) - calling calcAuthHash"; + + my ($sec,$min,$curHour,$dayOfMonth,$curMonth,$curYear,$wday,$yday,$isdst) = localtime(); + + my $zmAuthHashSecret = $hash->{helper}{ZM_AUTH_HASH_SECRET}; + if (not $zmAuthHashSecret) { + Log3 $name, 0, "ZoneMinder ($name) - calcAuthHash was called, but no hash secret was found. This shouldn't happen. Please contact the module maintainer."; + return undef; + } + my $username = $hash->{helper}{ZM_USERNAME}; + my $password = $hash->{helper}{ZM_PASSWORD}; + my $hashedPassword = password41($password); + + my $authHash = $zmAuthHashSecret . $username . $hashedPassword . $curHour . $dayOfMonth . $curMonth . $curYear; + my $authKey = md5_hex($authHash); + + readingsSingleUpdate($hash, 'authHash', $authKey, 1); + InternalTimer(gettimeofday() + 3600, "ZoneMinder_calcAuthHash", $hash); + + return undef; +} + +sub ZoneMinder_Shutdown { + ZoneMinder_Undef(@_); +} + +sub ZoneMinder_Undef { + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + DevIo_CloseDev($hash) if (DevIo_IsOpen($hash)); + RemoveInternalTimer($hash); + + return undef; +} + +sub ZoneMinder_Read { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $data = DevIo_SimpleRead($hash); + return if (!defined($data)); # connection lost + + my $buffer = $hash->{PARTIAL}; + $buffer .= $data; + #as long as the buffer contains newlines + while ($buffer =~ m/\n/) { + my $msg; + ($msg, $buffer) = split("\n", $buffer, 2); + chomp $msg; + $msg = "event:$msg"; +# Log3 $name, 3, "ZoneMinder ($name) incoming message $msg."; + my $dispatchResult = Dispatch($hash, $msg, undef); + } + $hash->{PARTIAL} = $buffer; +} + +sub ZoneMinder_DetailFn { + my ( $FW_wname, $deviceName, $FW_room ) = @_; + + my $hash = $defs{$deviceName}; + + my $zmWebUrl = ZoneMinder_getZmWebUrl($hash, 1); + my $zmUsername = urlEncode($hash->{helper}{ZM_USERNAME}); + my $zmPassword = urlEncode($hash->{helper}{ZM_PASSWORD}); + my $zmConsoleUrl = "$zmWebUrl/index.php?username=$zmUsername&password=$zmPassword&action=login&view=console"; + + if ($zmConsoleUrl) { + return ""; + } else { + return undef; + } +} + +sub ZoneMinder_Get { + my ( $hash, $name, $opt, $args ) = @_; + + my $zmApiUrl = ZoneMinder_getZmApiUrl($hash); + if ("autocreateMonitors" eq $opt) { + ZoneMinder_SimpleGet($hash, "$zmApiUrl/monitors.json", \&ZoneMinder_API_CreateMonitors_Callback); + return undef; + } elsif ("updateMonitorConfig" eq $opt) { + ZoneMinder_SimpleGet($hash, "$zmApiUrl/monitors.json", \&ZoneMinder_API_UpdateMonitors_Callback); + return undef; + } elsif ("calcAuthHash" eq $opt) { + ZoneMinder_calcAuthHash($hash); + return undef; + } + +# Log3 $name, 3, "ZoneMinder ($name) - Get done ..."; + return "Unknown argument $opt, choose one of autocreateMonitors updateMonitorConfig calcAuthHash"; +} + +sub ZoneMinder_Set { + my ( $hash, $param ) = @_; + + my $name = $hash->{NAME}; +# Log3 $name, 3, "ZoneMinder ($name) - Set done ..."; + return undef; +} + +sub ZoneMinder_Ready { + my ( $hash ) = @_; + my $name = $hash->{NAME}; + + if ( $hash->{STATE} eq "disconnected" ) { + my $err = DevIo_OpenDev($hash, 1, undef ); #if success, $err is undef + if (not $err) { + Log3 $name, 3, "ZoneMinder ($name) - reconnect to ZoneMinder successful"; + return 1; + } else { + Log3 $name, 0, "ZoneMinder ($name) - reconnect to ZoneMinder failed: $err"; + return $err; + } + } + + # This is relevant for Windows/USB only + if(defined($hash->{USBDev})) { + my $po = $hash->{USBDev}; + my ( $BlockingFlags, $InBytes, $OutBytes, $ErrorFlags ) = $po->status; + return ( $InBytes > 0 ); + } +} + +1; + + +# Beginn der Commandref + +=pod +=item device +=item summary Maintain ZoneMinder events and monitor operation modes in FHEM +=item summary_DE ZoneMinder events und Monitor Konfiguration in FHEM warten + +=begin html + + +

ZoneMinder

+ + + Define +
    + define <name> ZoneMinder <ZM-Host> [<username> <password>] +

    + Defines a ZoneMinder device at the given host address. This allows you to exchange events between ZoneMinder and FHEM. + Also providing username and password provides access to ZoneMinder API and more functionality. +
    + Example: +
      + define zm ZoneMinder 10.0.0.100
      + define zm ZoneMinder 10.0.0.100 fhemApiUser fhemApiPass +
    +
    +
+

+ + + Get +
    +
  • autocreateMonitors
    Queries the ZoneMinder API and autocreates all ZM_Monitor devices that belong to that installation. +
  • +
  • updateMonitorConfig
    Queries the ZoneMinder API and updates the Readings of ZM_Monitor devices (monitorFunction, motionDetectionEnabled, ...) +
  • +
  • calcAuthHash
    Calculates a fresh auth hash. Please note that the hash only changes with every full hour. So, calling this doesn't necessarily change any Readings, depending on the age of the current hash. +
  • +
+ +

+ + Attributes +

+
    +
  • publicAddress <address>
    This configures public accessibility of your LAN (eg your ddns address). Define a valid URL here, eg https://my.own.domain:2344
  • +
  • webConsoleContext <path>
    If not set, this defaults to /zm. This is used for building the URL to the ZoneMinder web console.
  • +
+ +

+ + + Readings +

+
    +
  • CPU_Load
    The CPU load of the ZoneMinder host. Provides 1, 5 and 15 minutes interval.
  • +
  • authHash
    The auth hash that allows access to Stream URLs without requiring username or password.
  • +
  • state
    The current connection state to the ZoneMinder Trigger Port (6802 per default)
  • +
+ + +=end html + +# Ende der Commandref +=cut diff --git a/fhem/FHEM/71_ZM_Monitor.pm b/fhem/FHEM/71_ZM_Monitor.pm new file mode 100755 index 000000000..e61ff6be5 --- /dev/null +++ b/fhem/FHEM/71_ZM_Monitor.pm @@ -0,0 +1,504 @@ +############################################################################## +# +# 71_ZM_Monitor.pm +# +# This file is part of Fhem. +# +# Fhem 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. +# +# Fhem 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 Fhem. If not, see . +# +############################################################################## +# +# ZoneMinder (c) Martin Gutenbrunner / https://github.com/delmar43/FHEM +# +# This module is designed to work as a logical device in connection with 70_ZoneMinder +# as a physical device. +# +# Discussed in FHEM Forum: https://forum.fhem.de/index.php/topic,91847.0.html +# +# $Id$ +# +############################################################################## + +package main; +use strict; +use warnings; +use HttpUtils; + +my @ZM_Functions = qw( None Monitor Modect Record Mocord Nodect ); +my @ZM_Alarms = qw( on off on-for-timer ); + +sub ZM_Monitor_Initialize { + my ($hash) = @_; + $hash->{NotifyOrderPrefix} = "71-"; + + $hash->{GetFn} = "ZM_Monitor_Get"; + $hash->{SetFn} = "ZM_Monitor_Set"; + $hash->{DefFn} = "ZM_Monitor_Define"; + $hash->{UndefFn} = "ZM_Monitor_Undef"; + $hash->{FW_detailFn} = "ZM_Monitor_DetailFn"; + $hash->{ParseFn} = "ZM_Monitor_Parse"; + $hash->{NotifyFn} = "ZM_Monitor_Notify"; + + $hash->{AttrList} = 'showLiveStreamInDetail:0,1 '.$readingFnAttributes; + $hash->{Match} = "^.*"; + + return undef; +} + +sub ZM_Monitor_Define { + my ( $hash, $def ) = @_; + $hash->{NOTIFYDEV} = "TYPE=ZoneMinder"; + + my @a = split( "[ \t][ \t]*", $def ); + + my $name = $a[0]; + my $module = $a[1]; + my $zmMonitorId = $a[2]; + + if(@a < 3 || @a > 3) { + my $msg = "ZM_Monitor ($name) - Wrong syntax: define ZM_Monitor "; + Log3 $name, 2, $msg; + return $msg; + } + + $hash->{NAME} = $name; + readingsSingleUpdate($hash, "state", "idle", 1); + + AssignIoPort($hash); + + my $ioDevName = $hash->{IODev}{NAME}; + my $logDevAddress = $ioDevName.'_'.$zmMonitorId; + # Adresse rückwärts dem Hash zuordnen (für ParseFn) +# Log3 $name, 3, "ZM_Monitor ($name) - Logical device address: $logDevAddress"; + $modules{ZM_Monitor}{defptr}{$logDevAddress} = $hash; + +# Log3 $name, 3, "ZM_Monitor ($name) - Define done ... module=$module, zmHost=$zmHost, zmMonitorId=$zmMonitorId"; + + $hash->{helper}{ZM_MONITOR_ID} = $zmMonitorId; + + ZM_Monitor_UpdateStreamUrls($hash); + + return undef; +} + +sub ZM_Monitor_UpdateStreamUrls { + my ( $hash ) = @_; + my $ioDevName = $hash->{IODev}{NAME}; + + my $zmPathZms = $hash->{IODev}{helper}{ZM_PATH_ZMS}; + if (not $zmPathZms) { + return undef; + } + + my $zmHost = $hash->{IODev}{helper}{ZM_HOST}; + my $streamUrl = "http://$zmHost"; + my $zmUsername = urlEncode($hash->{IODev}{helper}{ZM_USERNAME}); + my $zmPassword = urlEncode($hash->{IODev}{helper}{ZM_PASSWORD}); + my $authPart = "&user=$zmUsername&pass=$zmPassword"; + + readingsBeginUpdate($hash); + ZM_Monitor_WriteStreamUrlToReading($hash, $streamUrl, 'streamUrl', $authPart); + + my $pubStreamUrl = $attr{$ioDevName}{publicAddress}; + if ($pubStreamUrl) { + my $authHash = ReadingsVal($ioDevName, 'authHash', ''); + if ($authHash) { #if ZM_AUTH_KEY is defined, use the auth-hash. otherwise, use the previously defined username/pwd + $authPart = "&auth=$authHash"; + } + ZM_Monitor_WriteStreamUrlToReading($hash, $pubStreamUrl, 'pubStreamUrl', $authPart); + } + readingsEndUpdate($hash, 1); + + return undef; +} + +# is build by using hosname, NPH_ZMS, monitorId, streamBufferSize, and auth +sub ZM_Monitor_getZmStreamUrl { + my ($hash) = @_; + + #use private or public LAN for streaming access? + + return undef; +} + +sub ZM_Monitor_WriteStreamUrlToReading { + my ( $hash, $streamUrl, $readingName, $authPart ) = @_; + my $name = $hash->{NAME}; + + my $zmPathZms = $hash->{IODev}{helper}{ZM_PATH_ZMS}; + my $zmMonitorId = $hash->{helper}{ZM_MONITOR_ID}; + my $buffer = ReadingsVal($name, 'streamReplayBuffer', '1000'); + + my $imageUrl = $streamUrl."$zmPathZms?mode=single&scale=100&monitor=$zmMonitorId".$authPart; + my $imageReadingName = $readingName; + $imageReadingName =~ s/Stream/Image/g; + readingsBulkUpdate($hash, $imageReadingName, $imageUrl, 1); + + $streamUrl = $streamUrl."$zmPathZms?mode=jpeg&scale=100&maxfps=30&buffer=$buffer&monitor=$zmMonitorId".$authPart; + readingsBulkUpdate($hash, $readingName, "$streamUrl", 1); +} + +sub ZM_Monitor_DetailFn { + my ( $FW_wname, $deviceName, $FW_room ) = @_; + + my $hash = $defs{$deviceName}; + my $name = $hash->{NAME}; + + my $showLiveStream = $attr{$name}{showLiveStreamInDetail}; + return "
To view a live stream here, execute: attr $name showLiveStreamInDetail 1
" if (not $showLiveStream); + + my $streamDisabled = (ReadingsVal($deviceName, 'monitorFunction', 'None') eq 'None'); + if ($streamDisabled) { + return '
Streaming disabled
'; + } + + my $streamUrl = ReadingsVal($deviceName, 'pubStreamUrl', undef); + if (not $streamUrl) { + $streamUrl = ReadingsVal($deviceName, 'streamUrl', undef); + } + if ($streamUrl) { + return "
"; + } else { + return undef; + } +} + +sub ZM_Monitor_Undef { + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + return undef; +} + +sub ZM_Monitor_Get { + my ( $hash, $name, $opt, @args ) = @_; + +# return "Unknown argument $opt, choose one of config"; + return undef; +} + +sub ZM_Monitor_Set { + my ( $hash, $name, $cmd, @args ) = @_; + + if ( "monitorFunction" eq $cmd ) { + my $arg = $args[0]; + if (grep { $_ eq $arg } @ZM_Functions) { + my $arguments = { + method => 'changeMonitorFunction', + zmMonitorId => $hash->{helper}{ZM_MONITOR_ID}, + zmFunction => $arg + }; + my $result = IOWrite($hash, $arguments); + return $result; + } + return "Unknown value $arg for $cmd, choose one of ".join(' ', @ZM_Functions); + } elsif ("motionDetectionEnabled" eq $cmd ) { + my $arg = $args[0]; + if ($arg eq '1' || $arg eq '0') { + my $arguments = { + method => 'changeMonitorEnabled', + zmMonitorId => $hash->{helper}{ZM_MONITOR_ID}, + zmEnabled => $arg + }; + my $result = IOWrite($hash, $arguments); + return $result; + } + return "Unknown value $arg for $cmd, choose one of 0 1"; + } elsif ("alarmState" eq $cmd) { + my $arg = $args[0]; + if (grep { $_ eq $arg } @ZM_Alarms) { + + $arg .= ' '.$args[1] if ( 'on-for-timer' eq $arg ); + my $arguments = { + method => 'changeMonitorAlarm', + zmMonitorId => $hash->{helper}{ZM_MONITOR_ID}, + zmAlarm => $arg + }; + my $result = IOWrite($hash, $arguments); + return $result; + } + return "Unknown value $arg for $cmd, chose one of ".join(' '. @ZM_Alarms); + } elsif ("text" eq $cmd) { + my $arg = join ' ', @args; + if (not $arg) { + $arg = ''; + } + + my $arguments = { + method => 'changeMonitorText', + zmMonitorId => $hash->{helper}{ZM_MONITOR_ID}, + text => $arg + }; + my $result = IOWrite($hash, $arguments); + return $result; + } + + return 'monitorFunction:'.join(',', @ZM_Functions).' motionDetectionEnabled:0,1 alarmState:on,off,on-for-timer text'; +} + +# incoming messages from physical device module (70_ZoneMinder in this case). +sub ZM_Monitor_Parse { + my ( $io_hash, $message) = @_; + + my @msg = split(/\:/, $message, 2); + my $msgType = $msg[0]; + if ($msgType eq 'event') { + return ZM_Monitor_handleEvent($io_hash, $msg[1]); + } elsif ($msgType eq 'createMonitor') { + return ZM_Monitor_handleMonitorCreation($io_hash, $msg[1]); + } elsif ($msgType eq 'monitor') { + return ZM_Monitor_handleMonitorUpdate($io_hash, $msg[1]); + } else { + Log3 $io_hash, 0, "Unknown message type: $msgType"; + } + + return undef; +} + +sub ZM_Monitor_handleEvent { + my ( $io_hash, $message ) = @_; + + my $ioName = $io_hash->{NAME}; + my @msgTokens = split(/\|/, $message); + my $zmMonitorId = $msgTokens[0]; + my $alertState = $msgTokens[1]; + my $eventTs = $msgTokens[2]; + my $eventId = $msgTokens[3]; + + my $logDevAddress = $ioName.'_'.$zmMonitorId; + Log3 $io_hash, 5, "Handling event for logical device $logDevAddress"; + # wenn bereits eine Gerätedefinition existiert (via Definition Pointer aus Define-Funktion) + if(my $hash = $modules{ZM_Monitor}{defptr}{$logDevAddress}) { + Log3 $hash, 5, "Logical device $logDevAddress found. Writing readings"; + + readingsBeginUpdate($hash); + ZM_Monitor_createEventStreamUrl($hash, $eventId); + my $state; + if ($alertState eq "on") { + $state = "alert"; + } elsif ($alertState eq "off") { + $state = "idle"; + } + readingsBulkUpdate($hash, "state", $state, 1); + readingsBulkUpdate($hash, "alert", $alertState, 1); + readingsBulkUpdate($hash, "lastEventTimestamp", $eventTs); + readingsBulkUpdate($hash, "lastEventId", $eventId); + readingsEndUpdate($hash, 1); + + Log3 $hash, 5, "Writing readings done. Now returning log dev name: $hash->{NAME}"; + # Rückgabe des Gerätenamens, für welches die Nachricht bestimmt ist. + return $hash->{NAME}; + } else { + # Keine Gerätedefinition verfügbar. Daher Vorschlag define-Befehl: + my $autocreate = "UNDEFINED ZM_Monitor_$logDevAddress ZM_Monitor $zmMonitorId"; + Log3 $io_hash, 5, "logical device with address $logDevAddress not found. returning autocreate: $autocreate"; + return $autocreate; + } +} + +#for now, this is nearly a duplicate of writing the streamUrl reading. +#will need some love to make better use of existing code. +sub ZM_Monitor_createEventStreamUrl { + my ( $hash, $eventId ) = @_; + my $ioDevName = $hash->{IODev}{NAME}; + + my $zmPathZms = $hash->{IODev}{helper}{ZM_PATH_ZMS}; + if (not $zmPathZms) { + return undef; + } + + my $zmHost = $hash->{IODev}{helper}{ZM_HOST}; + my $streamUrl = "http://$zmHost"; + my $zmUsername = urlEncode($hash->{IODev}{helper}{ZM_USERNAME}); + my $zmPassword = urlEncode($hash->{IODev}{helper}{ZM_PASSWORD}); + my $authPart = "&user=$zmUsername&pass=$zmPassword"; + ZM_Monitor_WriteEventStreamUrlToReading($hash, $streamUrl, 'eventStreamUrl', $authPart, $eventId); + + my $pubStreamUrl = $attr{$ioDevName}{publicAddress}; + if ($pubStreamUrl) { + my $authHash = ReadingsVal($ioDevName, 'authHash', ''); + if ($authHash) { #if ZM_AUTH_KEY is defined, use the auth-hash. otherwise, use the previously defined username/pwd + $authPart = "&auth=$authHash"; + } + ZM_Monitor_WriteEventStreamUrlToReading($hash, $pubStreamUrl, 'pubEventStreamUrl', $authPart, $eventId); + } +} + +sub ZM_Monitor_handleMonitorUpdate { + my ( $io_hash, $message ) = @_; + + my $ioName = $io_hash->{NAME}; + my @msgTokens = split(/\|/, $message); #$message = "$monitorId|$function|$enabled|$streamReplayBuffer"; + my $zmMonitorId = $msgTokens[0]; + my $function = $msgTokens[1]; + my $enabled = $msgTokens[2]; + my $streamReplayBuffer = $msgTokens[3]; + my $logDevAddress = $ioName.'_'.$zmMonitorId; + + if ( my $hash = $modules{ZM_Monitor}{defptr}{$logDevAddress} ) { + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash, 'monitorFunction', $function); + readingsBulkUpdateIfChanged($hash, 'motionDetectionEnabled', $enabled); + my $bufferChanged = readingsBulkUpdateIfChanged($hash, 'streamReplayBuffer', $streamReplayBuffer); + readingsEndUpdate($hash, 1); + + ZM_Monitor_UpdateStreamUrls($hash); + + return $hash->{NAME}; +# } else { +# my $autocreate = "UNDEFINED ZM_Monitor_$logDevAddress ZM_Monitor $zmMonitorId"; +# Log3 $io_hash, 5, "logical device with address $logDevAddress not found. returning autocreate: $autocreate"; +# return $autocreate; + } + + return undef; +} + +sub ZM_Monitor_handleMonitorCreation { + my ( $io_hash, $message ) = @_; + + my $ioName = $io_hash->{NAME}; + my @msgTokens = split(/\|/, $message); #$message = "$monitorId"; + my $zmMonitorId = $msgTokens[0]; + my $logDevAddress = $ioName.'_'.$zmMonitorId; + + if ( my $hash = $modules{ZM_Monitor}{defptr}{$logDevAddress} ) { + return $hash->{NAME}; + } else { + my $autocreate = "UNDEFINED ZM_Monitor_$logDevAddress ZM_Monitor $zmMonitorId"; + Log3 $io_hash, 5, "logical device with address $logDevAddress not found. returning autocreate: $autocreate"; + return $autocreate; + } + + return undef; +} + +sub ZM_Monitor_WriteEventStreamUrlToReading { + my ( $hash, $streamUrl, $readingName, $authPart, $eventId ) = @_; + + my $zmPathZms = $hash->{IODev}{helper}{ZM_PATH_ZMS}; + $streamUrl = $streamUrl."/" if (not $streamUrl =~ m/\/$/); + + my $zmMonitorId = $hash->{helper}{ZM_MONITOR_ID}; + my $imageUrl = $streamUrl."$zmPathZms?mode=single&scale=100&monitor=$zmMonitorId".$authPart; + my $imageReadingName = $readingName; + $imageReadingName =~ s/Stream/Image/g; + readingsBulkUpdate($hash, $imageReadingName, $imageUrl, 1); + + $streamUrl = $streamUrl."$zmPathZms?source=event&mode=jpeg&event=$eventId&frame=1&scale=100&rate=100&maxfps=30".$authPart; + readingsBulkUpdate($hash, $readingName, $streamUrl, 1); + +} + +sub ZM_Monitor_Notify { + my ($own_hash, $dev_hash) = @_; + my $name = $own_hash->{NAME}; # own name / hash + + return "" if(IsDisabled($name)); # Return without any further action if the module is disabled + + my $devName = $dev_hash->{NAME}; # Device that created the events + + my $events = deviceEvents($dev_hash,1); + return if( !$events ); + + foreach my $event (@{$events}) { + $event = "" if(!defined($event)); + Log3 $name, 4, "ZM_Monitor ($name) - Incoming event: $event"; + + my @msg = split(/\:/, $event, 2); + if ($msg[0] eq 'authHash') { + ZM_Monitor_UpdateStreamUrls($own_hash); + } else { + Log3 $name, 4, "ZM_Monitor ($name) - ignoring"; + } + + # Examples: + # $event = "readingname: value" + # or + # $event = "INITIALIZED" (for $devName equal "global") + # + # processing $event with further code + } +} + +# Eval-Rückgabewert für erfolgreiches +# Laden des Moduls +1; + + +# Beginn der Commandref + +=pod +=item device +=item summary Logical device to change Monitor operation modes in ZoneMinder +=item summary_DE Logisches Modul zum Verändern der Kameraeinstellungen in ZoneMinder + +=begin html + + +

ZM_Monitor

+ + + Define +
    + define <name> ZM_Monitor <ZM-Monitor ID> +

    + This is usually called by autocreate and triggered by the ZoneMinder IODevice. +
    +
+

+ + + Set +
    +
  • alarmState
    Puts a monitor into alarm state or out of alarm state via the ZoneMinder trigger port.
  • +
  • monitorFunction
    Sets the operating mode of a Monitor in ZoneMinder via the ZoneMinder API.
  • +
  • motionDetectionEnabled
    Enables or disables monitor detection of a monitor via ZoneMinder API.
  • +
  • text
    Allows you to set a text for a Timestamp's %Q portion in ZoneMinder via the ZoneMinder trigger port.
  • +
+ +

+ + Attributes +

+
    +
  • showLiveStreamInDetail
    If set to 1, a live-stream of the current monitor will be shown on top of the FHEMWEB detail page.
  • +
+ +

+ + + Readings +

+
    +
  • alert
    The alert state.
  • +
  • eventImageUrl
    Link to the first image of the latest event recording, based on the ZM-Host parameter used in the device definition.
  • +
  • eventStreamUrl
    Link to the latest event recording, based on the ZM-Host parameter used in the device definition.
  • +
  • lastEventId
    ID of the latest event in ZoneMinder.
  • +
  • lastEventTimestamp
    Timestamp of the latest event from ZoneMinder.
  • +
  • monitorFunction
    Current operation mode of the monitor.
  • +
  • motionDetectionEnabled
    Equals the 'enabled' setting in ZoneMinder. Allows you to put the monitor into a more passive state (according to ZoneMinder documentation).
  • +
  • pubEventImageUrl
    Link to the first image of the latest event recording, based on the publicAddress attribute used in the ZoneMinder device.
  • +
  • pubEventStreamUrl
    Link to the latest event recording, based on the publicAddress attribute used in the ZoneMinder device.
  • +
  • pubImageUrl
    Link to the current live image, based on the publicAddress attribute used in the ZoneMinder device.
  • +
  • pubStreamUrlLink to the live-stream, based on the publicAddress attribute used in the ZoneMinder device.
  • +
  • streamReplayBuffer
    Taken from the ZoneMinder configuration. Used for the buffer parameter of stream URLs.
  • +
  • streamUrl
    Link to the live-stream, based on the ZM-Host parameter used in the device definition.
  • + +
+ +=end html + +# Ende der Commandref +=cut