I needed a simple temperature sensor and had an old Raspberry Pi laying around. Of course I wanted to have Smartthings integration as well. Adding a temperature sensor to a Pi is an old project for the Pi (posted at Adafruit’s website), and using REST endpoints, a SmartApp and a simulated temperature measurement device handler, the Pi can keep Smartthings updated. I thought someone else might be interested - it illustrates some basic Smartthings stuff for those trying to learn.
Pi Temperature Sensor with Smartthings Integration
Instructions for building a DS18B20 temperature sensor in a Raspberry Pi can be found here: [Raspberry Pi Lesson 11 DS18B20 Temperature Sensing]
I would not say this is a particularly cost effective way to get a temperature sensor if you’re only goal was to save money. However, if you happen to have a Pi laying around doing nothing (as I did), or had a Pi doing something and you wanted it to pull double duty monitoring temperature, or maybe if you can get one of those $5 Pi Zeros, then this should be cost effective for you. The delta in cost to add temperature sensing to an existing Pi is small - a couple of dollars.
The things you need:
A Raspberry Pi (of any sort - I used a Model B) with Raspbian installed
A DS18B20. I used the waterproof one, but the transistor-y looking one would work. As noted - do not use any of the TMP36 devices - those look the same but are analog devices.
A pull up resistor (4.7k. or 10k.)
A case (optional, but practical)
Some jumper wires, a little solder, and a little electrical tape
The waterproof version contains 3 leads. If you read the Adafruit lesson, you might think the lead colors are standard. They are not. And because these things aren’t very expensive (I paid US$6 for 5 of them), don’t expect much in the way of documentation. On mine, the blue wire is the data line, the red wire is the power line (VCC), and the black wire is ground. I suggest you google the colors you have. There also seem to be quite a few complaints of DOA ones, so if yours doesn’t work and you’re pretty sure you followed the instructions properly, then try another one.
What we are trying to build looks like this to those of you who read schematic:
What I did was get a bunch of jumper wires (40 for $5), and, cut 3 of them in half, and stripped the cut end. Then I twisted together the wires (or wrapped the wire around the resistor lead), and dropped a little solder on it to hold it in place (you might want to test that you’re ds18b20 works before dropping the solder).
Then I taped the resistor and wires to the top of case to keep them electrically isolated. Also, I wrapped a little electrical tape around the wire sheath - the part that will stay inside the case - as a cord strain relief.
The completed Pi with Temperature Sensor:
BTW I understand that you can connect more than one of these ds18b20’s in parallel which is kind of cool. It is suggested that you do one first, then label it with serial number of the device, so that you can tell them apart. I didn’t try this - I only needed, essentially, a single digital thermometer.
Now boot up the Pi. First, we are going to make sure that the Pi is properly setup and the DS18B20 is working.
The first thing we need to do is edit /etc/config.txt. It may be a little confusing, but we are not intending to use i2c for this device, but we need to enable 1-wire support by adding the line:
dtoverlay=w1-gpio
Reboot the Pi. When its back up and you’re logged in again, load the drivers in a terminal window as shown below. When you are in the ‘devices’ directory, the directory starting ‘28-’ will have a different name (the xxxx is the serial number of the device, so cd to the name of whatever directory is there. If you don’t see it, then something’s wrong.
sudo modprobe w1-gpio
sudo modprobe w1-therm
cd /sys/bus/w1/devices
ls
cd 28-xxxx (change this to match what serial number pops up)
cat w1_slave
You should see something like this:
09 01 4b 46 7f ff 0c 10 9c : crc=9c YES
09 01 4b 46 7f ff 0c 10 9c t=16562
There is some unreliability, but the YES at the end of the first line tells you that the temperature is valid. The temperature is the t=16562 at the end of the second line, in other words, 16.562 °C. At this point you know you have the sensor working properly.
Now we want to integrate it with Smartthings. We do this by creating a Smartapp with a REST endpoint that the Pi can call into. Then we create a simulated temperature sensor device that the Smartapp can update.
When we create the Smarttapp, we need to enable OAuth. The Smartapp itself is pretty simple, but obtaining the authorization code that the Pi uses to call into Smartthings is a bit cumbersome (at least at the time of writing).
Create the Smartapp from this code, remembering to enable OAuth:
/**
* Pi Temperature Sensor Endpoints
*
* Copyright 2016 Paul Cifarelli
*
* 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.
*
*/
definition(
name: "Pi Temperature Sensor",
namespace: "pcifarelli",
author: "Paul Cifarelli",
description: "REST endpoint for Pi to update a Simulated Temperature Sensor",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
oauth: [displayName: "Cifarelli.net", displayLink: ""])
preferences {
section("Allow Endpoint to Control This Thing") {
input "tdevice", "capability.temperatureMeasurement", title: "Which Simulated Temperature Sensor?", multiple: false
}
}
mappings {
path("/update/:temp/:units") {
action: [
PUT: "updateTemp"
]
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
// TODO: subscribe to attributes, devices, locations, etc.
}
// implement event handlers
void updateTemp() {
update(tdevice)
}
private void update(device) {
log.debug "update, request: params: ${params['temp']} ${params['units']} ${device.name}"
def t = 0
if (location.temperatureScale == params['units']) {
log.debug "yes, the temperatureScale is the same (${params['units']})"
t = Double.parseDouble(params['temp'])
} else if (params['units'] == "F") {
// so location is set for C
t = 5 * ( Double.parseDouble(params['temp']) - 32 ) / 9
} else if (params['units'] == "C") {
// so location is set for F
t = 9 * Double.parseDouble(params['temp']) / 5 + 32
}
def x = Math.round(t * 100) / 100
tdevice.setTemperature(x)
}
The device handler you need to create depends on whether you want °F or °C. The Smartapp is able to use your location settings to determine the preferred temperature scale, but I couldn’t find a good way to do this in the device handler, since the value tiles are statically defined. But the device handler is really simple, so having 2 of them isn’t too big a deal:
For °F:
/**
* Copyright 2016 Paul Cifarelli
*
* 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: "Pi Simulated Temperature Sensor °F", namespace: "pcifarelli", author: "Paul Cifarelli") {
capability "Temperature Measurement"
capability "Sensor"
command "setTemperature", ["number"]
}
// UI tile definition
tiles(scale:2) {
valueTile("temperature", "device.temperature", height: 4, width: 6, canChangeIcon: true) {
state("temperature", label:'${currentValue}°', unit:"F",
backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
)
}
main "temperature"
details("temperature")
}
}
// Parse incoming device messages to generate events
def parse(String description) {
def pair = description.split(":")
createEvent(name: pair[0].trim(), value: pair[1].trim())
}
def setTemperature(value) {
sendEvent(name:"temperature", value: value)
}
And for °C:
/**
* Copyright 2016 Paul Cifarelli
*
* 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: "Pi Simulated Temperature Sensor °C", namespace: "pcifarelli", author: "Paul Cifarelli") {
capability "Temperature Measurement"
capability "Sensor"
command "setTemperature", ["number"]
}
// UI tile definitions
tiles(scale:2) {
valueTile("temperature", "device.temperature", height: 4, width: 6, canChangeIcon: true) {
state("temperature", label:'${currentValue}°', unit:"C",
backgroundColors:[
[value: 0, color: "#153591"],
[value: 7, color: "#1e9cbb"],
[value: 15, color: "#90d2a7"],
[value: 23, color: "#44b621"],
[value: 29, color: "#f1d801"],
[value: 35, color: "#d04e00"],
[value: 36, color: "#bc2323"]
]
)
}
main "temperature"
details("temperature")
}
}
// Parse incoming device messages to generate events
def parse(String description) {
def pair = description.split(":")
createEvent(name: pair[0].trim(), value: pair[1].trim())
}
def setTemperature(value) {
sendEvent(name:"temperature", value: value)
}
Once one of these is created, you have to create a simulated device. Do this in the web developer pages, under “My Devices”, click “+ New Device”. Give it a name and a device id (which can be anything, as long as it’s unique). From the Type dropdown select the Pi Temperature Sensor device handler you just created (your custom device handlers are at the bottom of the list). Finally, select your location and hub and click “Create”.
Now you’ll need the client id and the client secret from the App Settings OAuth section. You use this to get an authorization token. Do not share these with anyone (which should go without saying, but…)
Smartthings uses OAuth2 - which is gives an auth token that will expire, the time it will expire, and a refresh token to use to refresh it when it does expire. Currently, Smartthings issues auth tokens that expire in 50 years, so we can ignore, for now, the fact that they expire.
I don’t know any way of getting an auth token without having a webpage in a browser, since you need to be redirected to a Smartthings page to select the hub and device on that hub to authorize. I do know someone set up a public website to do this, but I didn’t want to use it because I didn’t know how safe/secure it was (don’t share your client secret with anyone…). In our case, we want to give access to the Pi Simulated Temperature Sensor °F (or the Pi Simulated Temperature Sensor °C if you prefer). The page below redirects you to the Smartthings graph, allowing you to select the Hub and Device. Then it redirects back to your page with the access token.
One thing to point out (that I learned the hard way) is that your Smartapp must not have errors or throw exceptions when you are trying to authorize it. If it does, you will get a cryptic message saying that it cannot authorize you at this time, try later or contact support. The Smartapp doesn’t actually have to do anything though, just be free of errors.
Perhaps someone will know a better way to do this, but in the meantime you can use this PHP page I created (well, found/copied/modified) to return the auth token:
<?php
//client id and client secret
$client = '<put your client id from the App Settings here>';
$secret = '<put your client secret from the App Settings here>';
// the full url to redirect to this file
$url = get_this_url();
$f = array( 'code' => FILTER_SANITIZE_STRING, 'access_token' => FILTER_SANITIZE_STRING );
$request = filter_input_array(INPUT_GET, $f);
//STEP 1 - Get Access Code
if(!isset($request['code']) && !isset($request['access_token']))
{
header(
"Location: https://graph.api.smartthings.com/oauth/authorize?response_type=code&client_id=$client&redirect_uri=".$url."&scope=app"
);
}
//STEP 2 - Use Access Code to claim Access Token
else if(isset($request['code']))
{
$code = $request['code'];
$base = "https://graph.api.smartthings.com/oauth/token";
$page =
"$base?grant_type=authorization_code&client_id=".$client."&client_secret=".$secret."&redirect_uri=".$url."&code=".$code."&scope=app";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $page );
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt($ch, CURLOPT_POST, 0 );
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
$response = json_decode(curl_exec($ch),true);
curl_close($ch);
if(isset($response['access_token']))
{
//Redirect to self with access token for step 3 for ease of bookmarking
header( "Location: ?access_token=".$response['access_token'] ) ;
}
else
{
print "error requesting access token...";
print_r($response);
}
}
//Step 3 - Lookup Endpoint and write out urls
else if(isset($request['access_token']))
{
$url = "https://graph.api.smartthings.com/api/smartapps/endpoints/$client?access_token=".$request['access_token'];
$json = implode('', file($url));
$theEndpoints = json_decode($json,true);
print "<html><head><style>h3{margin-left:10px;}a:hover{background-color:#c4c4c4;} a{border:1px solid black; padding:5px; margin:5px;text-decoration:none;color:black;border-radius:5px;background-color:#dcdcdc}</style></head><body>";
print "<i>Save the above URL (access_token) for future reference.</i>";
print " <i>Right Click on buttons to copy link address.</i>";
}
function safe_server_var( $var, $op = FILTER_SANITIZE_STRING )
{
if (filter_has_var(INPUT_SERVER, $var))
{
$ret_val = filter_input(INPUT_SERVER, $var, $op, FILTER_NULL_ON_FALURE);
}
else
{
$ret_val = "";
}
return $ret_val;
}
function get_server_home($server_port = 0)
{
$sname = safe_server_var("SERVER_NAME");
$addr = safe_server_var("SERVER_ADDR");
return ( make_url($sname != "" ? $sname : $addr, $server_port) );
}
function make_url($server_addr, $server_port = 0)
{
$proto = "http://";
$sproto = safe_server_var("HTTPS", FILTER_UNSAFE_RAW);
if ($sproto != "")
{
$proto = "https://";
}
$sport = safe_server_var("SERVER_PORT");
$port = $server_port == 0 ? $sport : $server_port;
if (($proto == "http://" && $port == 80) || ($proto == "https://" && $port == 443))
{
return ( $proto . $server_addr );
}
else
{
return ( $proto . $server_addr . ":" . $port);
}
}
function get_this_url()
{
$page = safe_server_var("PHP_SELF");
return get_server_home() . htmlspecialchars($page);
}
?>
Now that you have the access token, we can go back to the Pi and setup the update script (which we will write in python, but feel free to do it however you want).
The script runs ‘niters’ times, updating Smartthings once at the start and then only if the temperature changes. It also checks the temperature every ‘interval’ seconds. The idea is to run this in a cron job - this way the temp sensor checks in at least every so often, and we know it’s still working. I have it setup to run for 15 minutes, checking the temperature every 15 seconds (niter=60, interval=15). This is run in a cron job scheduled to run every 15 minutes - this way we have continued monitoring of the temperature while also checking in to Smartthings at least every 15 minutes.
First though, make sure that you have python3-requests installed:
sudo apt-get install python3-requests
I also upgraded to curl4, but it’s probably not necessary:
sudo apt-get install libcurl4-openssl-dev
Here’s the code for ‘updateFromPi.py’:
#!/usr/bin/python3
import sys
import glob
import re
from io import BytesIO
import json
import pprint
import requests
import time
import subprocess
#client id and client secret
client = '<put client id here>'
access_token='<put access token here>'
base_dir = '/sys/bus/w1/devices/'
device_folder = glob.glob(base_dir + '28*')[0]
device_file = device_folder + '/w1_slave'
niters = 60
interval = 15
def read_temp_raw_old():
f = open(device_file, 'r')
lines = f.readlines()
f.close()
return lines
def read_temp_raw():
catdata = subprocess.Popen(['cat',device_file],stdout=subprocess.PIPE,stderr=subprocess.PIPE)
out,err = catdata.communicate()
out_decode = out.decode('utf-8')
lines = out_decode.split('\n')
return lines
def read_temp():
lines = read_temp_raw()
while lines[0].strip()[-3:] != 'YES':
time.sleep(0.2)
lines = read_temp_raw()
equals_pos = lines[1].find('t=')
if equals_pos != -1:
temp_string = lines[1][equals_pos+2:]
temp_c = float(temp_string) / 1000.0
temp_f = temp_c * 9.0 / 5.0 + 32.0
return temp_c, temp_f
def main():
old_temp = -999999999.99
endpoints_url = "https://graph.api.smartthings.com/api/smartapps/endpoints/%s?access_token=%s" % (client, access_token)
r = requests.get(endpoints_url)
if (r.status_code != 200):
print("Error: " + r.status_code)
else:
theendpoints = json.loads( r.text )
for counter in range(niters, 0, -1):
(temp_c, temp_f) = read_temp()
for endp in theendpoints:
uri = endp['uri']
temp_url = uri + ("/update/%.2f/F" % temp_f)
headers = { 'Authorization' : 'Bearer ' + access_token }
if ( round(temp_f, 2) != round(old_temp, 2) ):
r = requests.put(temp_url, headers=headers)
old_temp = temp_f
time.sleep(interval)
main()
Now, you can run this from the command line, and you should to test. Go to the Smartthings App and show your device. It should show the temperature updating: