Advertisement
Guest User

Untitled

a guest
Jan 13th, 2018
312
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 15.19 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. */
  56. private NEST_LOGIN_URL() { "https://home.nest.com/user/login" }
  57. private USER_AGENT_STR() { "Nest/1.1.0.10 CFNetwork/548.0.4" }
  58.  
  59. preferences
  60. {
  61. input( "username", "text", title: "Username", description: "Your Nest Username (usually an email address)", required: true, displayDuringSetup: true )
  62. input( "password", "password", title: "Password", description: "Your Nest Password", required: true, displayDuringSetup: true )
  63. input( "mac", "text", title: "MAC Address", description: "The MAC address of your smoke detector", required: true, displayDuringSetup: true )
  64. }
  65.  
  66. metadata
  67. {
  68. definition( name: "Nest Protect - Direct", author: "chad@monroe.io", namespace: "cmonroe" )
  69. {
  70. capability "Polling"
  71. capability "Refresh"
  72. capability "Battery"
  73. capability "Smoke Detector"
  74. capability "Carbon Monoxide Detector"
  75.  
  76. attribute "alarm_state", "string"
  77. attribute "night_light", "string"
  78. attribute "line_power", "string"
  79. attribute "co_previous_peak", "string"
  80. attribute "wifi_ip", "string"
  81. attribute "version_hw", "string"
  82. attribute "version_sw", "string"
  83. attribute "secondary_status", "string"
  84. }
  85.  
  86. simulator
  87. {
  88. /* TODO */
  89. }
  90.  
  91. tiles( scale: 2 )
  92. {
  93. multiAttributeTile( name:"alarm_state", type: "lighting", width: 6, height: 4 )
  94. {
  95. tileAttribute( "device.alarm_state", key: "PRIMARY_CONTROL" )
  96. {
  97. attributeState( "default", label:'--', icon: "st.unknown.unknown.unknown" )
  98. attributeState( "clear", label:"CLEAR", icon:"st.alarm.smoke.clear", backgroundColor:"#44b621" )
  99. attributeState( "smoke", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13" )
  100. attributeState( "co", label:"CO", icon:"st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor:"#e86d13" )
  101. attributeState( "tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13" )
  102. }
  103.  
  104. tileAttribute( "device.status_text", key: "SECONDARY_CONTROL" )
  105. {
  106. attributeState( "status_text", label: '${currentValue}', unit:"" )
  107. }
  108. }
  109.  
  110. standardTile( "smoke", "device.smoke", width: 2, height: 2 )
  111. {
  112. state( "default", label:'UNK', icon: "st.unknown.unknown.unknown" )
  113. state( "clear", label:"OK", icon:"st.alarm.smoke.clear", backgroundColor:"#44B621" )
  114. state( "detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13" )
  115. state( "tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13" )
  116. }
  117.  
  118. standardTile( "carbonMonoxide", "device.carbonMonoxide", width: 2, height: 2 )
  119. {
  120. state( "default", label:'UNK', icon: "st.unknown.unknown.unknown" )
  121. state( "clear", label:"OK", icon:"st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor:"#44B621" )
  122. state( "detected", label:"CO", icon:"st.alarm.carbon-monoxide.clear", backgroundColor:"#e86d13" )
  123. state( "tested", label:"TEST", icon:"st.alarm.carbon-monoxide.test", backgroundColor:"#e86d13" )
  124. }
  125.  
  126. standardTile( "night_light", "device.night_light", width: 2, height: 2 )
  127. {
  128. state( "default", label:'UNK', icon: "st.unknown.unknown.unknown" )
  129. state( "unk", label:'UNK', icon: "st.unknown.unknown.unknown" )
  130. state( "on", label: 'ON', icon: "st.switches.light.on", backgroundColor: "#44B621" )
  131. state( "low", label: 'LOW', icon: "st.switches.light.on", backgroundColor: "#44B621" )
  132. state( "med", label: 'MED', icon: "st.switches.light.on", backgroundColor: "#44B621" )
  133. state( "high", label: 'HIGH', icon: "st.switches.light.on", backgroundColor: "#44B621" )
  134. state( "off", label: 'OFF', icon: "st.switches.light.off", backgroundColor: "#ffffff" )
  135. }
  136.  
  137. valueTile( "version_hw", "device.version_hw", width: 2, height: 2, decoration: "flat" )
  138. {
  139. state( "default", label: 'Hardware ${currentValue}' )
  140. }
  141.  
  142. valueTile( "co_previous_peak", "device.co_previous_peak", width: 2, height: 2 )
  143. {
  144. state( "co_previous_peak", label: '${currentValue}', unit: "ppm",
  145. backgroundColors: [
  146. [value: 69, color: "#44B621"],
  147. [value: 70, color: "#e86d13"]
  148. ]
  149. )
  150. }
  151.  
  152. valueTile( "version_sw", "device.version_sw", width: 2, height: 2, decoration: "flat" )
  153. {
  154. state( "default", label: 'Software ${currentValue}' )
  155. }
  156.  
  157. standardTile("refresh", "device.refresh", inactiveLabel: false, width: 2, height: 2, decoration: "flat")
  158. {
  159. state( "default", label:'refresh', action:"polling.poll", icon:"st.secondary.refresh-icon" )
  160. }
  161.  
  162. valueTile("wifi_ip", "device.wifi_ip", inactiveLabel: false, width: 4, height: 2, decoration: "flat")
  163. {
  164. state( "default", label:'IP: ${currentValue}', height: 1, width: 2, inactiveLabel: false )
  165. }
  166.  
  167. main "alarm_state"
  168. details( ["alarm_state", "smoke", "carbonMonoxide", "night_light", "version_hw", "co_previous_peak", "version_sw", "wifi_ip", "refresh"] )
  169. }
  170. }
  171.  
  172.  
  173. /**
  174. * handle commands
  175. */
  176. def installed()
  177. {
  178. log.info "Nest Protect - Direct ${textVersion()}: ${textCopyright()} Installed"
  179. do_update()
  180. }
  181.  
  182. def initialize()
  183. {
  184. log.info "Nest Protect - Direct ${textVersion()}: ${textCopyright()} Initialized"
  185. do_update()
  186. }
  187.  
  188. def updated()
  189. {
  190. log.info "Nest Protect - Direct ${textVersion()}: ${textCopyright()} Updated"
  191. data.auth = null
  192. }
  193.  
  194. def poll()
  195. {
  196. log.debug "poll for protect with MAC: " + settings.mac.toUpperCase()
  197. do_update()
  198. }
  199.  
  200. def refresh()
  201. {
  202. log.debug "refresh for protect with MAC: " + settings.mac.toUpperCase()
  203. do_update()
  204. }
  205.  
  206. def reschedule()
  207. {
  208. log.debug "re-scheduling update for protect with MAC: " + settings.mac.toUpperCase()
  209. runIn( 300, 'do_update' )
  210. }
  211.  
  212. def do_update()
  213. {
  214. log.debug "refresh for device with MAC: " + settings.mac.toUpperCase()
  215.  
  216. api_exec( 'status', [] )
  217. {
  218. def status_text = ""
  219.  
  220. data.topaz = it.data.topaz.getAt( settings.mac.toUpperCase() )
  221.  
  222. //log.debug data.topaz
  223.  
  224. data.topaz.smoke_status = data.topaz.smoke_status == 0 ? 'clear' : 'detected'
  225. data.topaz.co_status = data.topaz.co_status == 0 ? 'clear' : 'detected'
  226. data.topaz.battery_health_state = data.topaz.battery_health_state == 0 ? 'ok' : 'low'
  227. data.topaz.kl_software_version = "v" + data.topaz.kl_software_version.split('Software ')[-1]
  228. data.topaz.model = "v" + data.topaz.model.split('-')[-1]
  229.  
  230. if ( data.topaz.night_light_enable )
  231. {
  232. switch ( data.topaz.night_light_brightness )
  233. {
  234. case 1:
  235. data.topaz.night_light_brightness = "low"
  236. break
  237. case 2:
  238. data.topaz.night_light_brightness = "med"
  239. break
  240. case 3:
  241. data.topaz.night_light_brightness = "high"
  242. break
  243. default:
  244. data.topaz.night_light_brightness = "on"
  245. break
  246. }
  247. }
  248. else
  249. {
  250. data.topaz.night_light_brightness = "off"
  251. }
  252.  
  253. if ( data.topaz.line_power_present )
  254. {
  255. data.topaz.line_power_present = "ok"
  256. }
  257. else
  258. {
  259. data.topaz.line_power_present = "dead"
  260. }
  261.  
  262. if ( !data.topaz.co_previous_peak )
  263. {
  264. /* protect 2.0 units do not support this */
  265. data.topaz.co_previous_peak = 'N/A'
  266. }
  267. else
  268. {
  269. data.topaz.co_previous_peak = "${data.topaz.co_previous_peak}ppm"
  270. }
  271.  
  272. sendEvent( name: 'smoke', value: data.topaz.smoke_status, descriptionText: "${device.displayName} smoke ${data.topaz.smoke_status}", displayed: false )
  273. sendEvent( name: 'carbonMonoxide', value: data.topaz.co_status, descriptionText: "${device.displayName} carbon monoxide ${data.topaz.co_status}", displayed: false )
  274. sendEvent( name: 'battery', value: data.topaz.battery_health_state, descriptionText: "${device.displayName} battery is ${data.topaz.battery_health_state}", displayed: false )
  275. sendEvent( name: 'night_light', value: data.topaz.night_light_brightness, descriptionText: "${device.displayName} night light is ${data.topaz.night_light_brightness}", displayed: true )
  276. sendEvent( name: 'line_power', value: data.topaz.line_power_present, descriptionText: "${device.displayName} line power is ${data.topaz.line_power_present}", displayed: false )
  277. 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 )
  278. sendEvent( name: 'wifi_ip', value: data.topaz.wifi_ip_address, descriptionText: "${device.displayName} WiFi IP is ${data.topaz.wifi_ip_address}", displayed: false )
  279. sendEvent( name: 'version_hw', value: data.topaz.model, descriptionText: "${device.displayName} hardware model is ${data.topaz.model}", displayed: false )
  280. sendEvent( name: 'version_sw', value: data.topaz.kl_software_version, descriptionText: "${device.displayName} software version is ${data.topaz.kl_software_version}", displayed: false )
  281.  
  282. app_alarm_sm()
  283.  
  284. status_text = "Line Power: ${device.currentState('line_power').value} Battery: ${device.currentState('battery').value}"
  285. sendEvent( name: 'status_text', value: status_text, descriptionText: status_text, displayed: false )
  286.  
  287. log.debug "Smoke: ${data.topaz.smoke_status}"
  288. log.debug "CO: ${data.topaz.co_status}"
  289. log.debug "Battery: ${data.topaz.battery_health_state}"
  290. log.debug "Night Light: ${data.topaz.night_light_brightness}"
  291. log.debug "Line Power: ${data.topaz.line_power_present}"
  292. log.debug "CO Previous Peak (PPM): ${data.topaz.co_previous_peak}"
  293. log.debug "WiFi IP: ${data.topaz.wifi_ip_address}"
  294. log.debug "Hardware Version: ${data.topaz.model}"
  295. log.debug "Software Version: ${data.topaz.kl_software_version}"
  296. }
  297.  
  298. reschedule()
  299. }
  300.  
  301. /**
  302. * state machine for setting global alarm state of app
  303. */
  304. def app_alarm_sm()
  305. {
  306. def alarm_state = "clear"
  307. def smoke = data.topaz.smoke_status
  308. def co = data.topaz.co_status
  309.  
  310. switch( smoke )
  311. {
  312. case 'clear':
  313. if ( co != "clear" )
  314. {
  315. alarm_state = "co"
  316. }
  317. break
  318. case 'detected':
  319. alarm_state = "smoke"
  320. break
  321. case 'tested':
  322. default:
  323. /**
  324. * ensure that real co alarm is not set before sending tested alarm for smoke
  325. */
  326. if ( co == 'detected' )
  327. {
  328. alarm_state = "co"
  329. }
  330. break
  331. }
  332.  
  333. log.info "alarm state machine finished, sending event.."
  334. log.info "alarm_state: ${alarm_state} smoke: ${smoke} CO: ${co}"
  335.  
  336. sendEvent( name: 'alarm_state', value: alarm_state, descriptionText: "Alarm: ${alarm_state} (Smoke/CO: ${smoke}/${co})", type: "physical", displayed: true, isStateChange: true )
  337. }
  338.  
  339. /**
  340. * main entry point for nest API calls
  341. */
  342. def api_exec(method, args = [], success = {})
  343. {
  344. log.debug "API exec method: ${method} with args: ${args}"
  345.  
  346. if( !logged_in() )
  347. {
  348. log.debug "login required"
  349.  
  350. login(method, args, success)
  351. return
  352. }
  353.  
  354. if( method == null )
  355. {
  356. log.info "API exec with no method passed and we are already logged in; bailing"
  357. return
  358. }
  359.  
  360. def methods =
  361. [
  362. 'status':
  363. [
  364. uri: "/v2/mobile/${data.auth.user}", type: 'get'
  365. ],
  366. ]
  367.  
  368. def request = methods.getAt( method )
  369.  
  370. log.debug "already logged in"
  371.  
  372. handle_request( request.uri, args, request.type, success )
  373. }
  374.  
  375. /**
  376. * handle_request() only works once logged in, therefor
  377. * call api_exec() rather than this method directly.
  378. */
  379. def handle_request(uri, args, type, success)
  380. {
  381. log.debug "handling request type: ${type} at URI: ${uri} with args: ${args}"
  382.  
  383. if( uri.charAt(0) == '/' )
  384. {
  385. uri = "${data.auth.urls.transport_url}${uri}"
  386. }
  387.  
  388. def params =
  389. [
  390. uri: uri,
  391. headers:
  392. [
  393. 'X-nl-protocol-version': 1,
  394. 'X-nl-user-id': data.auth.userid,
  395. 'Authorization': "Basic ${data.auth.access_token}",
  396. 'Accept-Language': 'en-us',
  397. 'userAgent': USER_AGENT_STR()
  398. ],
  399. body: args
  400. ]
  401.  
  402. def post_request = { response ->
  403. /**
  404. * 302
  405. **/
  406. if( response.getStatus() == 200 )
  407. {
  408. def locations = response.getHeaders( "Location" )
  409. def location = locations[0].getValue()
  410.  
  411. log.debug "redirecting to ${location}"
  412.  
  413. handle_request( location, args, type, success )
  414. }
  415. else
  416. {
  417. def retrievedStatus = response.getStatus()
  418. log.debug "got ${retrievedStatus} when we wanted 302"
  419. success.call( response )
  420. }
  421. }
  422.  
  423. try
  424. {
  425. if( type == 'get' )
  426. {
  427. httpGet( params, post_request )
  428. }
  429. }
  430. catch( Throwable e )
  431. {
  432. login()
  433. }
  434. }
  435.  
  436. def login(method = null, args = [], success = {})
  437. {
  438. def params =
  439. [
  440. uri: NEST_LOGIN_URL(),
  441. body: [ username: settings.username, password: settings.password ]
  442. ]
  443.  
  444. httpPost( params ) { response ->
  445.  
  446. data.auth = response.data
  447. data.auth.expires_in = Date.parse('EEE, dd-MMM-yyyy HH:mm:ss z', response.data.expires_in).getTime()
  448. log.debug data.auth
  449.  
  450. api_exec( method, args, success )
  451. }
  452. }
  453.  
  454. def logged_in()
  455. {
  456. if( !data.auth )
  457. {
  458. log.debug "data.auth is missing, not logged in"
  459. return false
  460. }
  461.  
  462. def now = new Date().getTime();
  463.  
  464. return( data.auth.expires_in > now )
  465. }
  466.  
  467. private def textVersion()
  468. {
  469. def text = "Version 1.6"
  470. }
  471.  
  472. private def textCopyright()
  473. {
  474. def text = "Copyright © 2016 Chad Monroe <chad@monroe.io>"
  475. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement