//******************************************************************************
//* Hikvision Camera Controller - Device Driver for Hubitat Elevation
//******************************************************************************
// Copyright 2024 Thomas R Schmidt, Wildwood IL
//******************************************************************************
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//******************************************************************************
// This driver allows you to trigger Alarm Input events on the camera and
// enable/disable Motion Detection features by running its custom commands
// from your rules and apps. It also acts as a Motion Sensor in HE if your
// camera has the Alarm Server/HTTP Listening feature available.
//
// The User Guide is required reading.
// https://tr-systems.github.io/web/HCC_UserGuide.html
// Contact for support: trsystems.help at the little G mail place.
//******************************************************************************
// Change Log
// Date Version Release Notes
// 24-01-26 1.0.0 First Release: Please refer to the User Guide
// 24-02-05 1.0.1 Remove/Replace Ping function from Ok2Run method for all Commands
// * Instead, set zDriver OFF if GET request for current state times out
// * Test/Practice versioning and update with HPM
// 24-02-06 1.0.2 Add link to User Guide on device driver page (provided by jtp10181)
// 24-02-07 1.0.3 Remove Link to User Guide from top of device driver page due to
// overlay of Events & Logs buttons when viewing device on a phone.
// 24-02-22 1.0.4 Bug fix: Update old Hikvision IPMD url paths to ISAPI paths.
// * Affected features: Basic Motion, Alarm Out trigger, IO Status.
// * Required to support newer cameras. May break older cams.
// 24-02-23 1.0.5 Bug fix: Null value exception for camera name when logging GET error during save
// Alarm Server: Log event messages for "Unknown" events for reporting to tr-systems.
// 24-02-26 1.0.6 Alarm Server: Add support for eventType "duration" and associated relationEvent
// 24-03-07 1.1.0 Add Pushable Button for Motion Events
// 24-03-11 2.0.0 Your Choice of Driver: Alarm Server, Controller or Both
// + Minimize Camera Validation requirements in Saving Preferences
// + Change Motion and Alarm I/O Feature Attributes to State Variables
// + Tested upgrade from 1.0.5 and 1.1.0 without saving preferences on existing devices
// 24-05-10 2.1.0 Add support for Video Tampering to Controller and Alarm Server
// 24-05-12 2.1.1 Bug fix for Video Tampering Event
//******************************************************************************
import groovy.transform.Field // Needed to use @Field static lists/maps
//******************************************************************************
@Field static final String DRIVER = "HCC 2.1.1"
@Field static final String USER_GUIDE = "https://tr-systems.github.io/web/HCC_UserGuide.html"
//******************************************************************************
metadata {
definition (name: "Hikvision Camera Controller",
author: "Thomas R Schmidt", namespace: "tr-systems", // github userid
singleThreaded: true) //
{
capability "Actuator"
capability "Switch"
capability "MotionSensor"
capability "PushableButton" //** v110
command "on" , [[name:"Trigger Alarm"]]
command "off" , [[name:"Clear Alarm"]]
command "Enable", [[name:"Features",type:"STRING",description:"Features: m.p.in.lc.re.rx.or.ub.vt.ai"]] //** v210
command "Disable", [[name:"Features",type:"STRING",description:"Features: m.p.in.lc.re.rx.or.ub.vt.ai"]] //** v210
command "push", [[name: "Event", type: "NUMBER", description: "1=in 2=lc 3=m 4=p 5=or 6=re 7=rx 8=ub 9=vt"]] //** v110, v210
attribute "motion", "STRING" // active/inactive
attribute "numberOfButtons", "NUMBER" // v110
attribute "pushed", "NUMBER" // last button pushed ** v110
attribute "switch", "STRING" // alarm on/off follows AlarmIn state ** v110
attribute "zDriver", "STRING" // State of this device in HE: OK, ERR, OFF, CRED
// OK = Everything is groovy
// ERR = Unexpected get/put errors occurred.
// OFF = Camera is offline
// CRED = Authentication failed, credentials on the camera have changed
// FAILED = Only during camera validation when saving preferences
}
preferences
{
input(name: "devUse", type: "enum", //** v200
title:"Select Driver Components",
description: " ",
options: ["Alarm Server","Controller","Both"],
defaultValue: "Both",
required: true)
input(name: "devIP", type: "string",
description: " ",
title:"Camera or NVR IP Address",
required: true)
input(name: "devPort", type: "string",
title:"Camera or NVR Virtual Port",
description: "(for controller)",
defaultValue: "80",
required: false) //** v200
input(name: "devCred", type: "password",
title:"Credentials for Login",
description: "userid:password (for controller)",
required: false) //** v200
input(name: "devMotionReset", type: "number",
title:"Reset Interval for Motion Detection",
description: "(From 1 to 20 minutes)",
range: "1..20",
devaultValue: 1,
required: false) //** v200
input(name: "devResetCounters", type: "enum",
title:"Reset Alarm Server Counters",
options: ["Daily","Weekly","Monthly","Only on Save"],
defaultValue: "Only on Save",
required: false) //** v200
input(name: "devExclude", type: "string",
title:"Exclude from Controller",
description: "List: ai.ao.m.p.in.lc.rx.re.or.ub.vt", //** v210
required: false)
input(name: "devExcludeA", type: "string", //** v200
title:"Exclude from Alarm Server",
description: "List: m.p.in.lc.rx.re.or.ub.vt", //** v210
required: false)
input(name: "devName", type: "string", //** v200
title:"Optional Name for Logging",
description: " ",
required: false)
input(name: "debug", type: "bool",
title: "Debug logging for Controller",
description: "(resets in 30 minutes)",
defaultValue: false)
input(name: "debuga", type: "bool",
title: "Debug logging for Alarm Server",
description: "(resets in 30 minutes)",
defaultValue: false)
// Link to User Guide
input name: "UserGuide", type: "hidden", title: fmtHelpInfo("User Guide")
}
}
//******************************************************************************
// This "global" is used to pass status (OK or error msg) back from the
// SendGet/Put Request methods, then used for logging and program control in the
// calling methods. Don't mess with strMsg unless you know what you're doing.
String strMsg = " "
Boolean SavingPreferences = false
//******************************************************************************
// CODER BEWARE HACK provided by jtp10181 - unsupported - undocumented
//******************************************************************************
String fmtHelpInfo(String str) {
String prefLink = "${str}
${DRIVER}
"
return "
${prefLink}
"
}
//******************************************************************************
// INSTALLED - INSTALLED - INSTALLED - Installing New Camera Device
//******************************************************************************
void installed() {
log.warn "Installing new camera"
log.info "Setting device Name to Label: " + device.getLabel()
device.setName(device.getLabel())
sendEvent(name:"zDriver",value:"Please read the User Guide before adding your first camera (scroll down for link)")
}
//******************************************************************************
// UPDATED - UPDATED - UPDATED - Preferences Saved
//******************************************************************************
void updated() {
String errcd = ""
String dni = ""
String cname = "" //** v200
unschedule()
state.clear()
//************************************************************* v200 START
cname = device.getLabel()
if (devName == null || devName.trim() == "") {
devName = cname
device.updateSetting("devName", [value:"$cname", type:"string"])
}
cname = cname.toUpperCase()
log.warn "Saving Preferences for " + cname + ", using " + devUse
if (devMotionReset == null) {device.updateSetting("devMotionReset", [value:1, type:"number"])}
if (devUse == "Alarm Server") {
log.info "Using Motion Reset Interval: " + device.getSetting("devMotionReset")
devIP = devIP.trim()
device.updateSetting("devIP", [value:"${devIP}", type:"string"])
if (GenerateDNI(devIP) == "ERR") {
sendEvent(name:"zDriver",value:"FAILED")
return
}
log.info "$cname Alarm Server now waiting for Event messages from " + devIP
sendEvent(name:"motion",value:"inactive")
sendEvent(name:"numberOfButtons",value:9) //** v210
sendEvent(name:"zDriver",value:"OK")
return
}
//************************************************************* v200 END
devIP = devIP.trim()
devPort = devPort.trim()
devCred = devCred.trim()
device.updateSetting("devIP", [value:"${devIP}", type:"string"])
device.updateSetting("devPort", [value:"${devPort}", type:"string"])
device.updateSetting("devCred", [value:"${devCred}", type:"string"])
// Start Fresh
device.removeDataValue("Name") //** v200
device.removeDataValue("Model")
device.removeDataValue("Firmware")
device.updateDataValue("CamID",devCred.bytes.encodeBase64().toString())
if (devCred == null) {
log.error "Credentials are required"
sendEvent(name:"zDriver",value:"FAILED")
return
}
long port = devPort.toInteger()
if (port >= 65001 && devUse == "Both") {
log.error "NVR Virtual Port is for Controller use only"
sendEvent(name:"zDriver",value:"FAILED")
return
}
if (devCred.length() > 6 && devCred.substring(0,6) == "admin:") {
log.error "Hikvision admin account not allowed"
sendEvent(name:"zDriver",value:"FAILED")
return
}
errcd = GetCameraInfo()
if (errcd != "OK") {
sendEvent(name:"zDriver",value:"FAILED")
return
}
// Validate the Operator account
errcd = GetUserInfo()
if (errcd != "OK") {
sendEvent(name:"zDriver",value:"FAILED")
return
}
//************************************************************* v200 START
if (devUse == "Both") {
if (GenerateDNI(devIP) == "ERR") {
sendEvent(name:"zDriver",value:"FAILED")
return
}
}
SavingPreferences = true
//************************************************************* v200 END
errcd = GetSetStates()
// Returns OK, NA w/StrMsg=new exclude filter, or ERR/CRED w/strMsg=error message
if (errcd == "ERR" || errcd == "CRED") {
sendEvent(name:"zDriver",value:"FAILED")
return
}
SavingPreferences = false //** v200
// Add features not found to the Exclude filter
if (errcd == "NA") {
strMsg = strMsg + devExclude
log.warn "Setting new Exclude Filter:" + strMsg
device.updateSetting("devExclude", [value:"${strMsg}", type:"string"])
}
if (debug || debuga) {runIn(1800, ResetDebugLogging, overwrite)}
log.warn "$cname validated, ready for operation"
if (devUse == "Both") { //** v200
sendEvent(name:"motion",value:"inactive") //** v200
sendEvent(name:"numberOfButtons",value:9) //** v200,v210
}
sendEvent(name:"switch",value:"off") //** v110
sendEvent(name:"zDriver",value:"OK")
}
//***************************************************************** v200 START
// GENERATE DNI - GENERATE DNI - GENERATE DNI - GENERATE DNI
//******************************************************************************
private GenerateDNI(String ip) {
String dni = ip.tokenize(".").collect {String.format( "%02x", it.toInteger() ) }.join()
dni = dni.toUpperCase()
try {device.deviceNetworkId = "${dni}"
} catch (Exception e) {
log.error e.message
return("ERR")
}
return(dni)
}
//******************************************************************* v200 END
// RESET DEBUG LOGGING - RESET DEBUG LOGGING - RESET DEBUG LOGGING
//******************************************************************************
void ResetDebugLogging() {
log.info "Debug logging is off"
device.updateSetting("debug", [value:false, type:"bool"])
device.updateSetting("debuga", [value:false, type:"bool"])
}
//******************************************************************************
// GET CAMERA INFO - GET CAMERA INFO - GET CAMERA INFO - GET CAMERA INFO
//******************************************************************************
private GetCameraInfo() {
String errcd = ""
log.info "GET: http://" + devIP + ":" + devPort + FeaturePaths.SysInfo
// If the response from the GET request is successful, the xml returned
// will be presented in the format requested and strMsg will be "OK".
// Otherwise, xml will be null and strMsg will contain the error message.
// Further analysis of GET errors is then performed by LogGETError.
// This applies to all calls to the SendGet and SendPut Request methods.
// Don't mess with strMsg unless you know what you're doing.
strMsg = ""
def xml = SendGetRequest(FeaturePaths.SysInfo,"GPATH")
if (strMsg != "OK") {
errcd = LogGETError()
return(errcd)
}
log.info "Device Type: " + xml.deviceType.text()
log.info "Name: " + xml.deviceName.text()
log.info "Model: " + xml.model.text()
log.info "Firmware: " + xml.firmwareVersion.text() + " " + xml.firmwareReleasedDate.text()
if (xml.deviceType.text() == "NVR" || xml.deviceType.text() == "DVR") {
strMsg = "You have connected to a Hikvision NVR/DVR, Use the Virtual Host Port or POE Subnet address to access the camera"
log.error strMsg
return("ERR")
}
device.updateDataValue("Name",xml.deviceName.text())
device.updateDataValue("Model",xml.model.text())
device.updateDataValue("Firmware",xml.firmwareVersion.text() + " " + xml.firmwareReleasedDate.text())
return("OK")
}
//******************************************************************************
// GET USER INFO - GET USER INFO - GET USER INFO - GET USER INFO - GET USER INFO
//******************************************************************************
private GetUserInfo() {
String errcd = ""
log.info "Validating Operator account"
// This GET will only return the user being used, not the entire list
// Only the admin account gets the entire list of users
// So glad it does this because now its easy to get the user id for the next step
xml = SendGetRequest(FeaturePaths.CamUsers,"GPATH")
if (strMsg != "OK") {
errcd = LogGETError()
return(errcd)
}
String userid = xml.User.id.text()
log.info "UserID: " + userid
log.info "UserLevel: " + xml.User.userLevel.text()
if (xml.User.userLevel.text() != "Operator") {
strMsg = "User account on camera is not an Operator"
log.warn strMsg
return("ERR")
}
String path = FeaturePaths.UserPerm + userid
// Get User Permissions
xml = SendGetRequest(path,"GPATH")
if (strMsg != "OK") {
errcd = LogGETError()
return(errcd)
}
if (xml.remotePermission.parameterConfig.text() != "true" || xml.remotePermission.alarmOutOrUpload.text() != "true") {
strMsg = "Operator account on camera must have both Remote Parameters and Remote Notify options selected."
log.error strMsg
return("ERR")
}
log.info "Operator account validated"
return("OK")
}
//******************************************************************************
// GET SET STATES - GET SET STATES - GET SET STATES - GET SET STATES
//******************************************************************************
def GetSetStates() {
Boolean err = false
Boolean na = false
String newfilter = ""
String errcd = " "
log.info "Initializing the State of All Available Features"
if (devExclude == null) {devExclude = ""}
errcd = "OK"
sendEvent(name:"motion",value:"inactive")
for (feature in FeatureCodesToName) {
if (!devExclude.contains("$feature.key")) {
errcd = GetSetFeatureState("$feature.value")
if (errcd == "ERR" || errcd == "CRED") {break}
if (errcd == "NA") {
na = true
newfilter = newfilter + feature.key + "."
}
} else {
// sendEvent(name:"$feature.value", value:"NA") //** v200
state."$feature.value" = "NA" //** v200
}
}
if (errcd == "ERR" || errcd == "CRED") {return(errcd)}
if (na) {
errcd = "NA"
strMsg = newfilter
}
return(errcd)
}
//******************************************************************************
// GET SET FEATURE STATE - GET SET FEATURE STATE - GET SET FEATURE STATE
//******************************************************************************
// Returns errcd OK,NA,ERR,CRED w/strMsg=error message
private GetSetFeatureState(String Feature) {
String errcd = ""
String camstate = ""
String Path = FeaturePaths."$Feature"
def xml = SendGetRequest(Path, "GPATH")
if (strMsg == "OK") {
if (Feature == "AlarmIO") {
camstate = xml.IOPortStatus[0].ioState.text()
log.info "AlarmIn: " + camstate
state.AlarmIn = camstate
if (camstate == "active") {sendEvent(name:"switch",value:"on")} //** v110
else {sendEvent(name:"switch",value:"off")} //** v110
camstate = xml.IOPortStatus[1].ioState.text()
log.info "AlarmOut: " + camstate
state.AlarmOut = camstate
} else {
camstate = xml.enabled.text()
if (camstate == "true") {camstate = "enabled"} else {camstate = "disabled"}
state."${Feature}" = camstate //** v200
log.info Feature + ": " + camstate
}
return("OK")
} else {
errcd = LogGETError()
if (errcd == "NA") {
log.info Feature + " is not available"
state."${Feature}" = "NA" //** v200
}
return(errcd)
}
}
//******************************************************************************
// ALARM ON - ALARM ON - ALARM ON - ALARM ON - ALARM ON - ALARM ON - ALARM ON
//******************************************************************************
void on() {
if (devUse == "Alarm Server") {return} //** v200
if (!Ok2Run("AlarmON")) {return}
String cname = devName.toUpperCase() //** v200
log.warn "TRIGGER ALARM on " + cname
if (device.currentValue("AlarmOut") == "NA") {
log.warn "Alarm Out Feature is excluded or not available"
return}
SwitchAlarm("active")
}
//******************************************************************************
// ALARM OFF - ALARM OFF - ALARM OFF - ALARM OFF - ALARM OFF - ALARM OFF
//******************************************************************************
void off() {
if (devUse == "Alarm Server") {return} //** v200
if (!Ok2Run("AlarmOFF")) {return}
String cname = devName.toUpperCase() //** v200
log.warn "CLEAR ALARM on " + cname
if (device.currentValue("AlarmOut") == "NA") {
log.warn "Alarm Out Feature is excluded or not available"
return}
SwitchAlarm("inactive")
}
//******************************************************************************
// ENABLE - ENABLE - ENABLE - ENABLE - ENABLE - ENABLE - ENABLE - ENABLE - ENABLE
//******************************************************************************
void Enable(String filter) {
if (devUse == "Alarm Server") {return} //** v200
if (!Ok2Run("Enable")) {return}
String cname = devName.toUpperCase() //** v200
log.info "ENABLE $cname with filter: " + filter
SwitchAll(filter, "true")
return
}
//******************************************************************************
// DISABLE - DISABLE - DISABLE - DISABLE - DISABLE - DISABLE - DISABLE - DISABLE
//******************************************************************************
void Disable(String filter) {
if (devUse == "Alarm Server") {return} //** v200
if (!Ok2Run("Disable")) {return}
String cname = devName.toUpperCase() //** v200
log.info "DISABLE $cname with filter: " + filter
SwitchAll(filter, "false")
return
}
//****************************************************************************** v110 START
// PUSH - PUSH - PUSH - PUSH - PUSH - PUSH - PUSH - PUSH - PUSH - PUSH - PUSH
//******************************************************************************
void push (buttonNumber) {
if (devUse == "Controller") {return}
if (!Ok2Run("Push")) {return}
if (buttonNumber == null) {return}
if (buttonNumber < 1 || buttonNumber > 9) {return} //** v211
String cname = devName.toUpperCase()
sendEvent(name: "pushed", value: buttonNumber, isStateChange: true)
state.LastButtonPush = buttonNumber.toString()
log.info "BUTTON PUSHED on " + cname + ": " + buttonNumber
}
//****************************************************************************** V1.1.0 END
// OK2RUN - OK2RUN - OK2RUN - OK2RUN - OK2RUN - OK2RUN - OK2RUN - OK2RUN - OK2RUN
//******************************************************************************
def Ok2Run(String cmd) {
String devstatus = device.currentValue("zDriver")
if (devstatus == "FAILED") {
log.warn "Not allowed to run: " + cmd
return(false)}
if (devstatus == "ERR") {
log.warn "Not allowed to run: " + cmd + ". Fix problem and Save Preferences to reset."
return(false)}
if (devstatus == "CRED") {
log.warn "Not allowed to run: " + cmd + ". Fix creds or config and Save Preferences to reset."
return(false)}
return(true)
}
//******************************************************************************
// SWITCH ALARM - SWITCH ALARM -SWITCH ALARM -SWITCH ALARM -SWITCH ALARM
//******************************************************************************
private SwitchAlarm(String newstate) {
String errcd = ""
String devstate = state.AlarmOut //** v200
String devaistate = state.AlarmIn //** v200
String camstate = ""
String camaistate = ""
String path = FeaturePaths.AlarmIO
def xml = SendGetRequest(path, "GPATH")
if (strMsg != "OK") {
errcd = LogGETError()
sendEvent(name:"zDriver",value:errcd)
return(errcd)
}
camstate = xml.IOPortStatus[1].ioState.text()
camaistate = xml.IOPortStatus[0].ioState.text()
if (camstate == newstate) {
log.info "OK, already " + newstate
state.AlarmOut = newstate //** v200
state.AlarmIn = camaistate //** v200
if (camaistate == "active") {sendEvent(name:"switch",value:"on")} //** v110
else {sendEvent(name:"switch",value:"off")} //** v110
sendEvent(name:"zDriver",value:"OK")
return("OK")
}
// Current state is reported as active/inactive
// To send the trigger, we set the outputState to high/low
if (newstate == "active") {newstate = "high"}
if (newstate == "inactive") {newstate = "low"}
// Can't get any easier than this...
strXML = "" +\
"" + newstate + ""
path = FeaturePaths.AlarmOut
xml = SendPutRequest(path, strXML)
if (strMsg == "OK") {
if (newstate == "high") {newstate = "active"} else {newstate = "inactive"}
log.info "OK, Alarm Out is now " + newstate
state.AlarmOut = newstate //** v200
sendEvent(name:"zDriver", value:"OK")
// Need to wait for the straggler Alarm alerts to be processed
// before reseting AlarmIn on the device, otherwise it just gets triggered again
// by the Alarm Server, leaving an out of sync condition. Play it safe, wait 60.
if (newstate == "inactive" && state.AlarmSvr == "OK") {
log.info "Waiting 1 minute for Alarm Event messages from camera to stop"
runIn(60, RefreshAlarmStates, overwrite)
} else {
runIn(5, RefreshAlarmStates, overwrite)
}
return("OK")
}
log.error "PUT Error: " + strMsg
if (strMsg.contains("code: 403") && strMsg.contains("Forbidden")) {
log.error "Operator does not have Remote Parameters or Remote Notify options selected"
sendEvent(name:"zDriver",value:"CRED")
return("CRED")
}
sendEvent(name:"zDriver",value:"ERR")
return("ERR")
}
//******************************************************************************
// REFRESH ALARM STATES - REFRESH ALARM STATES - REFRESH ALARM STATES
//******************************************************************************
void RefreshAlarmStates() {
log.info "Refreshing Alarm I/O states"
String errcd = GetSetFeatureState("AlarmIO")
if (errcd != "OK") {
log.error strMsg
sendEvent(name:"zDriver",value:errcd)
}
}
//******************************************************************************
// SWITCH ALL - SWITCH ALL - SWITCH ALL - SWITCH ALL - SWITCH ALL - SWITCH ALL
//******************************************************************************
void SwitchAll(String filter, String newstate) {
String errcd = " "
String Path = ""
if (filter == null) {filter = ""}
// AlarmInH will only be switched if specified in the filter, by design
if (filter != "" && filter.contains("ai")) {
if (device.currentValue("AlarmInH") != "NA" || state.AlarmInH != "NA") { //** v200
errcd = SetFeatureState("AlarmInH",newstate)
if (errcd != "OK") {return}
}
} else {
if (filter.contains("ai")) {log.info "Requested feature *ai* is NA"}
}
errcd = "OK"
for (feature in MotionFeatures) {
if (filter == "" || filter.contains("$feature.key")) {
if (state."$feature.value" != "NA") { //** v200
if (state."$feature.value" == null) {SavingPreferences = true} //** v200
errcd = SetFeatureState("$feature.value",newstate)
SavingPreferences = false //** v200
if (errcd == "NA") { //** v200
state."$feature.value" = "NA" //** v200
errcd = "OK" //** v200
} //** v200
if (errcd != "OK") {break}
} else {
if (filter.contains("$feature.key")) {log.info "Requested feature *$feature.key* is NA"}
}
}
}
sendEvent(name:"zDriver",value:errcd)
return
}
//******************************************************************************
// SET FEATURE STATE - SET FEATURE STATE - SET FEATURE STATE - SET FEATURE STATE
//******************************************************************************
private SetFeatureState(String Feature, String newstate) {
String errcd = ""
String devstate = state."$Feature" //** v200
String Path = FeaturePaths."$Feature"
def xml = SendGetRequest(Path, "XML")
if (strMsg != "OK") {
errcd = LogGETError()
sendEvent(name:"zDriver", value:errcd)
return(errcd)
}
// Find first occurence, Line Cross and Intrusion have sub-features
// that also include the enabled element.
def i = xml.indexOf("")
// this should never happen, cya
if (i == -1) {
strMsg = "Unexpected XML structure, element not found"
log.error strMsg
sendEvent(name:"zDriver", value:"ERR")
return("ERR")
}
// Extract current state from xml, first 4
String camstate = xml.substring(i+9,i+13)
if (camstate == "fals") {camstate = "false"}
// ditto, cma
if (camstate != "true" && camstate != "false") {
log.error "inSetFeatureState: XML element is not true/false"
log.error "inSetFeatureState: Extracted =" + camstate
sendEvent(name:"zDriver", value:"ERR")
return("ERR")
}
if (camstate == newstate) {
if (newstate == "true") {newstate = "enabled"} else {newstate = "disabled"}
log.info "OK, " + Feature + " is already " + newstate
state."$Feature" = newstate //** v200
sendEvent(name:"zDriver",value:"OK")
return("OK")
}
if (newstate == "true") {
xml = xml.replaceFirst("false<", "true<")
} else {
xml = xml.replaceFirst("true<", "false<")
}
if (debug) {log.info "PUT " + Path + "/enabled=" + newstate}
xml = SendPutRequest(Path, xml)
if (strMsg == "OK") {
if (newstate == "true") {newstate = "enabled"} else {newstate = "disabled"}
log.info "OK, " + Feature + " is now " + newstate
// sendEvent(name:"$Feature",value:newstate) //** v200
state."$Feature" = newstate //** v200
sendEvent(name:"zDriver",value:"OK")
return("OK")
}
log.error "PUT Error: " + strMsg
if (strMsg.contains("code: 403")) {
log.error "Operator does not have Remote Parameters or Remote Notify options selected"
sendEvent(name:"zDriver",value:"CRED")
return("CRED")
}
sendEvent(name:"zDriver",value:"ERR")
return("ERR")
}
//******************************************************************************
// SEND GET REQUEST - SEND GET REQUEST - SEND GET REQUEST - SEND GET REQUEST
//******************************************************************************
// Return XML OR GPATH with strMsg=OK, or strMsg=Error message
private SendGetRequest(String path, String rtype) {
String credentials = device.getDataValue("CamID")
def headers = [:]
def parms = [:]
def xml = ""
headers.put("HOST", devIP + ":" + devPort)
headers.put("Authorization", "Basic " + credentials)
parms.put("uri", "http://" + devIP + ":" + devPort + path)
parms.put("headers", headers)
parms.put("requestContentType", "application/xml")
if (rtype == "XML") {parms.put ("textParser", true)}
if (debug) {log.debug "GET ${path}"}
try {httpGet(parms)
{ response ->
if (debug) {
log.debug "GET response.status: " + response.getStatus()
log.debug "GET response.contentType: " + response.getContentType()
}
if (response.status == 200) {
strMsg = "OK"
if (rtype == "GPATH") {
xml = response.data
if (debug) {xml.'**'.each { node ->
log.debug "GPATH: " + node.name() + ": " + node.text()}}
} else {
xml = response.data.text
if (debug) {log.debug groovy.xml.XmlUtil.escapeXml(xml)}
}
} else {
strMsg = response.getStatus()
}
}}
catch (Exception e) {
strMsg = e.message
}
return(xml)
}
//******************************************************************************
// LOG GET ERROR - LOG GET ERROR - LOG GET ERROR - LOG GET ERROR
//******************************************************************************
private LogGETError() {
//************************************************************* v200 START
String cname = devName.toUpperCase()
String errcd = "ERR"
if (strMsg.contains("code: 403")) {
if (!SavingPreferences) {
log.error "GET Error: " + strMsg
log.warn "Resource not available or is restricted at a higher level"
}
return("NA")
}
log.error "GET Error: " + strMsg
if (strMsg.contains("No route to host") || strMsg.contains ("connect timed out")) {
log.warn "$cname is OFFLINE"
return("OFF")
}
//************************************************************* v200 END
if (strMsg.contains("code: 401")) {
log.warn "4) Operator Account requires Remote Parameters/Settings and Remote Notify options selected"
log.warn "3) Credentials do not match or have been changed on the camera since last Save Preferences"
log.warn "2) Network > Advanced > Integration Protocol > CGI Enabled w/Authentication=Digest/Basic"
log.warn "1) System > Security > Web Authentication=Digest/Basic"
log.warn "Authentication Failed, check the following:"
errcd = "CRED"
}
if (strMsg.contains("code: 404")) {
log.warn "Check Network > Advanced > Integration Protocol > CGI Enabled w/Authentication=Digest/Basic"
log.warn "Hikvision-CGI is NOT ENABLED or IP is not a Hikvision camera"
}
return(errcd)
}
//******************************************************************************
// SEND PUT REQUEST - SEND PUT REQUEST - SEND PUT REQUEST - SEND PUT REQUEST
//******************************************************************************
// Return strMsg=OK or strMsg=Error message
private SendPutRequest(String strPath, String strXML) {
def xml = ""
def credentials = device.getDataValue("CamID")
def headers = [:]
def parms = [:]
headers.put("HOST", devIP + ":" + devPort)
headers.put("Authorization", "Basic " + credentials)
headers.put("Content-Type", "application/xml")
parms.put("uri", "http://" + devIP + ":" + devPort + strPath)
parms.put("headers", headers)
parms.put("body", strXML)
parms.put("requestContentType", "application/xml")
try {httpPut(parms) { response ->
if (debug) {
log.debug "PUT response.status: " + response.getStatus()
log.debug "PUT response.contentType: " + response.getContentType()
}
if (response.status == 200) {
xml = response.data
strMsg = "OK"}
else {
strMsg = response.getStatus()
}
}}
catch (Exception e) {
strMsg = e.message
}
}
//******************************************************************************
// PARSE - PARSE - PARSE - PARSE - PARSE - PARSE - PARSE - PARSE - PARSE - PARSE
//******************************************************************************
void parse(String description) {
String etype = "" // eventType
String estate = "" // eventState
String evnum = "" // event button number //** v110
String lastpush = "" // LastButtonsPushed
String ecode = "" // event type feature code //** v200
Boolean ok = false // Only Supported Motion Events trigger motion on this device
Boolean ns = false // Not Supported and Unknown Events are logged and ignored
String logtag = "" // Logging tag for new Motion/Duration Event - v106
String cname = devName.toUpperCase() //** v200
if (device.currentValue("zDriver") == "OFF") {
log.warn "$cname is BACK ONLINE"
sendEvent(name:"zDriver",value:"OK")
}
if (device.currentValue("numberOfButtons") == 8) {sendEvent(name:"numberOfButtons",value:9)} //** v210
// Initialize on first event message
if (state.AlarmSvr == null || state.AlarmSvr == "NA") {
log.warn "Alarm Server NOW IN USE on " + cname
log.info "Initializing State variables"
state.AlarmSvr = "OK"
state.LastAlarm = "na"
state.AlarmCount = 0
state.LastMotionEvent = "na"
state.LastMotionTime = "na"
state.LastButtonPush = "na" //** v110
state.MotionEventCount = 0
state.OtherEventState = "inactive"
state.LastOtherEvent = "na"
state.LastOtherTime = "na"
state.OtherEventCount = 0
state.EventMsgCount = 0
state.ExcludedEvents = 0 //** v200
if (devResetCounters == "Daily") {schedule('0 0 1 * * ?',ResetCounters)}
if (devResetCounters == "Weekly") {schedule('0 0 1 ? * 1',ResetCounters)}
if (devResetCounters == "Monthly") {schedule('0 0 1 1 * ?',ResetCounters)}
}
def rawmsg = parseLanMessage(description)
def hdrs = rawmsg.headers // its a map
def msg = "" // tbd
if (debuga) {log.warn "EVENT MESSAGE RECEIVED"} //** v106
if (debuga) {hdrs.each {log.debug it}}
// This is the key to knowing what you have to work with
if (hdrs["Content-Type"] == "application/xml; charset=\"UTF-8\"" || hdrs["Content-Type"] == "application/xml") {
msg = new XmlSlurper().parseText(new String(rawmsg.body))
if (debuga) {log.debug "MSG:" + groovy.xml.XmlUtil.escapeXml(rawmsg.body)} //** v106
estate = msg.eventState.text()
etype = msg.eventType.text()
if (debuga) {log.debug "msg.eventType.text: " + etype}
if (debuga) {log.debug "msg.eventState.text: " + estate}
// ******************************************************* v106 START
if (etype == "duration") {
logtag = "-Duration"
etype = msg.DurationList.Duration.relationEvent.text()
if (debuga) {log.debug "msg.DurationList.Duration.relationEvent.text: " + etype}
}
if (eetype == "") {eetype = "Unknown"}
// ******************************************************** v106 END
etype = ">" + etype + "<"
for (event in SupportedEvents) {
if (etype == event) {
ok = true
break}
}
if (!ok) {
for (event in UnsupportedEvents) {
if (etype == event) {
ns = true
break}
}
}
} else {
msg = rawmsg.body.toString()
if (debuga) {log.debug "MSG:" + msg}
for (event in SupportedEvents) {
if (msg.contains("$event")) {
etype = event
ok = true
break}
}
if (!ok) {
for (event in UnsupportedEvents) {
if (msg.contains("$event")) {
etype = event
ns = true
break}
}
}
}
if (!ok && !ns) {
ns = true
etype = "Unknown"
} else {
etype = etype.substring(1,etype.length()-1)
}
// Translate to user/driver friendly names
etype = TranslateEvents."$etype"
state.EventMsgCount = state.EventMsgCount + 1
// For Unsuported Events, log the first occurence and ignore the rest
if (ns) {
if (state.OtherEventState == "inactive" || etype != state.LastOtherEvent) {
log.warn "OTHER EVENT1 on " + cname + ": " + etype + logtag //** v106
state.OtherEventState = "active"
state.LastOtherEvent = etype
state.LastOtherTime = new Date().format ("EEE MMM d HH:mm:ss")
state.OtherEventCount = state.OtherEventCount + 1
// 1.0.5 and 1.0.6 Updates - Log Unknown Events
if (etype == "Unknown") {
if (!debuga) {
if (hdrs["Content-Type"] == "application/xml; charset=\"UTF-8\"" || hdrs["Content-Type"] == "application/xml") {
log.warn "EVENT MESSAGE:" + groovy.xml.XmlUtil.escapeXml(rawmsg.body)
} else {
log.warn "EVENT MESSAGE:" + rawmsg.body
}
}
log.warn "Please report this event to trsystems.help@gmail.com"
}
}
// Give whatever this a minute to run its course
// Most of these are one-time(?) but a few are ongoing like motion
// And if thats the case, this will continue to get pushed out
// Cant even test some of these unsupported events
// And, all cameras behave differntly in terms of how many msgs they send
// and the interval between while an event is in progress
// One of my cameras sent 5 messages at 1 per second for each failed login
runIn(60, ResetUsupEvent, overwrite)
return
}
if (etype == "AlarmIn") {
// First time in?
if (state.AlarmIn == null || state.AlarmIn == "inactive") { //** v200
log.warn "ALARM INPUT on " + cname
state.AlarmIn = "active" //** v200
sendEvent(name:"switch",value:"on") //** v110
state.AlarmCount = state.AlarmCount + 1
state.LastAlarm = new Date().format ("EEE MMM d HH:mm:ss")
return
}
// Ignore the rest until it gets turned off
// Reset is handled by the off command/SwitchAlarm method
return
}
// ************************************************************ v210 START
if (etype == "VideoTamper") {
// First time in?
// if (state.OtherEventState == "inactive" || etype != state.LastOtherEvent) { //** v211
if (state.TamperState == null || state.TamperState == "inactive") {
state.TamperState = "active"
state.LastOtherEvent = etype
state.LastOtherTime = new Date().format ("EEE MMM d HH:mm:ss")
state.OtherEventCount = state.OtherEventCount + 1
// If motion is active, add this button push to the list
evnum = EventButtonNumbers."$etype"
if (device.currentValue("motion") == "active") {
lastpush = state.LastButtonPush
if (lastpush == null) {lastpush=""}
if (!lastpush.contains("$evnum")) {
lastpush = lastpush + "," + evnum
state.LastButtonPush = "$lastpush"
// sendEvent(name:"pushed",value:evnum) //** v211
}
} else {
state.LastButtonPush = evnum
// sendEvent(name:"pushed",value:evnum,isStateChange:true) //** v211
}
sendEvent(name:"pushed",value:evnum,isStateChange:true) //** v211
log.warn "VIDEO TAMPER on " + cname + "-Push: " + evnum
}
runIn(60, ResetTamperEvent, overwrite) //** v211
return
}
// ************************************************************ v210 END
// Supported Motion Event
// ************************************************************ v200 START
ecode = FeatureNamesToCode."$etype"
if (devExcludeA == null) {devExcludeA = ""}
if (devExcludeA.contains("$ecode")) {
if (state.LastExcluded == null || state.LastExcluded != etype) {
state.ExcludedEvents = state.ExcludedEvents + 1
state.LastExcluded = etype
log.warn "EXCLUDED MOTION EVENT on " + cname + ": " + etype
}
return
}
// ************************************************************ v200 END
if (device.currentValue("motion") == "inactive") {
sendEvent(name:"motion",value:"active")
state.LastMotionEvent = etype
state.LastMotionTime = new Date().format ("EEE MMM d HH:mm:ss")
state.MotionEventCount = state.MotionEventCount + 1
// ******************************************************** v110 START
evnum = EventButtonNumbers."$etype"
state.LastButtonPush = evnum
sendEvent(name:"pushed",value:evnum,isStateChange:true) //** v200
logtag = logtag + "-Push: " + evnum
// ******************************************************** v110 END
log.warn "MOTION EVENT1 on " + cname + ": " + etype + logtag //** v106
} else {
// We may have more than one going on
if (etype != state.LastMotionEvent) {
state.LastMotionEvent = etype
state.LastMotionTime = new Date().format ("EEE MMM d HH:mm:ss")
// ******************************************************** v110 START
evnum = EventButtonNumbers."$etype"
lastpush = state.LastButtonPush
if (lastpush == null) {lastpush=""}
if (!lastpush.contains("$evnum")) {
lastpush = lastpush + "," + evnum
state.LastButtonPush = "$lastpush"
sendEvent(name:"pushed",value: evnum) //** v200
logtag = logtag + "-Push: " + evnum
}
// ******************************************************** v110 END
log.info "MOTION EVENT+ on " + cname + ": " + etype + logtag // v.1.0.6
}
}
// Wait minutes, not seconds for all motion events to clear
// Avoid unnecessary motion state changes in HE
// Typical PIR sensors will wait 4 minutes before signaling clear
runIn (settings.devMotionReset.toInteger() * 60, ResetMotion, overwrite)
}
void ResetMotion() {
String cname = devName.toUpperCase() //** v200
sendEvent(name:"motion",value:"inactive")
log.info "MOTION CLEARED on " + cname
}
void ResetUsupEvent() {
String cname = devName.toUpperCase() //** v200
state.OtherEventState = "inactive"
log.info "OTHER EVENT CLEARED on " + cname
}
// ******************************************************** v211 START
void ResetTamperEvent() {
String cname = devName.toUpperCase()
state.TamperState = "inactive"
log.info "VIDEO TAMPER CLEARED on " + cname
}
// ******************************************************** v211 END
void ResetCounters() {
log.warn "Resetting Alarm Server Counters"
state.AlarmCount = 0
state.MotionEventCount = 0
state.OtherEventCount = 0
state.EventMsgCount = 0
state.ExcludedEvents = 0
}
//*****************************************************************
// Static Constants
//*****************************************************************
@Field static List SupportedEvents = [
">IO<",
">VMD<",
">PIR<",
">linedetection<",
">fielddetection<",
">regionEntrance<",
">regionExiting<",
">attendedBaggage<",
">unattendedBaggage<",
">shelteralarm<" //** v210
]
@Field static List UnsupportedEvents = [
">facedetection<",
">faceSnap<",
">loitering<",
">scenechangedetection<",
">storageDetection<",
">diskerror<",
">diskfull<",
">illAccess<",
">illaccess<", //** v210 other versions are lower case
">ipconflict<"
]
@Field static Map TranslateEvents = [
IO:"AlarmIn",
VMD:"Motion",
PIR:"PIR",
linedetection:"LineCross",
fielddetection:"Intrusion",
attendedBaggage:"ObjectR",
regionEntrance:"RgnEnter",
regionExiting:"RgnExit",
unattendedBaggage:"UBaggage",
shelteralarm:"VideoTamper", //** v210
// unsupported
facedetection:"FaceD",
faceSnap:"FaceSnap",
loitering:"Loitering",
scenechangedetection:"SceneChg",
storageDetection:"Storage",
diskerror:"DiskError",
diskfull:"DiskFull",
illAccess:"LoginFailed",
illaccess:"LoginFailed", //** v210
ipconflict:"ipConflict",
Unknown:"Unknown"
]
@Field static List FeatureCodes = ["ai","ao","in","lc","m","p","or","re","rx","ub","vt"] //** v210
@Field static Map FeatureCodesToName = [
ai:"AlarmInH",
ao:"AlarmIO",
in:"Intrusion",
lc:"LineCross",
m:"MotionD",
p:"PIRSensor",
or:"ObjectR",
re:"RgnEnter",
rx:"RgnExit",
ub:"UBaggage",
vt:"VideoTamper" //** v210
]
@Field static Map MotionFeatures = [
in:"Intrusion",
lc:"LineCross",
m:"MotionD",
p:"PIRSensor",
or:"ObjectR",
re:"RgnEnter",
rx:"RgnExit",
ub:"UBaggage",
vt:"VideoTamper" //** v210
]
@Field static Map FeatureNamesToCode = [
Intrusion:"in",
LineCross:"lc",
MotionD:"m",
PIRSensor:"p",
ObjectR:"or",
RgnEnter:"re",
RgnExit:"rx",
UBaggage:"ub"
]
// ****************************************** v110 START
@Field static Map EventButtonNumbers = [
Intrusion:"1",
LineCross:"2",
Motion:"3", //** v200 bug fix
PIR:"4", //** v200 bug fix
ObjectR:"5",
RgnEnter:"6",
RgnExit:"7",
UBaggage:"8",
VideoTamper:"9" //** v210
]
// ****************************************** v110 END
// Includes paths to system info and features not implemented
@Field static Map FeaturePaths = [
SysInfo:"/ISAPI/System/deviceInfo",
Network:"/ISAPI/System/Network/Interfaces/1",
CamUsers:"/ISAPI/Security/users",
UserPerm:"/ISAPI/Security/UserPermission/",
AlarmSvr:"/ISAPI/Event/notification/httpHosts",
AlarmInH:"/ISAPI/System/IO/inputs/1",
AlarmIO:"/ISAPI/System/IO/status",
AlarmOut:"/ISAPI/System/IO/outputs/1/trigger",
Intrusion:"/ISAPI/Smart/FieldDetection/1",
LineCross:"/ISAPI/Smart/LineDetection/1",
MotionD:"/ISAPI/System/Video/inputs/channels/1/motionDetection",
ObjectR:"/ISAPI/Smart/attendedBaggage/1",
PIRSensor:"/ISAPI/WLAlarm/PIR",
RgnEnter:"/ISAPI/Smart/regionEntrance/1",
RgnExit:"/ISAPI/Smart/regionExiting/1",
UBaggage:"/ISAPI/Smart/unattendedBaggage/1",
VideoTamper:"/ISAPI/System/Video/inputs/channels/1/tamperDetection", //** V210
Face:"/ISAPI/Smart/FaceDetect/1",
Loitering:"/ISAPI/Smart/loitering/1",
PeopleD:"/ISAPI/Smart/peopleDetection",
SceneChg:"/ISAPI/Smart/SceneChangeDetection/1"
]