Posted to tcl by Napier_ at Thu Jul 03 02:16:19 GMT 2014view raw

  1. # --------------------------------------------
  2. # Nest & Smoke+CO TCL Integration Library
  3. # Connects to the Nest API for control &
  4. # monitoring of NEST Products.
  5. #
  6. # Authors: BN & RR
  7. # --------------------------------------------
  8. # Main Command Syntax:
  9. # -fanTimer <start,stop> -mode <off, heat, cool, heat-cool> -
  10. #
  11. set thermNum 0
  12.  
  13. #DICTS
  14. set devices_status [dict create]
  15.  
  16. proc LOG {data} {
  17. puts $data
  18. }
  19.  
  20. #LOG functions
  21. proc enableLOG { status } {
  22. global LOGENABLE
  23. set LOGENABLE $status
  24. }
  25.  
  26. enableLOG 0
  27. set varIfMRXMAC "000"
  28.  
  29. # Include Packages in the includePackages List
  30. set includePackages "http tls json json::write tdom uri"
  31. foreach {pkg} $includePackages {package require $pkg}
  32.  
  33. tls::init -tls1 true
  34. http::register https 443 tls::socket
  35.  
  36. namespace eval API {
  37. # Be Sure to Set your Client Secret & Client ID
  38. variable FB_Auth "auth=c.Q2z4m8hGR9dPA2lFvrXWNiJzlGLhsIIY3CKMRdZtWYOBPm25CLcWu3Nd8608scr63m8FeoOJXgLFuIyEUxm2Anj0IPNH8HjUa6aR6iKRIseNy1NUc212hpIik1JxBt4TYz5CRlkAje4Gohln"
  39. variable authState $::varIfMRXMAC
  40. variable baseURL "https://developer-api.nest.com/"
  41. variable redirectURL ""
  42. variable basePort 443
  43. variable client_secret "wCJzrq2VuyWcH70DjENWpjupi"
  44. variable client_id "7c3fe371-5740-48ac-a1b2-74df90145813"
  45. variable ThermBaseURL "devices/thermostats/"
  46. variable authRequestURL "https://home.nest.com/login/oauth2?client_id=7c3fe371-5740-48ac-a1b2-74df90145813&state=ip"
  47. variable authorizeURL "https://api.home.nest.com/oauth2/access_token?code=|AUTHPIN|&client_id=${client_id}&client_secret=${client_secret}&grant_type=authorization_code"
  48. #variable forecast "https://home.nest.com/api/0.1/weather/forecast/10528,USA"
  49.  
  50. namespace eval CMDS {
  51. variable FanTimer "fan_timer_active" ; # Boolean - Sets 15 Minute Fan Timer {"fan_timer_active": true }
  52. # JSON Example: { "fan_timer_active": true }
  53. variable Temp_F "target_temperature_f" ; # Integer - Sets Fahrenheit {"target_temperature_f": 70} (1 Accuracy)
  54. # JSON Example: { "target_temperature_f": 70 }
  55. variable Temp_C "target_temperature_c" ; # Integer - Sets Celcius (0.5 Accuracy)
  56. # JSON Example: { "target_temperature_c": 30.5 }
  57. variable HighTemp_F "target_temperature_high_f" ; # Integer - Set High Temp in Fahrenheit (1 Accuracy)
  58. # JSON Example: { "target_temperature_high_f": 75 }
  59. variable HighTemp_C "target_temperature_high_c" ; # Integer - Set High Temp in Celcius (0.5 Accuracy)
  60. # JSON Example: { "target_temperature_high_c": 40.5 }
  61. variable LowTemp_F "target_temperature_low_f" ; # Integer - Set Low Temp in Fahrenheit (1 Accuracy)
  62. # JSON Example: { "target_temperature_low_f": 65 }
  63. variable LowTemp_C "target_temperature_low_c" ; # Integer - Set Low Temp in Celcius (0.5 Accuracy)
  64. # JSON Example: { "target_temperature_low_c": 20.5 }
  65. variable mode "hvac_mode" ; # String - Set HVAC Mode <heat, cool, heat-cool, off>
  66. # JSON Example: {"hvac_mode": "heat-cool"}
  67.  
  68. variable Type 0
  69.  
  70. trace add variable ::API::CMDS::FanTimer read {::System::traceCallback i}
  71. trace add variable ::API::CMDS::Temp_F read {::System::traceCallback i}
  72. trace add variable ::API::CMDS::Temp_C read {::System::traceCallback i}
  73. trace add variable ::API::CMDS::HighTemp_F read {::System::traceCallback i}
  74. trace add variable ::API::CMDS::HighTemp_C read {::System::traceCallback i}
  75. trace add variable ::API::CMDS::LowTemp_F read {::System::traceCallback i}
  76. trace add variable ::API::CMDS::LowTemp_C read {::System::traceCallback i}
  77. trace add variable ::API::CMDS::mode read {::System::traceCallback s}
  78. }
  79. }
  80.  
  81. namespace eval Web {
  82.  
  83. variable RequestData [dict create]
  84.  
  85. proc Send {args} {
  86. # Syntax:
  87. # -method $METHOD -url $URL -params $PARAMS -suffix $URL-Suffix -port $port -body $body -request $request
  88. # -request feeds the RequestData Variable to Identify to the Callback
  89. # Web::Send -method PATCH -url https://www.url.com -params "Hi Hello" -suffix "devices/hello" -body $json -port 9553 would PATCH:
  90. # https://www.url.com:9553/devices/hello?auth={AUTH CODE}&hi=hello with the value of $json as the body
  91. LOG "Beginning Web::Send $args"
  92. set tempDict $args
  93. if {$::API::FB_Auth == 0} {LOG "OAuth Required Before Continuing"; #TODO: Build Re-Auth Service}
  94. if {![dict exists $tempDict -suffix]} {LOG "No Suffix Detected"; dict set tempDict -suffix ""}
  95. if {![dict exists $tempDict -url]} {
  96. LOG "No URL using Default";
  97. #if {$::API::redirectURL != ""} {
  98. #dict set tempDict -url $::API::redirectURL
  99. #} else {
  100. dict set tempDict -url $::API::baseURL
  101. #}
  102. }
  103. if {![dict exists $tempDict -method]} {LOG "No Method Defined, Using GET"; dict set tempDict -method "GET"}
  104. if {[dict exists $tempDict -params]} {LOG "Formatting Query"; dict set tempDict -params "&[::http::formatQuery {*}[dict get $tempDict -params]]"
  105. } else {LOG "No Params Detected"; dict set tempDict -params ""}
  106. if {[dict exists $tempDict -port]} {LOG "Custom Port Detected: [dict get $tempDict -port]"; http::register https [dict get $tempDict -port] tls::socket
  107. } else {http::register https $::API::basePort tls::socket}
  108. if {![dict exists $tempDict -body]} {LOG "No Body Detected"; dict set tempDict -body ""}
  109. if {![dict exists $tempDict -request]} {dict set tempDict -request "None"}
  110. set ::Web::RequestData $tempDict
  111. http::config -accept text/event-stream
  112. if {[dict get $tempDict -method] == "GET"} {set token [http::geturl [dict get $tempDict -url]/[dict get $tempDict -suffix]?${::API::FB_Auth}[dict get $tempDict -params] -headers {Accept text/event-stream} -keepalive 1 -progress ::Web::httpProgress -command ::Web::httpCallback]
  113. } else {set token [http::geturl [dict get $tempDict -url]/[dict get $tempDict -suffix]?${::API::FB_Auth}[dict get $tempDict -params] \
  114. -method [dict get $tempDict -method] -query [dict get $tempDict -body] -command ::Web::httpCallback]
  115. }
  116. }
  117.  
  118. proc httpCallback {token} {
  119. set data $::Web::RequestData
  120. set response [dict create data [http::data $token] code [http::code $token]]
  121. LOG "----- RECEIVING DATA -----"
  122. LOG "$response"
  123. LOG "--------------------------"
  124.  
  125. #set ::device_status []
  126. if {[string match {30[1237]} [::http::ncode $token]]} {LOG "Redirect Required!"; ::Web::startRedirect $token; return
  127. } elseif {[::http::ncode $token] == 200} {LOG "Successfull Call Detected"
  128. switch -- [dict get $data -request] {
  129. Query {LOG "DEV STATUS"; ::Nest::setStatus [::json::json2dict [dict get $response data]]}
  130. Authorize {LOG "AUTHORIZE"; set response [::json::json2dict [dict get $response data]]}
  131. default {LOG "Unknown or Undefined Request, Saving Value to ::Nest::StatusUndefined"; set ::Nest::StatusUndefined $data}
  132. }
  133. } elseif {[::http::ncode $token] == 429} {LOG "Too Many Requests Error Received"}
  134. ::http::cleanup $token
  135. }
  136.  
  137. proc startRedirect {token} {
  138. set data $::Web::RequestData
  139. array set meta [set ${token}(meta)]
  140. LOG "[parray {*}$token]"
  141. http::cleanup $token
  142. if {![info exist meta(Location)]} {LOG "No Redirection Indicator?"; return}
  143. array set uri [::uri::split $meta(Location)]
  144. LOG "URI:"; LOG "[parray uri]"
  145. unset meta
  146. if {$uri(host) == ""} {set uri(host) $uri(host)}
  147. set url [eval ::uri::join [array get uri]]
  148. set ::API::redirectURL "$uri(scheme)://$uri(host):$uri(port)"
  149. LOG "URL: $url"
  150. http::config -accept text/event-stream
  151. if {[dict get $data -method] == "GET"} {set token [http::geturl $url -keepalive 1 -headers {Accept text/stream} -progress ::Web::httpProgress -command ::Web::httpCallback]
  152. } else {set token [http::geturl $url -method [dict get $data -method] -query [dict get $data -body] -command ::Web::httpCallback]}
  153. }
  154.  
  155. proc httpProgress {token total current} {
  156. upvar #0 $token state
  157. set body "$state(body)"
  158. puts $body
  159. }
  160.  
  161. proc sendEmail {to msg args} {
  162. # This Procedure will Prepare and Send an E-Mail to the address(es) provided in
  163. # the "to" argument. It will include the content in the "msg" argument.
  164. # Additionally the following arguments can be defined if needed:
  165. # -footer $txt
  166.  
  167. }
  168.  
  169. }
  170.  
  171. namespace eval Nest {
  172. variable Status ""
  173. variable StatusUndefined 0
  174. variable thermostats [dict create]
  175. variable devices [dict create thermostats "" protects ""]
  176.  
  177. proc Authorize {authPIN} {
  178. # Sends PIN to Nest to retrieve authentication string
  179. set authURL [string map "|AUTHPIN| $authPIN" $::API::authorizeURL]
  180. LOG "Authorization URL:\n $authURL"
  181. Web::Send -method POST -url $authURL -request Authorize
  182. #TODO: Send the Authorization as POST with Parameters
  183. }
  184.  
  185. proc Query {args} {
  186. # Queries The Nest API Based on Your Parameters
  187. # If blank (::Nest::Query) it will query all parameters into the main dictionary
  188. # -thermostat <therm ID or name> -smoke <smokeCO ID or name>
  189.  
  190. Web::Send -request Query
  191. #after 10000 {Nest::Query}
  192. }
  193.  
  194. proc Set {args} {
  195. # This Procedure allows you to Control Nest Products using
  196. # their Official API.
  197. # -command - Specify the command as a key/value pair. Comaand options and
  198. # examples can be found in the ::API::CMDS Namespace and should be referenced
  199. # as that variable.
  200. # -value - Specify the Value you would like to feed to the Process. This should follow
  201. # the examples in the "::API::CMDS" namespace.
  202. # -device {deviceID} - This argument specifies the device's ID which
  203. # should be manipulated
  204. # -kind {kind} - Specify what kind of device will be controlled.
  205. # this is an optional value but specifying it will increase overall speed.
  206. # Syntax Example:
  207. # ::Nest::Set $::API::CMDS::Temp(F) 75 -device $thermID -kind thermostat
  208.  
  209. LOG "Beginning Set Procedure"
  210. if {[dict exists $args -kind]} {
  211. LOG "Kind was Specified: [dict get $args -kind]"
  212. switch -regexp -- [dict get $args -kind] {
  213. {^thermostats?$|^therms?$} {::Nest::setThermostat [dict get $args -command] [dict get $args -value] [dict get $args -device]}
  214. {^protects?$|^smokecos?$|^detectors?$} {LOG "Nest Protect Specified\nThere are currently No Official Set Options for Nest Protect"}
  215. default {puts "No"}
  216. }
  217. } else {
  218. LOG "Kind was Not Specified - Starting ID Trace"
  219. }
  220. }
  221.  
  222. proc setThermostat {cmd value id} {
  223. LOG "Setting the Thermostat with ID: $id"
  224. # This Procedure should generally not be called directly, use ::Nest::Set $cmd $value -kind (optional)
  225. # Currently Adds JSON and sends one Command at a Time - another procedure will be written to send multiple
  226. # values with a single call.
  227.  
  228. if {$::API::CMDS::Type == "i"} { set json [::json::write object $cmd $value]
  229. } elseif {$::API::CMDS::Type == "s"} { set json [::json::write object $cmd [::json::write string $value]]
  230. }
  231. LOG "|--- Sending in Patch Body:\n$json"
  232. LOG "|--- Sending to Suffix:\n${::API::baseURL}${::API::ThermBaseURL}$id"
  233. ::Web::Send -method PATCH -suffix ${::API::ThermBaseURL}$id -body $json
  234. }
  235.  
  236. proc Experimental {args} {
  237. # This procedure will house non-official API procedures that
  238. # may not be supported in the long-term. Generally this will
  239. # emulate features used on the Official Nest App & Web Control
  240. # app.
  241.  
  242. }
  243.  
  244. proc setStatus {newStatus args} {
  245. LOG "PRINT #1"
  246. # This Procedure handles Status Setting when a Query is Made. If Status was already present, it will save the
  247. # "current" Status into the "old" Status Dictionary and the newly received status into the "current" dictionary.
  248. # This can then be used to evaluate events as needed for Macro Integration
  249.  
  250. LOG "---- New Nest Status Received!\n$newStatus"
  251. if {$::Nest::Status != "" && [dict exists $::Nest::Status current]} {dict set ::Nest::Status old [dict get $::Nest::Status current]}
  252. dict set ::Nest::Status current $newStatus
  253. getDevices thermostats
  254. getDevices protects
  255. getHomes
  256. }
  257.  
  258. proc getHomes {} {
  259. LOG "PRINT #4"
  260. set tpDict [dict create]
  261. if {$::Nest::Status == ""} {LOG "Query Never Performed? Attempting Query..."; Nest::Query}
  262.  
  263. if {[dict exists $::Nest::Status current structures]} {
  264. set thermHomes [dict keys [dict get $::Nest::Status current structures]]
  265. LOG "_____THERMOSTATS HOMES: $thermHomes"
  266.  
  267. foreach id $thermHomes {
  268. set name [dict get $::Nest::Status current structures $id name]
  269. dict set tpDict $name $id
  270. dict set ::Nest::Homes thermostats $tpDict
  271. LOG "------ HOMES ___: $::Nest::Homes"
  272. }
  273. } else {LOG "No Homes Detected - Attempting Query"}
  274. }
  275.  
  276. proc getDevices {kind} {
  277. # Parses the Data from the Queries and returns "kind" to the proper
  278. # Dictionary with name/id as the key/value pairs
  279. # Options for Kind are: thermostats, smoke
  280.  
  281. LOG "PRINT #2"
  282. set tempDict [dict create]
  283. if {$::Nest::Status == ""} {LOG "Query Never Performed? Attempting Query..."; Nest::Query}
  284. switch -regexp -nocase -- $kind {
  285. {^thermostats?$} {
  286. LOG "PRINT #3"
  287. if {[dict exists $::Nest::Status current devices thermostats]} {
  288. set thermIDs [dict keys [dict get $::Nest::Status current devices thermostats]]
  289. LOG "_____THERMOSTATS LIST: $thermIDs"
  290. } else {LOG "No Thermostats Detected - Attempting Query"}
  291. foreach id $thermIDs {
  292. set name [dict get $::Nest::Status current devices thermostats $id name]
  293. dict set tempDict $name $id
  294. dict set ::Nest::Devices thermostats $tempDict
  295. incr ::thermNum
  296. }
  297. }
  298. {^protects?$} {
  299. if {[dict exists $::Nest::Status current devices smoke_co_alarms]} {set protectIDs [dict keys [dict get $::Nest::Status current devices smoke_co_alarms]]
  300. } else {LOG "No Protects Were Detected"}
  301. foreach id $protectIDs {
  302. set name [dict get $::Nest::Status current devices smoke_co_alarms $id name]
  303. dict set tempDict $name $id
  304. dict set ::Nest::Devices protects $tempDict
  305. }
  306. }
  307. {^structure?$} {}
  308. {^all} {}
  309. default {LOG "Kind was not Recognized - should be thermostats, protects, or all"}
  310. }
  311. }
  312.  
  313. #proc nameFromID {id {kind "thermostats"}} {
  314. ## Used to Return the "Room Name" of a given "Device ID"
  315. #dict for {k v} [dict filter [dict get $::Nest::Devices $kind] value $id] {
  316. #lappend keys $k
  317. #}
  318. #return $keys
  319. #}
  320.  
  321. proc listRooms {} {
  322. # This procedure will return the rooms within the project which includes both
  323. # the Thermostats & the Nest Protects. Returns into List which should be useable
  324. # for URC's TCL List Objects. Automatically handles duplicate values and only returns
  325. # unique values.
  326.  
  327. set l [dict keys [dict get $::Nest::Devices thermostats]]
  328. lappend l {*}[dict keys [dict get $::Nest::Devices protects]]
  329. LOG "Raw List is:\n$l"
  330. set l [lsort -unique $l]
  331. LOG "Formatted List is:\n$l"
  332. return $l
  333. }
  334.  
  335. proc findDeviceKind {id} {
  336. # INCOMPLETE!
  337. # This Procedure will return the "Kind" of Device based on a specified ID
  338. # Example: ::Nest::findDeviceKind _MXPPHwK669HMC6Mtqb51qtN3uLTVu2B
  339. # Returns: < thermostat , protect >
  340.  
  341. }
  342. }
  343.  
  344. namespace eval URC {
  345. proc interfaceServer {} {
  346. # Interfaces with URC Interfaces
  347. }
  348. LOG "-- URC Module Information --"
  349. #LOG "Module Version: $::varIfVersionNum"
  350. LOG "Created for: Nest API, Firmware v4.0"
  351.  
  352. }
  353.  
  354. namespace eval System {
  355. proc traceCallback {value args} {
  356. switch -- $value {
  357. i {set ::API::CMDS::Type i}
  358. s {set ::API::CMDS::Type s}
  359. }
  360. }
  361. }
  362.  
  363. proc setTemp {type num} {
  364. #DoNothing
  365. }
  366.  
  367. proc displayListType {num} {
  368. #DoNothing
  369. }
  370.  
  371. #set thermID "_aLf8zP-ItVL9f8vs_Ej6DSC7Dulq49h"
  372. #Nest::Query;after 3000 {set i 0};vwait i
  373. Nest::Query
  374. #Nest::Set -command $::API::CMDS::Temp_F -value 85 -device $thermID -kind thermostat;after 3000 {set i 0};vwait i
  375. vwait forever
  376.