Posted to tcl by Napier at Fri Jul 11 15:46:47 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. # Author: Braden Napier & Remi Rosa
  7. # --------------------------------------------
  8. # Main Command Syntax:
  9. # -fanTimer <start,stop> -mode <off, heat, cool, heat-cool> -
  10. #
  11.  
  12. enableLOG 0
  13. set varIfMRXMAC "000"
  14.  
  15. proc enableLOG { status } {
  16. global LOGENABLE
  17. set LOGENABLE $status
  18. }
  19.  
  20. proc LOG {data} {
  21. puts $data
  22. }
  23.  
  24. # Include Packages in the includePackages List
  25. set includePackages "http tls json json::write tdom uri struct::list"
  26. foreach {pkg} $includePackages {package require $pkg}
  27.  
  28. tls::init -tls1 true
  29. http::register https 443 tls::socket
  30.  
  31. namespace eval API {
  32. # Be Sure to Set your Client Secret & Client ID
  33. # Automation Apps
  34. variable FB_Auth "auth=c.BIm6hEjxafQbnF6CUXuxkmIvX7ztXndPiNbwZAhlYVpla6jZCjAquph6OhwA2gtel1uSeHGtvcMAf5i4reaOxEha4S7bKyBuIiXnSHrVWjvbuCcr1mZ8oEu3cfzj5R9Zp0lrT753ywiyzc42"
  35. variable authState $::varIfMRXMAC
  36. variable baseURL "https://developer-api.nest.com"
  37. variable basePort 443
  38. variable client_secret "wCJzrq2VuyWcH70DjENWpjupi"
  39. variable client_id "7c3fe371-5740-48ac-a1b2-74df90145813"
  40. variable ThermBaseURL "devices/thermostats/"
  41. variable authRequestURL "https://home.nest.com/login/oauth2?client_id=7c3fe371-5740-48ac-a1b2-74df90145813&state=ip"
  42. variable authorizeURL "https://api.home.nest.com/oauth2/access_token?code=|AUTHPIN|&client_id=${client_id}&client_secret=${client_secret}&grant_type=authorization_code"
  43. variable maxNestEvents 10
  44. variable maxUIEvents 10
  45. # variable forecast "https://home.nest.com/api/0.1/weather/forecast/${zip},${country}"
  46.  
  47. namespace eval CMDS {
  48. variable FanTimer "fan_timer_active" ; # Boolean - Sets 15 Minute Fan Timer {"fan_timer_active": true }
  49. # JSON Example: { "fan_timer_active": true }
  50. variable Temp_f "target_temperature_f" ; # Integer - Sets Fahrenheit {"target_temperature_f": 70} (1 Accuracy)
  51. # JSON Example: { "target_temperature_f": 70 }
  52. variable Temp_c "target_temperature_c" ; # Integer - Sets Celcius (0.5 Accuracy)
  53. # JSON Example: { "target_temperature_c": 30.5 }
  54. variable HighTemp_f "target_temperature_high_f" ; # Integer - Set High Temp in Fahrenheit (1 Accuracy)
  55. # JSON Example: { "target_temperature_high_f": 75 }
  56. variable HighTemp_c "target_temperature_high_c" ; # Integer - Set High Temp in Celcius (0.5 Accuracy)
  57. # JSON Example: { "target_temperature_high_c": 40.5 }
  58. variable LowTemp_f "target_temperature_low_f" ; # Integer - Set Low Temp in Fahrenheit (1 Accuracy)
  59. # JSON Example: { "target_temperature_low_f": 65 }
  60. variable LowTemp_c "target_temperature_low_c" ; # Integer - Set Low Temp in Celcius (0.5 Accuracy)
  61. # JSON Example: { "target_temperature_low_c": 20.5 }
  62. variable mode "hvac_mode" ; # String - Set HVAC Mode <heat, cool, heat-cool, off>
  63. # JSON Example: {"hvac_mode": "heat-cool"}
  64.  
  65. variable Type 0
  66.  
  67. trace add variable ::API::CMDS::FanTimer read {::System::traceCallback i}
  68. trace add variable ::API::CMDS::Temp_f read {::System::traceCallback i}
  69. trace add variable ::API::CMDS::Temp_c read {::System::traceCallback i}
  70. trace add variable ::API::CMDS::HighTemp_f read {::System::traceCallback i}
  71. trace add variable ::API::CMDS::HighTemp_c read {::System::traceCallback i}
  72. trace add variable ::API::CMDS::LowTemp_f read {::System::traceCallback i}
  73. trace add variable ::API::CMDS::LowTemp_c read {::System::traceCallback i}
  74. trace add variable ::API::CMDS::mode read {::System::traceCallback s}
  75. }
  76. }
  77.  
  78. namespace eval Web {
  79.  
  80. variable RequestData [dict create]
  81. variable streamToken ""
  82.  
  83. proc Send {args} {
  84. # Syntax:
  85. # -method $METHOD -url $URL -params $PARAMS -suffix $URL-Suffix -port $port -body $body -request $request
  86. # -request feeds the RequestData Variable to Identify to the Callback
  87. # Web::Send -method PATCH -url https://www.url.com -params "Hi Hello" -suffix "devices/hello" -body $json -port 9553 would PATCH:
  88. # https://www.url.com:9553/devices/hello?auth={AUTH CODE}&hi=hello with the value of $json as the body
  89. LOG "Beginning Web::Send $args"
  90. http::cleanup $::Web::streamToken
  91. set tempDict $args
  92. if {$::API::FB_Auth == 0} {LOG "OAuth Required Before Continuing"; #TODO: Build Re-Auth Service}
  93. if {![dict exists $tempDict -suffix]} {LOG "No Suffix Detected"; dict set tempDict -suffix ""}
  94. if {![dict exists $tempDict -url]} {LOG "No URL using Default"; dict set tempDict -url $::API::baseURL}
  95. if {![dict exists $tempDict -method]} {LOG "No Method Defined, Using GET"; dict set tempDict -method "GET"}
  96. if {[dict exists $tempDict -params]} {LOG "Formatting Query"; dict set tempDict -params "&[::http::formatQuery {*}[dict get $tempDict -params]]"
  97. } else {LOG "No Params Detected"; dict set tempDict -params ""}
  98. if {[dict exists $tempDict -port]} {LOG "Custom Port Detected: [dict get $tempDict -port]"; http::register https [dict get $tempDict -port] tls::socket
  99. } else {http::register https $::API::basePort tls::socket}
  100. if {![dict exists $tempDict -body]} {LOG "No Body Detected"; dict set tempDict -body ""}
  101. if {![dict exists $tempDict -request]} {LOG "No Request Detected"; dict set tempDict -request "None"
  102. } elseif {[dict get $tempDict -request] == "Query"} {LOG "Set Event Stream Header"; http::config -accept "text/event-stream"
  103. } else {LOG "Set Accept All Header"; http::config -accept "/*"}
  104. set ::Web::RequestData $tempDict
  105. 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] -command ::Web::httpCallback]
  106. } else {set token [http::geturl [dict get $tempDict -url]/[dict get $tempDict -suffix]?${::API::FB_Auth}[dict get $tempDict -params] \
  107. -method [dict get $tempDict -method] -query [dict get $tempDict -body] -command ::Web::httpCallback]
  108. }
  109. }
  110.  
  111. proc httpCallback {token} {
  112. set data $::Web::RequestData
  113. set response [dict create data [http::data $token] code [http::code $token]]
  114. LOG "----- RECEIVING DATA -----"
  115. LOG "$response"
  116. LOG "--------------------------"
  117. LOG "Token: $token"
  118. if {[string match {30[1237]} [::http::ncode $token]]} {LOG "Redirect Required!"; ::Web::startRedirect $token
  119. } elseif {[::http::ncode $token] == 200} {LOG "Successfull Call Detected"
  120. ::Nest::Query
  121. #http::cleanup $token
  122. #switch -- [dict get $data -request] {
  123. # Authorize {LOG "Authorize Data Assumed"; set response [::json::json2dict [dict get $response data]]}
  124. # default {LOG "Unknown or Undefined Request, Saving Value to ::Nest::StatusUndefined"; set ::Nest::StatusUndefined $data}
  125. #}
  126. # Query {LOG "Query Data Assumed"; ::Nest::setStatus [::json::json2dict [dict get $response data]]}
  127. } elseif {[::http::ncode $token] == 429} {LOG "!!!! ---- |||| Too Many Requests Error Received |||| ----- !!!!"}
  128. }
  129. set secondSend 0
  130. set dataBuff ""
  131.  
  132.  
  133. proc httpProgress {token total current} {
  134. LOG "Start Progress"; LOG "Total: $total"; LOG "Current: $current"
  135. # The Following if Statement is used to parse out the random "e" received
  136. # at the start of the Event Stream Data.
  137. if {$current == 1} {LOG "END"; return}
  138. upvar #0 $token state
  139. if {$::Web::secondSend == 1} {
  140. LOG "!!!!!! Sending AGAIN"
  141. append ::Web::dataBuff [http::code $token]
  142. set body $::Web::dataBuff
  143. set ::Web::secondSend 0
  144. } else {
  145. LOG "No more Data?"
  146. set body "$state(body)"
  147. if {$state(body) == "e"} {return}
  148. }
  149. if {$current == 8193} {
  150. set ::Web::secondSend 1
  151. set ::Web::dataBuff $body
  152. Nest::Query
  153. return
  154. } else {
  155. LOG "Data has Finished Buffering"
  156. set occurrences [regsub -all {\s*\n\s*\n(\s*\n)*} $body "\n" body]
  157. set body [split $body \n]
  158. }
  159.  
  160. if {[dict exists [lindex $body 0] "event:"] && [dict get [lindex $body 0] "event:"] == "auth_revoked"} {::Nest::AuthRevoked}
  161. # The occurrences variable actively tracks how many events are currently saved
  162. # in the $state(body) variable. It is likely a good idea to flush this out at
  163. # a set interval by closing (http::cleanup) and re-initiating the event.
  164. LOG "Event Occurences: $occurrences"
  165. if {$occurrences >= $::API::maxNestEvents} {LOG "Flushing Events"
  166. # Process to Flush Events & Re-Initiate the Event Stream
  167. # should go here.
  168. set state(body) ""
  169. }
  170. if {$::Nest::Status == ""} {
  171. set startData [::System::convertJSON [lindex $body 1]]
  172. LOG "Start Data:\n$startData"
  173. ::Nest::setStatus [dict get $startData data]
  174. } else {
  175. set newData [::System::convertJSON [lindex $body end-1]]
  176. if {$newData != ""} {
  177. LOG "New Data:\n$newData"
  178. if {[dict exists $::Nest::Status current] && [dict get $::Nest::Status current] != [dict get $newData data]} {
  179. LOG "Data is Different, Setting Nest Status Variable"; ::Nest::setStatus [dict get $newData data]
  180. } else {LOG "New Data has Not Changed, Do Nothing"}
  181. }
  182. }
  183. }
  184.  
  185. proc startRedirect {token} {
  186. set data $::Web::RequestData
  187. array set meta [set ${token}(meta)]
  188. LOG "[parray {*}$token]"
  189. http::cleanup $token
  190. if {![info exist meta(Location)]} {LOG "No Redirection Indicator?"; return}
  191. array set uri [::uri::split $meta(Location)]
  192. LOG "URI:"; LOG "[parray uri]"
  193. unset meta
  194. if {$uri(host) == ""} {set uri(host) $uri(host)}
  195. set url [eval ::uri::join [array get uri]]
  196. LOG "URL: $url"
  197. if {[dict get $data -method] == "GET"} {http::config -accept "text/event-stream"; set ::Web::streamToken [http::geturl $url -keepalive 1 -progress ::Web::httpProgress]
  198. } else {set token [http::geturl $url -method [dict get $data -method] -query [dict get $data -body] -command ::Web::httpCallback]}
  199. }
  200.  
  201. proc sendEmail {to msg args} {
  202. # This Procedure will Prepare and Send an E-Mail to the address(es) provided in
  203. # the "to" argument. It will include the content in the "msg" argument.
  204. # Additionally the following arguments can be defined if needed:
  205. # -footer $txt
  206.  
  207. }
  208. }
  209.  
  210. namespace eval Nest {
  211. variable Status ""
  212. variable StatusUndefined 0
  213. variable thermostats [dict create]
  214. variable devices [dict create thermostats "" protects "" structures ""]
  215. variable presence [dict create]
  216.  
  217.  
  218. proc Authorize {authPIN} {
  219. # Sends PIN to Nest to retrieve authentication string
  220. set authURL [string map "|AUTHPIN| $authPIN" $::API::authorizeURL]
  221. LOG "Authorization URL:\n $authURL"
  222. Web::Send -method POST -url $authURL -request Authorize
  223. #TODO: Send the Authorization as POST with Parameters
  224. }
  225.  
  226. proc AuthRevoked {args} {
  227. # This Procedure is called when the "Auth_Revoked" Event
  228. # is Received from Nest. You will need to re-authenticate
  229. # and get a new "Auth Token" when this occurs.
  230. }
  231.  
  232. proc Query {args} {
  233. # Queries The Nest API Based on Your Parameters
  234. # If blank (::Nest::Query) it will query all parameters into the main dictionary
  235. # Do NOT do this Often as Nest limits the amount of calls you can do per
  236. # month per token.
  237. # All data will be kept up-to-date using the Rest Streaming Feature of the API.
  238. # -thermostat <therm ID or name> -smoke <smokeCO ID or name>
  239. Web::Send -request "Query"
  240. }
  241.  
  242. proc Set {args} {
  243. # This Procedure allows you to Control Nest Products using
  244. # their Official API.
  245. # -command - Specify the command. Command options and examples can be found in the
  246. # ::API::CMDS Namespace and should be referenced as that variable.
  247. # -value - Specify the Value you would like to feed to the Process. This should follow
  248. # the examples in the "::API::CMDS" namespace.
  249. # -device {deviceID} - This argument specifies the device's ID which
  250. # should be manipulated. It can also use the device name directly.
  251. # -kind {kind} - Specify what kind of device will be controlled.
  252. # this is an optional value but specifying it will increase overall speed.
  253. # Syntax Example:
  254. # ::Nest::Set -command $::API::CMDS::Temp_f -value 70 -device Kitchen -kind thermostat
  255. LOG "Beginning Set Procedure"
  256.  
  257. if {[dict exists $args -device]} {
  258. set device [dict get $args -device]
  259. LOG "Device is: $device"
  260. if {[dict exists $::Nest::devices thermostats $device]} {
  261. set device [dict get $::Nest::devices thermostats $device]
  262. LOG "Device is now: $device"
  263. }
  264. }
  265. if {[dict exists $args -kind]} {
  266. LOG "Kind was Specified: [dict get $args -kind]"
  267. switch -regexp -- [dict get $args -kind] {
  268. {^thermostats?$|^therms?$} {::Nest::setThermostat [dict get $args -command] [dict get $args -value] $device}
  269. {^protects?$|^smokecos?$|^detectors?$} {LOG "Nest Protect Specified\nThere are currently No Official Set Options for Nest Protect"}
  270. default {LOG "Error: Set Params Incorrect"}
  271. }
  272. } else {
  273. LOG "Kind was Not Specified"
  274. }
  275. }
  276.  
  277. proc setThermostat {cmd value id} {
  278. LOG "Setting the Thermostat with ID: $id"
  279. # This Procedure should generally not be called directly, use ::Nest::Set $cmd $value -kind (optional)
  280. # Currently Adds JSON and sends one Command at a Time - another procedure will be written to send multiple
  281. # values with a single call.
  282.  
  283. if {$::API::CMDS::Type == "i"} { set json [::json::write object $cmd $value]
  284. } elseif {$::API::CMDS::Type == "s"} { set json [::json::write object $cmd [::json::write string $value]]
  285. }
  286. LOG "|--- Sending in Patch Body:\n$json"
  287. LOG "|--- Sending to Suffix:\n${::API::baseURL}/${::API::ThermBaseURL}$id"
  288. ::Web::Send -method PATCH -suffix ${::API::ThermBaseURL}$id -body $json
  289. }
  290.  
  291. proc Experimental {args} {
  292. # This procedure will house non-official API procedures that
  293. # may not be supported in the long-term. Generally this will
  294. # emulate features used on the Official Nest App & Web Control
  295. # app.
  296.  
  297. }
  298.  
  299. proc setStatus {newStatus args} {
  300. # This Procedure handles Status Setting when a Query is Made. If Status was already present, it will save the
  301. # "current" Status into the "old" Status Dictionary and the newly received status into the "current" dictionary.
  302. # This can then be used to evaluate events as needed for Macro Integration
  303.  
  304. LOG "---- New Nest Status Received!\n$newStatus"
  305. if {$::Nest::Status != "" && [dict exists $::Nest::Status current]} {
  306. dict set ::Nest::Status old [dict get $::Nest::Status current]
  307. }
  308. dict set ::Nest::Status current $newStatus
  309. getDevices all
  310. }
  311.  
  312. proc getDevices {kind} {
  313. # Parses the Data from the Queries and returns "kind" to the proper
  314. # Dictionary with name/id as the key/value pairs
  315. # Options for Kind are: thermostats, smoke
  316. set tempDict [dict create]
  317. if {$::Nest::Status == ""} {LOG "Query Never Performed? Attempting Query..."; Nest::Query}
  318. switch -regexp -nocase -- $kind {
  319. {^thermostats?$} {
  320. LOG "Getting Thermostats"
  321. if {[dict exists $::Nest::Status current devices thermostats]} {set thermIDs [dict keys [dict get $::Nest::Status current devices thermostats]]
  322. } else {LOG "No Thermostats Detected"; return -1}
  323. LOG "Therm IDs: $thermIDs"
  324. foreach id $thermIDs {
  325. set name [dict get $::Nest::Status current devices thermostats $id name]
  326. dict set tempDict $name $id
  327. }
  328. if {[dict exists $::Nest::devices thermostats] && [dict get $::Nest::devices thermostats] == $tempDict} {LOG "No Change to Thermostats, Do Nothing"; return}
  329. LOG "Change Detected to Thermostats, Setting Devices Dictionary"
  330. dict set ::Nest::devices thermostats $tempDict
  331. return
  332. }
  333. {^protects?$} {
  334. LOG "Getting Protects"
  335. if {[dict exists $::Nest::Status current devices smoke_co_alarms]} {set protectIDs [dict keys [dict get $::Nest::Status current devices smoke_co_alarms]]
  336. } else {LOG "No Protects Were Detected"; return -1}
  337. LOG "Protect IDs: $protectIDs"
  338. foreach id $protectIDs {
  339. set name [dict get $::Nest::Status current devices smoke_co_alarms $id name]
  340. dict set tempDict $name $id
  341. }
  342. if {[dict exists $::Nest::devices protects] && [dict get $::Nest::devices protects] == $tempDict} {LOG "No Change to Protects, Do Nothing"; return}
  343. LOG "Change Detected to Protects, Setting Devices Dictionary"
  344. dict set ::Nest::devices protects $tempDict
  345. return
  346. }
  347. {^structures?$|^homes?$|^buildings?$} {
  348. LOG "Getting Structures"
  349. if {[dict exists $::Nest::Status current structures]} {set structIDs [dict keys [dict get $::Nest::Status current structures]]
  350. } else {LOG "No Structured Were Detected"; return -1}
  351. foreach id $structIDs {
  352. set name [dict get $::Nest::Status current structures $id name]
  353. if {[dict exists $::Nest::Status current structures $id thermostats]} {set therms [dict get $::Nest::Status current structures $id thermostats]
  354. } else {LOG "No Thermostats for Structure $name"; set therms ""}
  355. if {[dict exists $::Nest::Status current structures $id smoke_co_alarms]} {set protects [dict get $::Nest::Status current structures $id smoke_co_alarms]
  356. } else {LOG "No Protects for Structure: $name"; set protects ""}
  357. # Check Presence Status for Structures and Report to the System if it has changed
  358. dict set tempDict $name [dict create id $id thermostats $therms protects $protects]
  359. set presence [dict get $::Nest::Status current structures $id away]
  360. if {![dict exists $::Nest::presence $id]} {LOG "Creating Presence for First Time"; dict set ::Nest::presence $id [dict create status $presence startup 1]
  361. } elseif {[dict exists $::Nest::presence $id] && [dict get $::Nest::presence $id status] != $presence} {LOG "Presence Changed"; dict with ::Nest::presence $id {set status $presence; set startup 0}}
  362. }
  363. if {[dict exists $::Nest::devices structures] && [dict get $::Nest::devices structures] == $tempDict} {LOG "No Change to Structures, Do Nothing"; return}
  364. LOG "Change Detected to Structures, Setting Devices Dictionary"
  365. dict set ::Nest::devices structures $tempDict
  366. return
  367. }
  368. {^all} {LOG "Getting All Devices & Structures"
  369. getDevices thermostats; getDevices protects; getDevices structures
  370. }
  371. default {LOG "Kind was not Recognized - should be thermostats, protects, or all"}
  372. }
  373. }
  374.  
  375. proc nameFromID {id {kind "thermostats"}} {
  376. # Used to Return the "Room Name" or "Structure Name" of a given "ID"
  377.  
  378. if {$kind != "structures"} {
  379. LOG "Parsing ID"
  380. dict for {k v} [dict filter [dict get $::Nest::devices $kind] value $id] {
  381. set keys $k
  382. }
  383. } else {
  384. LOG "Getting Structure Name"
  385. set structNames [dict keys [dict get $::Nest::devices structures]]
  386. foreach struct $structNames {
  387. if {$id == [dict get $::Nest::devices structures $struct id]} {
  388. set keys $struct
  389. } else {continue}
  390. }
  391. }
  392. LOG "Returning: $keys"
  393. return $keys
  394. }
  395.  
  396. proc deviceNamesByStructure {struct {kind "thermostats"} args} {
  397. # Will return a list with the "kind" names for a given Structure.
  398. # Structure can be the Structure ID or Structure Name.
  399. # If -unique 1 is specified as an argument, it will erase any duplications
  400. # that would occur should a Thermostat and Detector share the same name
  401. set names ""
  402. if {![dict exists $::Nest::devices structures $struct $kind]} {
  403. LOG "Getting Name from Structure ID"; set struct [::Nest::nameFromID $struct structures]
  404. }
  405. foreach deviceID [dict get $::Nest::devices structures $struct $kind] {lappend names [::Nest::nameFromID $deviceID $kind]}
  406. LOG "$kind in Room: $names"
  407. if {[dict exists $args -unique] && [dict get $args -unique] == 1} {set names [lsort -unique $names]}
  408. return $names
  409. }
  410.  
  411. proc listRooms {{struct "all"}} {
  412. # This procedure will return the rooms within the project which includes both
  413. # the Thermostats & the Nest Protects. Returns into List which should be useable
  414. # for URC's TCL List Objects. Automatically handles duplicate values and only returns
  415. # unique values.
  416. # By default listRooms will list all rooms in the System regardless of what Nest
  417. # structure they are within. Providing a Structure Name or ID as an argument will
  418. # list rooms only for that structure.
  419. # This differentiates itself from ::Nest::DeviceNamesByStructure because it returns
  420. # a unique list of rooms. It also automatically checks all room names that any
  421. # device is placed into.
  422. # Syntax:
  423. # ::Nest::listRooms "Home"
  424.  
  425. set l ""
  426. if {$struct == "all"} {
  427. set l [dict keys [dict get $::Nest::devices thermostats]]
  428. lappend l {*}[dict keys [dict get $::Nest::devices protects]]
  429. } else {
  430. if {![dict exists $::Nest::devices structures $struct]} {
  431. LOG "Getting Name from Structure ID"; set struct [::Nest::nameFromID $struct structures]
  432. }
  433. foreach deviceID [dict get $::Nest::devices structures $struct thermostats] {lappend l [::Nest::nameFromID $deviceID thermostats]}
  434. foreach deviceID [dict get $::Nest::devices structures $struct protects] {lappend l [::Nest::nameFromID $deviceID protects]}
  435. }
  436. LOG "Raw List is:\n$l"
  437. set l [lsort -unique $l]
  438. LOG "Formatted List is:\n$l"
  439. return $l
  440. }
  441.  
  442. proc findDeviceKind {id} {
  443. # This Procedure will return the "Kind" of Device based on a specified ID
  444. # Example: ::Nest::findDeviceKind _MXPPHwK669HMC6Mtqb51qtN3uLTVu2B
  445. # Returns: < thermostats , protects, structures >
  446. # You can then call ::Nest::nameFromID $response to get the name/room/structure.
  447. # set name [::Nest::nameFromID $id [::Nest::findDeviceKind $id]]
  448.  
  449. set l [dict values [dict get $::Nest::devices thermostats]]
  450. if {[string match *$id* $l]} {return thermostats}
  451. set l [dict values [dict get $::Nest::devices protects]]
  452. if {[string match *$id* $l]} {return protects}
  453. set l ""
  454. set structNames [dict keys [dict get $::Nest::devices structures]]
  455. foreach struct $structNames {
  456. lappend l {*}[dict get $::Nest::devices structures $struct id]
  457. }
  458. if {[string match *$id* $l]} {return structures}
  459. }
  460.  
  461. # Trace the Devices Variable to trigger changes to UI when new devices are added or changed
  462. # in any way.
  463. trace add variable ::Nest::devices write {::System::traceCallback devices}
  464. trace add variable ::Nest::Status write {::System::traceCallback status}
  465. trace add variable ::Nest::presence write {::System::traceCallback presence}
  466. }
  467.  
  468. namespace eval URC {
  469.  
  470. variable UpdateListing ""
  471.  
  472. proc UpdateUI {eventType deviceID newValue} {
  473. # This Procedure is called when a setting needs to be
  474. # updated to URC User Interfaces.
  475. # The previous 10 changes are provided for the module
  476. # script to parse using a "foreach" function or similar.
  477. # Newest Events will be first in the listing
  478. # Note: The Temp Type is fed based on the thermostats current
  479. # scale setting. Be sure to set the scale based on the _$s value.
  480. # {presence _FNXMFJHKHKSHJ away} {targetTemp_F _FNXKSLFJSLF 75} ...
  481. LOG "Adding UI Event to Update Queue"
  482. set e "$eventType $deviceID $newValue"
  483. set ::URC::UpdateListing "{$e} $::URC::UpdateListing"
  484. if {[llength $::URC::UpdateListing] > $::API::maxUIEvents} {set ::URC::UpdateListing [lreplace $::URC::UpdateListing 10 end]}
  485. LOG "Latest UI Events:\n$::URC::UpdateListing"
  486. }
  487.  
  488. proc onPresenceChange {id status} {
  489. # Called when Presence change is considered valid.
  490. # Will include the Structure ID of the Status Change
  491. # as well as the status.
  492. # It is safe to Execute Event at this point.
  493. # Status Will be Either: <away, home>
  494.  
  495. LOG "Presence Changed for ID: $id\nChanged to: $status"
  496. ::URC::UpdateUI presence $id $status
  497. }
  498.  
  499. proc interfaceServer {get} {
  500. # Interfaces with URC Interfaces using the device_proc command
  501.  
  502. switch -nocase -- $get {
  503. status {if {[dict exists $::Nest::Status current]} {return [dict get $::Nest::Status current]}}
  504. events {return $::URC::UpdateListing}
  505. devices {return $::Nest::devices}
  506. structureNames {return [dict keys [dict get $::Nest::devices structures]]}
  507. default {return -1}
  508. }
  509. }
  510. LOG "-- URC Module Information --"
  511. # LOG "Module Version: $::varIfVersionNum"
  512. LOG "Created for: Nest API, Firmware v4.1"
  513.  
  514. }
  515.  
  516. namespace eval System {
  517.  
  518. proc traceCallback {value args} {
  519. LOG "Trace Callback Initiated"
  520. LOG "Value: $value"
  521. LOG "Args:\n$args"
  522.  
  523. switch -- $value {
  524. status {
  525. LOG "Nest Status has been Updated!"
  526. if {[dict exists $::Nest::Status old]} {set changes [::System::dictCompare [dict get $::Nest::Status current] [dict get $::Nest::Status old]]
  527. } else {LOG "First Status Update, Ending"; return}
  528. # In General Response will follow this index map:
  529. # 0 - <devices, structures>
  530. # 1 - (Devices) - <thermostats, smoke_co_alarms> | (Structures) - <ID>
  531. # 2 - (Devices) - <ID> | (Structures) - <valueKey>
  532. # 3 - (Devices) - <valueKey>
  533. LOG "Changes Response:\n$changes"
  534. foreach change $changes {
  535. set newValue [lindex $change 1]
  536. if {[lindex [lindex $change 0] 0] == "devices"} {
  537.  
  538. } elseif {[lindex [lindex $change 0] 0] == "structures"} {
  539.  
  540. }
  541. if {[regexp -all {_f|_c} $change] > 1} {}
  542. }
  543. }
  544. i {set ::API::CMDS::Type i}
  545. s {set ::API::CMDS::Type s}
  546. devices {
  547. LOG "|---- NEW Devices Variable Value:\n$::Nest::devices"
  548. # ::URC::UpdateUI devices
  549. }
  550. presence {
  551. LOG "|---- Presence Change Detected"
  552. dict for {id val} $::Nest::presence {
  553. if {[dict get $::Nest::presence $id startup] == 0} {
  554. LOG "Presence Change Validated"
  555. dict set ::Nest::presence $id startup -1
  556. ::URC::onPresenceChange $id [dict get $::Nest::presence $id status]
  557. }
  558. }
  559.  
  560.  
  561. }
  562. default {LOG "ERROR in traceCallback"}
  563. }
  564. }
  565.  
  566. proc convertJSON {data} {
  567. LOG "|--- Convert JSON Starts"
  568. set newData [string map {"data: " ""} $data]
  569. if {$newData == "null"} {LOG "Heartbeat Detected, Ending Parse"; return}
  570. # set newData [split $data " "]
  571. set newData [json::json2dict $newData]
  572. return $newData
  573. }
  574.  
  575. proc difflists {l1 l2} {
  576. set lcsData [struct::list longestCommonSubsequence $l1 $l2]
  577. set diffs [struct::list lcsInvert $lcsData [llength $l1] [llength $l2]]
  578. set changedKeys ""
  579. foreach d $diffs {
  580. set op ""
  581. lassign $d type i1 i2; lassign $i1 s1 e1; lassign $i2 s2 e2
  582. LOG "$type: [lrange $l2 {*}$i2] to -> [lrange $l1 {*}$i1]"
  583. #:[lindex $l1 $e1] -> [lindex $l2 $s2-1]:[lindex $l2 $e2]
  584. set flatkey [lindex $l1 $s1-1]; lappend op [string map {. " "} $flatkey]
  585. lappend op [lrange $l1 {*}$i1]
  586. lappend changedKeys $op
  587. }
  588. return $changedKeys
  589. }
  590.  
  591. proc dict_flatten {dictVal} {
  592. dict for {k v} $dictVal {
  593. if {[llength $v] % 2 == 0} {
  594. set dictVal [dict remove $dictVal[set dictVal {}] $k]
  595. dict for {sk sv} [dict_flatten $v] {dict set dictVal ${k}.${sk} $sv}
  596. }
  597. }
  598. return $dictVal
  599. }
  600.  
  601. proc dictCompare {newDict oldDict} {
  602. # Compares Two Dictionaries and Returns with a List
  603. # with all changed Dictionary Keys.
  604. # Example Resonse:
  605. # {key1 key1 key1} {key1 key1 key2} {key2 key1 key1}
  606. # Get Values using the {*} operator on the response:
  607. # dict get $dict {*}[lindex $differences 0]
  608. set differences [difflists [dict_flatten $newDict] [dict_flatten $oldDict]]
  609. return $differences
  610. }
  611. }
  612.  
  613. Nest::Query
  614. after 10000 {set i 0}
  615. vwait i