Advertisement
Guest User

Untitled

a guest
Jan 13th, 2018
334
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 15.22 KB | None | 0 0
  1. /**
  2. * Nest Protect (Direct)
  3. * Author: chad@monroe.io
  4. * Author: nick@nickhbailey.com
  5. * Author: dianoga7@3dgo.net
  6. * Date: 2016.01.24
  7. *
  8. *
  9. * INSTALLATION
  10. * =========================================
  11. * 1) Create a new device type from code (https://graph.api.smartthings.com/ide/devices)
  12. * Copy and paste the below, save, publish "For Me"
  13. *
  14. * 2) Create a new device (https://graph.api.smartthings.com/device/list)
  15. * Name: Your Choice
  16. * Device Network Id: Your Choice
  17. * Type: Nest Protect (should be the last option)
  18. * Location: Choose the correct location
  19. * Hub/Group: Leave blank
  20. *
  21. * 3) Update device preferences
  22. * Click on the new device to see the details.
  23. * Click the edit button next to Preferences
  24. * Fill in your information.
  25. * To find your serial number, login to http://home.nest.com. Click on the smoke detector
  26. * you want to see. Under settings, go to Technical Info. Your serial number is
  27. * the second item.
  28. *
  29. * Original design/inspiration provided by:
  30. * -> https://github.com/sidjohn1/SmartThings-NestProtect
  31. * -> https://gist.github.com/Dianoga/6055918
  32. *
  33. * Copyright (C) 2016 Chad Monroe <chad@monroe.io>
  34. * Copyright (C) 2014 Nick Bailey <nick@nickhbailey.com>
  35. * Copyright (C) 2013 Brian Steere <dianoga7@3dgo.net>
  36. *
  37. * Permission is hereby granted, free of charge, to any person obtaining a copy of this
  38. * software and associated documentation files (the "Software"), to deal in the Software
  39. * without restriction, including without limitation the rights to use, copy, modify,
  40. * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
  41. * permit persons to whom the Software is furnished to do so, subject to the following
  42. * conditions: The above copyright notice and this permission notice shall be included
  43. * in all copies or substantial portions of the Software.
  44. *
  45. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  46. * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  47. * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  48. * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  49. * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
  50. * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  51. **/
  52.  
  53. /**
  54. * Static info
  55. * was using: https://home.nest.com/user/login
  56. */
  57. private NEST_LOGIN_URL() { "https://developer-api.nest.com" }
  58. private USER_AGENT_STR() { "Nest/1.1.0.10 CFNetwork/548.0.4" }
  59.  
  60. preferences
  61. {
  62. input( "username", "text", title: "Username", description: "Your Nest Username (usually an email address)", required: true, displayDuringSetup: true )
  63. input( "password", "password", title: "Password", description: "Your Nest Password", required: true, displayDuringSetup: true )
  64. input( "mac", "text", title: "MAC Address", description: "The MAC address of your smoke detector", required: true, displayDuringSetup: true )
  65. }
  66.  
  67. metadata
  68. {
  69. definition( name: "Nest Protect - Direct", author: "chad@monroe.io", namespace: "cmonroe" )
  70. {
  71. capability "Polling"
  72. capability "Refresh"
  73. capability "Battery"
  74. capability "Smoke Detector"
  75. capability "Carbon Monoxide Detector"
  76.  
  77. attribute "alarm_state", "string"
  78. attribute "night_light", "string"
  79. attribute "line_power", "string"
  80. attribute "co_previous_peak", "string"
  81. attribute "wifi_ip", "string"
  82. attribute "version_hw", "string"
  83. attribute "version_sw", "string"
  84. attribute "secondary_status", "string"
  85. }
  86.  
  87. simulator
  88. {
  89. /* TODO */
  90. }
  91.  
  92. tiles( scale: 2 )
  93. {
  94. multiAttributeTile( name:"alarm_state", type: "lighting", width: 6, height: 4 )
  95. {
  96. tileAttribute( "device.alarm_state", key: "PRIMARY_CONTROL" )
  97. {
  98. attributeState( "default", label:'--', icon: "st.unknown.unknown.unknown" )
  99. attributeState( "clear", label:"CLEAR", icon:"st.alarm.smoke.clear", backgroundColor:"#44b621" )
  100. attributeState( "smoke", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13" )
  101. attributeState( "co", label:"CO", icon:"st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor:"#e86d13" )
  102. attributeState( "tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13" )
  103. }
  104.  
  105. tileAttribute( "device.status_text", key: "SECONDARY_CONTROL" )
  106. {
  107. attributeState( "status_text", label: '${currentValue}', unit:"" )
  108. }
  109. }
  110.  
  111. standardTile( "smoke", "device.smoke", width: 2, height: 2 )
  112. {
  113. state( "default", label:'UNK', icon: "st.unknown.unknown.unknown" )
  114. state( "clear", label:"OK", icon:"st.alarm.smoke.clear", backgroundColor:"#44B621" )
  115. state( "detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13" )
  116. state( "tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13" )
  117. }
  118.  
  119. standardTile( "carbonMonoxide", "device.carbonMonoxide", width: 2, height: 2 )
  120. {
  121. state( "default", label:'UNK', icon: "st.unknown.unknown.unknown" )
  122. state( "clear", label:"OK", icon:"st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor:"#44B621" )
  123. state( "detected", label:"CO", icon:"st.alarm.carbon-monoxide.clear", backgroundColor:"#e86d13" )
  124. state( "tested", label:"TEST", icon:"st.alarm.carbon-monoxide.test", backgroundColor:"#e86d13" )
  125. }
  126.  
  127. standardTile( "night_light", "device.night_light", width: 2, height: 2 )
  128. {
  129. state( "default", label:'UNK', icon: "st.unknown.unknown.unknown" )
  130. state( "unk", label:'UNK', icon: "st.unknown.unknown.unknown" )
  131. state( "on", label: 'ON', icon: "st.switches.light.on", backgroundColor: "#44B621" )
  132. state( "low", label: 'LOW', icon: "st.switches.light.on", backgroundColor: "#44B621" )
  133. state( "med", label: 'MED', icon: "st.switches.light.on", backgroundColor: "#44B621" )
  134. state( "high", label: 'HIGH', icon: "st.switches.light.on", backgroundColor: "#44B621" )
  135. state( "off", label: 'OFF', icon: "st.switches.light.off", backgroundColor: "#ffffff" )
  136. }
  137.  
  138. valueTile( "version_hw", "device.version_hw", width: 2, height: 2, decoration: "flat" )
  139. {
  140. state( "default", label: 'Hardware ${currentValue}' )
  141. }
  142.  
  143. valueTile( "co_previous_peak", "device.co_previous_peak", width: 2, height: 2 )
  144. {
  145. state( "co_previous_peak", label: '${currentValue}', unit: "ppm",
  146. backgroundColors: [
  147. [value: 69, color: "#44B621"],
  148. [value: 70, color: "#e86d13"]
  149. ]
  150. )
  151. }
  152.  
  153. valueTile( "version_sw", "device.version_sw", width: 2, height: 2, decoration: "flat" )
  154. {
  155. state( "default", label: 'Software ${currentValue}' )
  156. }
  157.  
  158. standardTile("refresh", "device.refresh", inactiveLabel: false, width: 2, height: 2, decoration: "flat")
  159. {
  160. state( "default", label:'refresh', action:"polling.poll", icon:"st.secondary.refresh-icon" )
  161. }
  162.  
  163. valueTile("wifi_ip", "device.wifi_ip", inactiveLabel: false, width: 4, height: 2, decoration: "flat")
  164. {
  165. state( "default", label:'IP: ${currentValue}', height: 1, width: 2, inactiveLabel: false )
  166. }
  167.  
  168. main "alarm_state"
  169. details( ["alarm_state", "smoke", "carbonMonoxide", "night_light", "version_hw", "co_previous_peak", "version_sw", "wifi_ip", "refresh"] )
  170. }
  171. }
  172.  
  173.  
  174. /**
  175. * handle commands
  176. */
  177. def installed()
  178. {
  179. log.info "Nest Protect - Direct ${textVersion()}: ${textCopyright()} Installed"
  180. do_update()
  181. }
  182.  
  183. def initialize()
  184. {
  185. log.info "Nest Protect - Direct ${textVersion()}: ${textCopyright()} Initialized"
  186. do_update()
  187. }
  188.  
  189. def updated()
  190. {
  191. log.info "Nest Protect - Direct ${textVersion()}: ${textCopyright()} Updated"
  192. data.auth = null
  193. }
  194.  
  195. def poll()
  196. {
  197. log.debug "poll for protect with MAC: " + settings.mac.toUpperCase()
  198. do_update()
  199. }
  200.  
  201. def refresh()
  202. {
  203. log.debug "refresh for protect with MAC: " + settings.mac.toUpperCase()
  204. do_update()
  205. }
  206.  
  207. def reschedule()
  208. {
  209. log.debug "re-scheduling update for protect with MAC: " + settings.mac.toUpperCase()
  210. runIn( 300, 'do_update' )
  211. }
  212.  
  213. def do_update()
  214. {
  215. log.debug "refresh for device with MAC: " + settings.mac.toUpperCase()
  216.  
  217. api_exec( 'status', [] )
  218. {
  219. def status_text = ""
  220.  
  221. data.topaz = it.data.topaz.getAt( settings.mac.toUpperCase() )
  222.  
  223. //log.debug data.topaz
  224.  
  225. data.topaz.smoke_status = data.topaz.smoke_status == 0 ? 'clear' : 'detected'
  226. data.topaz.co_status = data.topaz.co_status == 0 ? 'clear' : 'detected'
  227. data.topaz.battery_health_state = data.topaz.battery_health_state == 0 ? 'ok' : 'low'
  228. data.topaz.kl_software_version = "v" + data.topaz.kl_software_version.split('Software ')[-1]
  229. data.topaz.model = "v" + data.topaz.model.split('-')[-1]
  230.  
  231. if ( data.topaz.night_light_enable )
  232. {
  233. switch ( data.topaz.night_light_brightness )
  234. {
  235. case 1:
  236. data.topaz.night_light_brightness = "low"
  237. break
  238. case 2:
  239. data.topaz.night_light_brightness = "med"
  240. break
  241. case 3:
  242. data.topaz.night_light_brightness = "high"
  243. break
  244. default:
  245. data.topaz.night_light_brightness = "on"
  246. break
  247. }
  248. }
  249. else
  250. {
  251. data.topaz.night_light_brightness = "off"
  252. }
  253.  
  254. if ( data.topaz.line_power_present )
  255. {
  256. data.topaz.line_power_present = "ok"
  257. }
  258. else
  259. {
  260. data.topaz.line_power_present = "dead"
  261. }
  262.  
  263. if ( !data.topaz.co_previous_peak )
  264. {
  265. /* protect 2.0 units do not support this */
  266. data.topaz.co_previous_peak = 'N/A'
  267. }
  268. else
  269. {
  270. data.topaz.co_previous_peak = "${data.topaz.co_previous_peak}ppm"
  271. }
  272.  
  273. sendEvent( name: 'smoke', value: data.topaz.smoke_status, descriptionText: "${device.displayName} smoke ${data.topaz.smoke_status}", displayed: false )
  274. sendEvent( name: 'carbonMonoxide', value: data.topaz.co_status, descriptionText: "${device.displayName} carbon monoxide ${data.topaz.co_status}", displayed: false )
  275. sendEvent( name: 'battery', value: data.topaz.battery_health_state, descriptionText: "${device.displayName} battery is ${data.topaz.battery_health_state}", displayed: false )
  276. sendEvent( name: 'night_light', value: data.topaz.night_light_brightness, descriptionText: "${device.displayName} night light is ${data.topaz.night_light_brightness}", displayed: true )
  277. sendEvent( name: 'line_power', value: data.topaz.line_power_present, descriptionText: "${device.displayName} line power is ${data.topaz.line_power_present}", displayed: false )
  278. sendEvent( name: 'co_previous_peak', value: data.topaz.co_previous_peak, descriptionText: "${device.displayName} previous CO peak (PPM) is ${data.topaz.co_previous_peak}", displayed: false )
  279. sendEvent( name: 'wifi_ip', value: data.topaz.wifi_ip_address, descriptionText: "${device.displayName} WiFi IP is ${data.topaz.wifi_ip_address}", displayed: false )
  280. sendEvent( name: 'version_hw', value: data.topaz.model, descriptionText: "${device.displayName} hardware model is ${data.topaz.model}", displayed: false )
  281. sendEvent( name: 'version_sw', value: data.topaz.kl_software_version, descriptionText: "${device.displayName} software version is ${data.topaz.kl_software_version}", displayed: false )
  282.  
  283. app_alarm_sm()
  284.  
  285. status_text = "Line Power: ${device.currentState('line_power').value} Battery: ${device.currentState('battery').value}"
  286. sendEvent( name: 'status_text', value: status_text, descriptionText: status_text, displayed: false )
  287.  
  288. log.debug "Smoke: ${data.topaz.smoke_status}"
  289. log.debug "CO: ${data.topaz.co_status}"
  290. log.debug "Battery: ${data.topaz.battery_health_state}"
  291. log.debug "Night Light: ${data.topaz.night_light_brightness}"
  292. log.debug "Line Power: ${data.topaz.line_power_present}"
  293. log.debug "CO Previous Peak (PPM): ${data.topaz.co_previous_peak}"
  294. log.debug "WiFi IP: ${data.topaz.wifi_ip_address}"
  295. log.debug "Hardware Version: ${data.topaz.model}"
  296. log.debug "Software Version: ${data.topaz.kl_software_version}"
  297. }
  298.  
  299. reschedule()
  300. }
  301.  
  302. /**
  303. * state machine for setting global alarm state of app
  304. */
  305. def app_alarm_sm()
  306. {
  307. def alarm_state = "clear"
  308. def smoke = data.topaz.smoke_status
  309. def co = data.topaz.co_status
  310.  
  311. switch( smoke )
  312. {
  313. case 'clear':
  314. if ( co != "clear" )
  315. {
  316. alarm_state = "co"
  317. }
  318. break
  319. case 'detected':
  320. alarm_state = "smoke"
  321. break
  322. case 'tested':
  323. default:
  324. /**
  325. * ensure that real co alarm is not set before sending tested alarm for smoke
  326. */
  327. if ( co == 'detected' )
  328. {
  329. alarm_state = "co"
  330. }
  331. break
  332. }
  333.  
  334. log.info "alarm state machine finished, sending event.."
  335. log.info "alarm_state: ${alarm_state} smoke: ${smoke} CO: ${co}"
  336.  
  337. sendEvent( name: 'alarm_state', value: alarm_state, descriptionText: "Alarm: ${alarm_state} (Smoke/CO: ${smoke}/${co})", type: "physical", displayed: true, isStateChange: true )
  338. }
  339.  
  340. /**
  341. * main entry point for nest API calls
  342. */
  343. def api_exec(method, args = [], success = {})
  344. {
  345. log.debug "API exec method: ${method} with args: ${args}"
  346.  
  347. if( !logged_in() )
  348. {
  349. log.debug "login required"
  350.  
  351. login(method, args, success)
  352. return
  353. }
  354.  
  355. if( method == null )
  356. {
  357. log.info "API exec with no method passed and we are already logged in; bailing"
  358. return
  359. }
  360.  
  361. def methods =
  362. [
  363. 'status':
  364. [
  365. uri: "/v2/mobile/${data.auth.user}", type: 'get'
  366. ],
  367. ]
  368.  
  369. def request = methods.getAt( method )
  370.  
  371. log.debug "already logged in"
  372.  
  373. handle_request( request.uri, args, request.type, success )
  374. }
  375.  
  376. /**
  377. * handle_request() only works once logged in, therefor
  378. * call api_exec() rather than this method directly.
  379. */
  380. def handle_request(uri, args, type, success)
  381. {
  382. log.debug "handling request type: ${type} at URI: ${uri} with args: ${args}"
  383.  
  384. if( uri.charAt(0) == '/' )
  385. {
  386. uri = "${data.auth.urls.transport_url}${uri}"
  387. }
  388.  
  389. def params =
  390. [
  391. uri: uri,
  392. headers:
  393. [
  394. 'X-nl-protocol-version': 1,
  395. 'X-nl-user-id': data.auth.userid,
  396. 'Authorization': "Basic ${data.auth.access_token}",
  397. 'Accept-Language': 'en-us',
  398. 'userAgent': USER_AGENT_STR()
  399. ],
  400. body: args
  401. ]
  402.  
  403. def post_request = { response ->
  404.  
  405. if( response.getStatus() == 302 )
  406. {
  407. def locations = response.getHeaders( "Location" )
  408. def location = locations[0].getValue()
  409.  
  410. log.debug "redirecting to ${location}"
  411.  
  412. handle_request( location, args, type, success )
  413. }
  414. else
  415. {
  416. def retrievedStatus = response.getStatus()
  417. log.debug "got ${retrievedStatus} when we wanted 302"
  418. success.call( response )
  419. }
  420. }
  421.  
  422. try
  423. {
  424. if( type == 'get' )
  425. {
  426. httpGet( params, post_request )
  427. }
  428. }
  429. catch( Throwable e )
  430. {
  431. login()
  432. }
  433. }
  434.  
  435. def login(method = null, args = [], success = {})
  436. {
  437. def params =
  438. [
  439. uri: NEST_LOGIN_URL(),
  440. body: [ username: settings.username, password: settings.password ]
  441. ]
  442.  
  443. httpPost( params ) { response ->
  444.  
  445. data.auth = response.data
  446. data.auth.expires_in = Date.parse('EEE, dd-MMM-yyyy HH:mm:ss z', response.data.expires_in).getTime()
  447. log.debug data.auth
  448.  
  449. api_exec( method, args, success )
  450. }
  451. }
  452.  
  453. def logged_in()
  454. {
  455. if( !data.auth )
  456. {
  457. log.debug "data.auth is missing, not logged in"
  458. return false
  459. }
  460.  
  461. def now = new Date().getTime();
  462.  
  463. return( data.auth.expires_in > now )
  464. }
  465.  
  466. private def textVersion()
  467. {
  468. def text = "Version 1.6"
  469. }
  470.  
  471. private def textCopyright()
  472. {
  473. def text = "Copyright © 2016 Chad Monroe <chad@monroe.io>"
  474. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement