Generic Camera Device using local connection (new version now available)

After a week of hacking around while waiting for support to respond to my ticket, I finally figured out how to do local http GET and parse the response for cameras.

So I wrote this nice little generic camera device that I have tested with Panasonic Cameras and D-Link cameras and should work for any camera that has a url to retrieve the latest image.

Requirements:
Code here… https://github.com/pstuart/smartthings-ps/blob/master/devicetypes/generic_camera.groovy

Edit preferences after install in devices in IDE or on App on phone:
-IP of the camera, this is your local ip, does NOT need to be publicly accessible
-Port of the camera, typically 80 but could be anything
-Path to image, different for each camera
Panasonic Camera is "/SnapshotJPEG?Resolution=640x480&Quality=Clarity"
D-Link is "/image/jpeg.cgi"
Foscam is “/snapshot.cgi?user=${username}&pwd=${password}”
-Requires Authentication or not (if you are going to embed the username and password in the path, do not set this to true)
-Uses GET or POST (defaults to GET)
-Username for authentication
-Password for authentication

Plug in all these values and press the take button and viola, locally access camera will return an image.

I would love your feedback and testing on other cameras.

How does the physicalgraph.device.HubAction work?

What I finally put together was that the device.deviceNetworkId has to be the hex IP and hex Port for the parse command to work.

So you will see in the code, I take the user input IP and Port and convert those to hex and then over write the device.deviceNetworkId to this and that makes the response from the HubAction work.

Then it just parses the response to the S3 image handler.

Just a word of caution, the S3 image handler posts the image to the cloud and then streams it back into your phone, this means the camera image will work outside of your network, but ultimately is saving an image from your camera up in the amazons3 cloud service.

Hope you enjoy it… I am providing this AS IS, no warranty and no support.

17 Likes

Thanks. So is the cloud like a simple temp directory. Is there a method for retrieving images for permanent storage?

Yeah, you could write anything to send the image anywhere as a part of a HTTP Post if you wanted to, in theory. But the s3 storage looks like it is coreAPI function. I have no idea how long the images will stay up, but there probably is a path to the image sent to the app, if you want to sniff it out, feel free :smile:

I tried once, but guessing it is in the sandbox (I am not rooted - and have no wish to be).

I did really quick… It appears the images are streaming in via this url https://smartthings-smartsense-camera.s3.amazonaws.com/{key_for_image}

I just tried this outside the app, and good news, there seems to be some authentication to the storage, so can’t access the images outside of the app, easily…

can’t get this to work with my Foscam 8910W.

Heres a good link for the foscam URLs: http://www.ispyconnect.com/man.aspx?n=foscam

But i cannot get it to pull anything from the path “/snapshot.cgi?” also tried to make the path this:
"/snapshot.cgi?user=[USERNAME]&pwd=[PASSWORD]&count=0"

Logs show it is is failing at line 183 of the device code for me.

java.lang.StringIndexOutOfBoundsException: String index out of range: 8 @ line 183

Any ideas?

Not having a foscam to test with I bet the device is using a Post instead of Get.

EDIT: I added a POST option in the settings / configuration to the latest github code

Generic Camera Device

What happens if you set author to false and hardcode the user and pass in the url?

This looks like you didn’t enter the IP address properly in the settings of the device. Make sure you enter it as 192.168.100.123 do not enter http:// or anything like that.

Hello Patrick - How exactly do I add this code to my SmartThings (hub or App)? This is all new to me, sorry if this has been covered elsewhere.

I have a Mobotix camera that has 360 degree field of view I would love to be able to integrate into ST>

You need IDE access. https://graph.api.smartthings.com/

go into my device types https://graph.api.smartthings.com/ide/devices

click + New SmartDevice

fill out just name, namespace and author and click create

copy and paste my code from the github https://github.com/pstuart/smartthings/blob/master/generic_camera.groovy

Click the save button

Click the publish button, for me

Go to My Devices

click + Add New Devices

Give it the following:
Name = anything you want
device Network Id = unique id (should be the hex IP and hex port of the device but my code will auto insert that when you set the ip and take a photo)
Type = Generic Camera Device
Version = Published
Location = your location
hub = your hub
group = what folder do you want the “thing” in, none is the default.

then click create

you should then get a page that shows you the device info

look for a preferences section with a (edit) link.

Click the edit link, add the properties for the camera: (these are one of my camera’s settings, replace with your values)

Camera IP Address: 192.168.101.249
Camera Port: 80
Camera Path to Image: /SnapshotJPEG?Resolution=640x480&Quality=Clarity
Does Camera require User Auth?: true
Does Camera use a Post or Get, normally Get?: GET
Camera User: username
Camera Password: password

click save.

Go to your app and under things you should now see a new camera thing. Tap it.

Tap the take button, should see a picture show up if everything worked.

If not, go back into the IDE https://graph.api.smartthings.com/

Go to the Logs section https://graph.api.smartthings.com/ide/logs

go back to your app and device tap the take button again and paste in the console results for the camera.

6 Likes

Yeah… there is something wrong with your string manipulations. It’s returning ‘a00001a’ for ‘10.0.0.26’ (which ain’t right).

Yep, i did. I’m a server admin by day so i understand how this stuff works

Same here. I am also getting the wrong HEX value returned. i get “a000016” for “10.0.0.22”

By the way thanks Pat for doing this!!! you rock man!

I have removed the redundant host parsing and the prefs entry for it. This will work if you simply enter the correct device network ID in the device (i.e. 0A00001A:1A2D for 10.0.0.26:6701). This uses the same hex conversion as my other device-types so I’m pretty sure it will work with any IP/port. Thanks @pstuart for the image parsing!

metadata {
	definition (name: "D-Link Camera", namespace: "soletc.com", author: "Scottin Pollock") {
		capability "Image Capture"
		capability "Sensor"
		capability "Actuator"
	}

    preferences {
    input("CameraPath", "string", title:"Camera Path to Image", description: "Please enter the path to the image", defaultValue: "/image/jpeg.cgi", required: true, 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, 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 {
		standardTile("camera", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: true) {
			state "default", label: "", action: "", icon: "st.camera.dropcam-centered", backgroundColor: "#FFFFFF"
		}

		carouselTile("cameraDetails", "device.image", width: 3, height: 2) { }

		standardTile("take", "device.image", width: 1, 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"
		}

		main "camera"
		details(["cameraDetails", "take"])
	}
}

// parse events into attributes
def parse(String description) {
	log.debug "Parsing '${description}'"

	def map = stringToMap(description)

	def result = []

	if (map.bucket && map.key)
	{ //got a s3 pointer
		putImageInS3(map)
	}
	else if (map.headers && map.body)
	{ //got device info response

		def headerString = new String(map.headers.decodeBase64())
		if (headerString.contains("404 Not Found")) {
			state.snapshot = "/snapshot.cgi"
		}

		if (map.body) {
			def bodyString = new String(map.body.decodeBase64())
			def body = new XmlSlurper().parseText(bodyString)
			def productName = body?.productName?.text()
			if (productName)
			{
				log.trace "Product Name: $productName"
				state.snapshot = ""
			}
		}
	}

	result
}

def putImageInS3(map) {

	def s3ObjectContent

	try {
		def imageBytes = getS3Object(map.bucket, map.key + ".jpg")

		if(imageBytes)
		{
			s3ObjectContent = imageBytes.getObjectContent()
			def bytes = new ByteArrayInputStream(s3ObjectContent.bytes)
			storeImage(getPictureName(), bytes)
		}
	}
	catch(Exception e) {
		log.error e
	}
	finally {
		//explicitly close the stream
		if (s3ObjectContent) { s3ObjectContent.close() }
	}
}

// handle commands
def take() {
    def userpassascii = "${CameraUser}:${CameraPassword}"
    def userpass = "Basic " + userpassascii.encodeAsBase64().toString()
    def path = CameraPath //"/image/jpeg.cgi"
    log.debug "path is: $path"
    log.debug "Requires Auth: $CameraAuth"
    
    
   	if (CameraAuth) {
    	def hubAction = new physicalgraph.device.HubAction(
        method: "GET",
        path: path,
        headers: [HOST:getHostAddress(), Authorization:userpass]
        )
        
		hubAction.options = [outputMsgToS3:true]
   		hubAction
		}
    else
    {
    	def hubAction = new physicalgraph.device.HubAction(
        method: "GET",
        path: path,
        headers: [HOST:getHostAddress()]
        )
        
		hubAction.options = [outputMsgToS3:true]
    	hubAction
    	}
    }

private getPictureName() {
	def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '')
	return device.deviceNetworkId + "_$pictureUuid" + ".jpg"
}

private Long converIntToLong(ipAddress) {
	log.debug "from Long converIntToLong = $ipAddress"
	long result = 0
	def parts = ipAddress.split("\\.")
    for (int i = 3; i >= 0; i--) {
        result |= (Long.parseLong(parts[3 - i]) << (i * 8));
    }

    return result & 0xFFFFFFFF;
}

private String convertIPToHex(ipAddress) {
	return Long.toHexString(converIntToLong(ipAddress));
}

private Integer convertHexToInt(hex) {
log.debug 
	Integer.parseInt(hex,16)
}
private String convertHexToIP(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
}

It isn’t redundant, it just overrides the device setting since most people don’t know how to get the Hex ip and port of their device.

That code also allows you to change the ip in the settings and not have to go back into the IDE and change the deviceNetworkId since there is no way to change it in the App.

First of all, remove the [Host:… line, that contains your password… Probably don’t want that on a forum.

Second, try my latest code…

What is the ip and port you are trying to use, specifically the port, as it appears the port code to hex screwed up.

mine is 8006 for the port and it converts it correctly. mine seems to be failing at the IP conversion. :frowning:

I just updated the code, the length of the hex port was too long, for port # under 100 I have to pad 00 to the hex, but over that I have to just use the result. Should be fixed.

https://github.com/pstuart/smartthings/blob/master/generic_camera.groovy

Not having luck with this. Tried Post and Get. Entering 192.168.1.201:84400//Streaming/channels/1/picture for my camera in a browser pulls up an image after authenticating. In the device tile, after hitting “take”, it gets stuck on “taking” with no image displayed. Camera is a Hivision IP Cam.

ea0696fb-64c7-45b4-a8fe-045a98fe5069 12:09:55 PM: debug The method is GET
ea0696fb-64c7-45b4-a8fe-045a98fe5069 12:09:55 PM: debug c0a801c9:0020d0
ea0696fb-64c7-45b4-a8fe-045a98fe5069 12:09:55 PM: debug Uses which method: Get
ea0696fb-64c7-45b4-a8fe-045a98fe5069 12:09:55 PM: debug Requires Auth: true
ea0696fb-64c7-45b4-a8fe-045a98fe5069 12:09:55 PM: debug path is: /Streaming/channels/1/picture

ea0696fb-64c7-45b4-a8fe-045a98fe5069 12:10:42 PM: debug The method is POST
ea0696fb-64c7-45b4-a8fe-045a98fe5069 12:10:42 PM: debug c0a801c9:0020d0
ea0696fb-64c7-45b4-a8fe-045a98fe5069 12:10:42 PM: debug Uses which method: Post
ea0696fb-64c7-45b4-a8fe-045a98fe5069 12:10:42 PM: debug Requires Auth: true
ea0696fb-64c7-45b4-a8fe-045a98fe5069 12:10:42 PM: debug path is: /Streaming/channels/1/picture

I had to fix a bit of code for ports larger then 100

Also, remove the line above from your post:

ea0696fb-64c7-45b4-a8fe-045a98fe5069 12:09:55 PM: debug The Header is
[HOST:192.168.1.201:8400, Authorization:Basic THIS IS YOUR USERNAME AND PASSWORD]

Try the latest code

https://github.com/pstuart/smartthings/blob/master/generic_camera.groovy