The only other website I usually try is the link below.
https://www.ispyconnect.com/man.aspx?n=Tenvis
Can you get the JPG/snapshot page to work from your computer? If you cant get that working then dont move into the Smartthings side. I find a search of modelname+snapshot works well in Google to get hints, and then aided by removing any authentication settings on the camera while testing.
C
Yes.
The URL isā¦
http://IPADDRESS/tmpfs/auto.jpg
ā¦and the jpg comes up just fine when I go to that URL in a browser.
Hi all.
Iāve read through the entire thread and understand that ST keep removing undocumented access and changing API calls. The work, and value, of the community is hugely apparent in this and why ST havenāt implemented this functionality natively is a surprise.
What I havenāt been able to really work out is - Is this currently working or not?
I manage to get to a point where my logs show:
d22db97a-ed33-486f-9f9d-10380dde758a 9:48:57 PM: debug 486b581197b145c4a08ac15b2a50e310
d22db97a-ed33-486f-9f9d-10380dde758a 9:48:57 PM: debug Parsing 'index:19, mac:000DC5D951AF, ip:C0A8010F, port:1F4F, requestId:777e61da-f8e3-4b85-b0c6-637cda64e501, tempImageKey:b0c0ce16-0ce2-4451-bb9d-a6b64e398bba'
which would imply that the issue is with storing to S3. Iāve tried an array of cameras so I donāt think it is image size (which varies from camera to camera about 28k-43k)
Is there further debugging or testing I can do to prove itās the storage element?
thanks
Any chance youād be willing to combine this one, with the newer one that can stream? I have the newer one working great, but Iād like to be able to take a single frame-grab when something happensā¦ Either from the RTSP or from a JPG URL in addition to the stream URL.
Thoughts?
Feel free to fork it. I have ended my support for these DTHs so if anyone wants to pick up and fork the github and combine the two, feel free.
Once AGAIN Patrick RULES ! I just updated to the new code and it works again.
Hey SMARTTHINGS - WAKE UP and stop hosing your devleopers. WIthout them, you have no product.
Hi there,
I tried this thing, on a small website, that has nothing except deliver that .jpg file, and the thing is I donāt see anything on the network flowing from the hub to the HTTP website. is there anything I need to activate, to have the hub support this hubaction thing and finally send those request.
and by the way I agree with all of you, the documentation is just crappy. and provides absolutely no clear guidance for a beginner.
This is meant for lan only. Use httpget for internet requests.
Thanks Patrick,
Iām actually trying to get it work on LAN
since nothing was working, I decided to give it a try from Internet with HTTPGet, which worked
but for now I need this crappy thing to work in LAN. with this really not explicit hubaction, this seems to me almost impossible.
Iām sure as an absolute novice, I might be missing something really obviousā¦ but I canāt seem to find what
Anyone know why iām getting this error?
error Error creating device: physicalgraph.app.exception.UnknownDeviceTypeException: Device type āGeneric Video Cameraā in namespace āpstuartā not found.
Any ability (or plans) for motion sensing triggers?
Iām hoping @pstuart, @RBoy, and/or anyone else can come up with a DH for Sercomm 8221. Hereās some API information I found already but donāt know how to integrate it into ST:
Custom vendor reset and API access:
API documentation details:
All models video URLs:
https://www.ispyconnect.com/man.aspx?n=Sercomm
@pstuart Is there a reason I couldnt merge this with the video device handler? That way I can see the video feed while on my local connection, and then when I am away from home I can still press āTakeā in order to get a snapshot of what is going on.
I love the fact that I can automate the āTakeā function through CoRE, so I dont want to lose that, but presumably it could sit there as a secondary function to the DH without any issues? I just thought I would quickly ask the question before making a start, in case there were any words of wisdom that I should take note of!
Thanks in advance
Anything is possible, feel free to fork or submit a pull request against it the repo. I have discontinued development of smartthings related code for the time being.
Yes I saw that happening with all the changes they introduced! Even now it is incredibly frustrating getting code to work with nigh on zero documentation about large swathes of the features!
I will give it a go and put in the request based on how successfully it works!
I was playing around with pstuartās generic camera handlers and managed to combine them. If anyone is interested, itās pasted below. I literally copied the video handler code into the photo handler and tweaked it a bit. I take no credit, except for my excellent copy and pasting.
/**
*
* Generic IP Camera
*
* Based on pstuart's Generic Video Camera and Generic Camera Device
*
* 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.
*
*/
metadata {
definition (name: "Generic IP Camera", namespace: "tinlau", author: "tinlau") {
capability "Image Capture"
capability "Sensor"
capability "Actuator"
capability "Configuration"
capability "Video Camera"
capability "Video Capture"
capability "Refresh"
capability "Switch"
command "start"
attribute "hubactionMode", "string"
}
preferences {
input("CameraIP", "string", title:"Camera IP Address", description: "Please enter your camera's IP Address", required: false, displayDuringSetup: true)
input("CameraPort", "string", title:"Camera Port", description: "Please enter your camera's Port", defaultValue: 80 , required: false, displayDuringSetup: true)
input("CameraPath", "string", title:"Camera Path to Image", description: "Please enter the path to the image", defaultValue: "/snapshot.jpg", required: false, displayDuringSetup: true)
input("CameraPathVideo", "string", title:"Camera Path to Video", description: "Please enter the path to the video", defaultValue: "/stream/getvideo", required: false, displayDuringSetup: true)
input("CameraAuth", "bool", title:"Does Camera require User Auth?", description: "Please choose if the camera requires authentication (only basic is supported)", defaultValue: true, required: false, displayDuringSetup: true)
input("CameraPostGet", "string", title:"Does Camera use a Post or Get, normally Get?", description: "Please choose if the camera uses a POST or a GET command to retreive the image", defaultValue: "GET", required: false, displayDuringSetup: true)
input("CameraUser", "string", title:"Camera User", description: "Please enter your camera's username", required: false, displayDuringSetup: true)
input("CameraPassword", "string", title:"Camera Password", description: "Please enter your camera's password", required: false, displayDuringSetup: true)
}
simulator {
}
tiles {
multiAttributeTile(name: "videoPlayer", type: "videoPlayer", width: 6, height: 4) {
tileAttribute("device.switch", key: "CAMERA_STATUS") {
attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", action: "switch.off", backgroundColor: "#79b821", defaultState: true)
attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", action: "switch.on", backgroundColor: "#ffffff")
attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0")
attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", action: "refresh.refresh", backgroundColor: "#F22000")
}
tileAttribute("device.errorMessage", key: "CAMERA_ERROR_MESSAGE") {
attributeState("errorMessage", label: "", value: "", defaultState: true)
}
tileAttribute("device.camera", key: "PRIMARY_CONTROL") {
attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", backgroundColor: "#79b821", defaultState: true)
attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", backgroundColor: "#ffffff")
attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0")
attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", backgroundColor: "#F22000")
}
tileAttribute("device.startLive", key: "START_LIVE") {
attributeState("live", action: "start", defaultState: true)
}
tileAttribute("device.stream", key: "STREAM_URL") {
attributeState("activeURL", defaultState: true)
}
}
standardTile("take", "device.image", width: 2, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking"
state "taking", label:'Taking', action: "", icon: "st.camera.take-photo", backgroundColor: "#53a7c0"
state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking"
}
standardTile("refresh", "device.alarmStatus", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
state "refresh", action:"polling.poll", icon:"st.secondary.refresh"
}
standardTile("blank", "device.image", width: 2, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") {
state "blank", label: "", action: "", icon: "", backgroundColor: "#FFFFFF"
}
carouselTile("cameraDetails", "device.image", width: 6, height: 4) { }
main "take"
details(["videoPlayer", "take", "blank", "refresh", "cameraDetails"])
}
}
//for photo
def parse(String description) {
log.debug "Parsing '${description}'"
def map = [:]
def retResult = []
def descMap = parseDescriptionAsMap(description)
//Image
def imageKey = descMap["tempImageKey"] ? descMap["tempImageKey"] : descMap["key"]
if (imageKey) {
storeTemporaryImage(imageKey, getPictureName())
}
}
// handle commands
def take() {
def userpassascii = "${CameraUser}:${CameraPassword}"
def userpass = "Basic " + userpassascii.encodeAsBase64().toString()
def host = CameraIP
def hosthex = convertIPtoHex(host).toUpperCase() //thanks to @foxxyben for catching this
def porthex = convertPortToHex(CameraPort).toUpperCase()
device.deviceNetworkId = "$hosthex:$porthex"
log.debug "The device id configured is: $device.deviceNetworkId"
def path = CameraPath
log.debug "path is: $path"
log.debug "Requires Auth: $CameraAuth"
log.debug "Uses which method: $CameraPostGet"
def headers = [:]
headers.put("HOST", "$host:$CameraPort")
if (CameraAuth) {
headers.put("Authorization", userpass)
}
log.debug "The Header is $headers"
def method = "GET"
try {
if (CameraPostGet.toUpperCase() == "POST") {
method = "POST"
}
}
catch (Exception e) { // HACK to get around default values not setting in devices
settings.CameraPostGet = "GET"
log.debug e
log.debug "You must not of set the perference for the CameraPOSTGET option"
}
log.debug "The method is $method"
try {
def hubAction = new physicalgraph.device.HubAction(
method: method,
path: path,
headers: headers
)
hubAction.options = [outputMsgToS3:true]
log.debug hubAction
hubAction
}
catch (Exception e) {
log.debug "Hit Exception $e on $hubAction"
}
}
def parseDescriptionAsMap(description) {
description.split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
}
private getPictureName() {
def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '')
log.debug pictureUuid
def picName = device.deviceNetworkId.replaceAll(':', '') + "_$pictureUuid" + ".jpg"
return picName
}
private String convertIPtoHex(ipAddress) {
String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join()
log.debug "IP address entered is $ipAddress and the converted hex code is $hex"
return hex
}
private String convertPortToHex(port) {
String hexport = port.toString().format( '%04x', port.toInteger() )
log.debug hexport
return hexport
}
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
private String convertHexToIP(hex) {
log.debug("Convert hex to ip: $hex")
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
private getHostAddress() {
def parts = device.deviceNetworkId.split(":")
log.debug device.deviceNetworkId
def ip = convertHexToIP(parts[0])
def port = convertHexToInt(parts[1])
return ip + ":" + port
}
//for video
private getVideoURL() {
def videoURL = ""
if (CameraAuth) {
videoURL = "http://${CameraUser}:${CameraPassword}@${CameraIP}:${CameraPort}${CameraPathVideo}"
} else {
videoURL = "http://${CameraIP}:${CameraPort}${CameraPathVideo}"
}
return videoURL
}
mappings {
path("/getInHomeURL") {
action:
[GET: "getInHomeURL"]
}
}
def installed() {
configure()
}
def updated() {
configure()
}
// parse events into attributes
/*def parse(String description) {
log.debug "Parsing '${description}'"
}*/
// handle commands
def configure() {
log.debug "Executing 'configure'"
sendEvent(name:"switch", value: "on")
}
def start() {
log.trace "start()"
def videoURL = getVideoURL()
log.debug videoURL
def dataLiveVideo = [
OutHomeURL : videoURL,
InHomeURL : videoURL,
ThumbnailURL: "http://cdn.device-icons.smartthings.com/camera/dlink-indoor@2x.png",
cookie : [key: "key", value: "value"]
]
def event = [
name : "stream",
value : groovy.json.JsonOutput.toJson(dataLiveVideo).toString(),
data : groovy.json.JsonOutput.toJson(dataLiveVideo),
descriptionText: "Starting the livestream",
eventType : "VIDEO",
displayed : false,
isStateChange : true
]
sendEvent(event)
}
def getInHomeURL() {
[InHomeURL: getVideoURL()]
}
That works a treat for me! You beat me to it! Thanks @tinlau
In case it is useful for anybody reading, I have a Trendnet camera and the URLs are below for picture and video:
/streaming/channels/1/picture
/streaming/channels/1/httpPreview
With @tinlauās version, video only works if you are physically on the same LAN as the camera. This because the live video is actually being rendered by your mobile device, and not by the hub (unlink the still snapshot, which IS taken by the hub over the local LAN). If youāre arenāt on the same LAN, youāll get a ācannot connectā error when you try to run live video.
I fixed this by adding (optional) support for specifying the DNS address and Port ID of the camera (in addition to the local IP address and local port). This way, if you have port forwarding configured for your camera (manually or via UPnP), you can get the live video remotely as well. The implementation will (supposedly) use the local IP if on the same LAN, or use the external DNS address if not.
/**
*
* Generic IP Camera
*
* Based on pstuart's Generic Video Camera and Generic Camera Device
*
* 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.
*
*/
metadata {
definition (name: "Generic IP Camera", namespace: "tinlau", author: "tinlau") {
capability "Image Capture"
capability "Sensor"
capability "Actuator"
capability "Configuration"
capability "Video Camera"
capability "Video Capture"
capability "Refresh"
capability "Switch"
command "start"
attribute "hubactionMode", "string"
}
preferences {
input("CameraIP", "string", title:"Camera internal IP Address", description: "Enter your camera's local IP Address", required: false, displayDuringSetup: true)
input("CameraPort", "string", title:"Camera internal Port", description: "Enter your camera's local Port", defaultValue: 80 , required: false, displayDuringSetup: true)
input("CameraDNS", "string", title:"External DNS Address (optional)", description: "Enter your camera's external DNS Address", defaultValue: '', required: false, displayDuringSetup: true)
input("CameraEPort", "string", title:"External Port (optional)", description: "Enter your camera's external Port", defaultValue: '', required: false, displayDuringSetup: true)
input("CameraPath", "string", title:"Camera Path to Image", description: "Please enter the path to the image", defaultValue: "/snapshot.jpg", required: false, displayDuringSetup: true)
input("CameraPathVideo", "string", title:"Camera Path to Video", description: "Please enter the path to the video", defaultValue: "/stream/getvideo", required: false, displayDuringSetup: true)
input("CameraAuth", "bool", title:"Does Camera require User Auth?", description: "Please choose if the camera requires authentication (only basic is supported)", defaultValue: true, required: false, displayDuringSetup: true)
input("CameraPostGet", "string", title:"Does Camera use a Post or Get, normally Get?", description: "Please choose if the camera uses a POST or a GET command to retreive the image", defaultValue: "GET", required: false, displayDuringSetup: true)
input("CameraUser", "string", title:"Camera User", description: "Please enter your camera's username", required: false, displayDuringSetup: true)
input("CameraPassword", "string", title:"Camera Password", description: "Please enter your camera's password", required: false, displayDuringSetup: true)
}
simulator {
}
tiles {
multiAttributeTile(name: "videoPlayer", type: "videoPlayer", width: 6, height: 4) {
tileAttribute("device.switch", key: "CAMERA_STATUS") {
attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", action: "switch.off", backgroundColor: "#79b821", defaultState: true)
attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", action: "switch.on", backgroundColor: "#ffffff")
attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0")
attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", action: "refresh.refresh", backgroundColor: "#F22000")
}
tileAttribute("device.errorMessage", key: "CAMERA_ERROR_MESSAGE") {
attributeState("errorMessage", label: "", value: "", defaultState: true)
}
tileAttribute("device.camera", key: "PRIMARY_CONTROL") {
attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", backgroundColor: "#79b821", defaultState: true)
attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", backgroundColor: "#ffffff")
attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0")
attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", backgroundColor: "#F22000")
}
tileAttribute("device.startLive", key: "START_LIVE") {
attributeState("live", action: "start", defaultState: true)
}
tileAttribute("device.stream", key: "STREAM_URL") {
attributeState("activeURL", defaultState: true)
}
}
standardTile("take", "device.image", width: 2, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking"
state "taking", label:'Taking', action: "", icon: "st.camera.take-photo", backgroundColor: "#53a7c0"
state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking"
}
standardTile("refresh", "device.alarmStatus", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
state "refresh", action:"polling.poll", icon:"st.secondary.refresh"
}
standardTile("blank", "device.image", width: 2, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") {
state "blank", label: "", action: "", icon: "", backgroundColor: "#FFFFFF"
}
carouselTile("cameraDetails", "device.image", width: 6, height: 4) { }
main "take"
details(["videoPlayer", "take", "blank", "refresh", "cameraDetails"])
}
}
//for photo
def parse(String description) {
log.debug "Parsing '${description}'"
def map = [:]
def retResult = []
def descMap = parseDescriptionAsMap(description)
//Image
def imageKey = descMap["tempImageKey"] ? descMap["tempImageKey"] : descMap["key"]
if (imageKey) {
storeTemporaryImage(imageKey, getPictureName())
}
}
// handle commands
def take() {
def userpassascii = "${CameraUser}:${CameraPassword}"
def userpass = "Basic " + userpassascii.encodeAsBase64().toString()
def host = CameraIP
def hosthex = convertIPtoHex(host).toUpperCase() //thanks to @foxxyben for catching this
def porthex = convertPortToHex(CameraPort).toUpperCase()
device.deviceNetworkId = "$hosthex:$porthex"
log.debug "The device id configured is: $device.deviceNetworkId"
def path = CameraPath
log.debug "path is: $path"
log.debug "Requires Auth: $CameraAuth"
log.debug "Uses which method: $CameraPostGet"
def headers = [:]
headers.put("HOST", "$host:$CameraPort")
if (CameraAuth) {
headers.put("Authorization", userpass)
}
log.debug "The Header is $headers"
def method = "GET"
try {
if (CameraPostGet.toUpperCase() == "POST") {
method = "POST"
}
}
catch (Exception e) { // HACK to get around default values not setting in devices
settings.CameraPostGet = "GET"
log.debug e
log.debug "You must not of set the perference for the CameraPOSTGET option"
}
log.debug "The method is $method"
try {
def hubAction = new physicalgraph.device.HubAction(
method: method,
path: path,
headers: headers
)
hubAction.options = [outputMsgToS3:true]
log.debug hubAction
hubAction
}
catch (Exception e) {
log.debug "Hit Exception $e on $hubAction"
}
}
def parseDescriptionAsMap(description) {
description.split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
}
private getPictureName() {
def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '')
log.debug pictureUuid
def picName = device.deviceNetworkId.replaceAll(':', '') + "_$pictureUuid" + ".jpg"
return picName
}
private String convertIPtoHex(ipAddress) {
String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join()
log.debug "IP address entered is $ipAddress and the converted hex code is $hex"
return hex
}
private String convertPortToHex(port) {
String hexport = port.toString().format( '%04x', port.toInteger() )
log.debug hexport
return hexport
}
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
private String convertHexToIP(hex) {
log.debug("Convert hex to ip: $hex")
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
private getHostAddress() {
def parts = device.deviceNetworkId.split(":")
log.debug device.deviceNetworkId
def ip = convertHexToIP(parts[0])
def port = convertHexToInt(parts[1])
return ip + ":" + port
}
//for video
private getVideoURL() {
if (CameraDNS != '') {
def videoURL = ""
if (CameraAuth) {
videoURL = "http://${CameraUser}:${CameraPassword}@${CameraDNS}:${CameraEPort}${CameraPathVideo}"
} else {
videoURL = "http://${CameraDNS}:${CameraEPort}${CameraPathVideo}"
}
return videoURL
} else {
return getInHomeVideoURL()
}
}
private getInHomeVideoURL() {
def videoURL = ""
if (CameraAuth) {
videoURL = "http://${CameraUser}:${CameraPassword}@${CameraIP}:${CameraPort}${CameraPathVideo}"
} else {
videoURL = "http://${CameraIP}:${CameraPort}${CameraPathVideo}"
}
return videoURL
}
mappings {
path("/getInHomeURL") {
action:
[GET: "getInHomeURL"]
}
}
def installed() {
configure()
}
def updated() {
configure()
}
// parse events into attributes
/*def parse(String description) {
log.debug "Parsing '${description}'"
}*/
// handle commands
def configure() {
log.debug "Executing 'configure'"
sendEvent(name:"switch", value: "on")
}
def start() {
log.trace "start()"
def videoURL = getVideoURL()
def inHomeVideoURL = getInHomeVideoURL()
log.debug "\n${videoURL}\n${inHomeVideoURL}"
def dataLiveVideo = [
OutHomeURL : videoURL,
InHomeURL : inHomeVideoURL,
ThumbnailURL: "http://cdn.device-icons.smartthings.com/camera/dlink-indoor@2x.png",
cookie : [key: "key", value: "value"]
]
def event = [
name : "stream",
value : groovy.json.JsonOutput.toJson(dataLiveVideo).toString(),
data : groovy.json.JsonOutput.toJson(dataLiveVideo),
descriptionText: "Starting the livestream",
eventType : "VIDEO",
displayed : false,
isStateChange : true
]
sendEvent(event)
}
def getInHomeURL() {
[InHomeURL: getInHomeVideoURL()]
}
You should be able to save this on top of @tinlauās version without breaking anything, then just edit the preferences to add the external DNS and Port for the camera (it will ty to use the IP address as before if you donāt edit the preferences).