Posted to tcl by Napier_ at Thu Jul 03 02:16:19 GMT 2014view raw
- # --------------------------------------------
- # Nest & Smoke+CO TCL Integration Library
- # Connects to the Nest API for control &
- # monitoring of NEST Products.
- #
- # Authors: BN & RR
- # --------------------------------------------
- # Main Command Syntax:
- # -fanTimer <start,stop> -mode <off, heat, cool, heat-cool> -
- #
- set thermNum 0
-
- #DICTS
- set devices_status [dict create]
-
- proc LOG {data} {
- puts $data
- }
-
- #LOG functions
- proc enableLOG { status } {
- global LOGENABLE
- set LOGENABLE $status
- }
-
- enableLOG 0
- set varIfMRXMAC "000"
-
- # Include Packages in the includePackages List
- set includePackages "http tls json json::write tdom uri"
- foreach {pkg} $includePackages {package require $pkg}
-
- tls::init -tls1 true
- http::register https 443 tls::socket
-
- namespace eval API {
- # Be Sure to Set your Client Secret & Client ID
- variable FB_Auth "auth=c.Q2z4m8hGR9dPA2lFvrXWNiJzlGLhsIIY3CKMRdZtWYOBPm25CLcWu3Nd8608scr63m8FeoOJXgLFuIyEUxm2Anj0IPNH8HjUa6aR6iKRIseNy1NUc212hpIik1JxBt4TYz5CRlkAje4Gohln"
- variable authState $::varIfMRXMAC
- variable baseURL "https://developer-api.nest.com/"
- variable redirectURL ""
- variable basePort 443
- variable client_secret "wCJzrq2VuyWcH70DjENWpjupi"
- variable client_id "7c3fe371-5740-48ac-a1b2-74df90145813"
- variable ThermBaseURL "devices/thermostats/"
- variable authRequestURL "https://home.nest.com/login/oauth2?client_id=7c3fe371-5740-48ac-a1b2-74df90145813&state=ip"
- variable authorizeURL "https://api.home.nest.com/oauth2/access_token?code=|AUTHPIN|&client_id=${client_id}&client_secret=${client_secret}&grant_type=authorization_code"
- #variable forecast "https://home.nest.com/api/0.1/weather/forecast/10528,USA"
-
- namespace eval CMDS {
- variable FanTimer "fan_timer_active" ; # Boolean - Sets 15 Minute Fan Timer {"fan_timer_active": true }
- # JSON Example: { "fan_timer_active": true }
- variable Temp_F "target_temperature_f" ; # Integer - Sets Fahrenheit {"target_temperature_f": 70} (1 Accuracy)
- # JSON Example: { "target_temperature_f": 70 }
- variable Temp_C "target_temperature_c" ; # Integer - Sets Celcius (0.5 Accuracy)
- # JSON Example: { "target_temperature_c": 30.5 }
- variable HighTemp_F "target_temperature_high_f" ; # Integer - Set High Temp in Fahrenheit (1 Accuracy)
- # JSON Example: { "target_temperature_high_f": 75 }
- variable HighTemp_C "target_temperature_high_c" ; # Integer - Set High Temp in Celcius (0.5 Accuracy)
- # JSON Example: { "target_temperature_high_c": 40.5 }
- variable LowTemp_F "target_temperature_low_f" ; # Integer - Set Low Temp in Fahrenheit (1 Accuracy)
- # JSON Example: { "target_temperature_low_f": 65 }
- variable LowTemp_C "target_temperature_low_c" ; # Integer - Set Low Temp in Celcius (0.5 Accuracy)
- # JSON Example: { "target_temperature_low_c": 20.5 }
- variable mode "hvac_mode" ; # String - Set HVAC Mode <heat, cool, heat-cool, off>
- # JSON Example: {"hvac_mode": "heat-cool"}
-
- variable Type 0
-
- trace add variable ::API::CMDS::FanTimer read {::System::traceCallback i}
- trace add variable ::API::CMDS::Temp_F read {::System::traceCallback i}
- trace add variable ::API::CMDS::Temp_C read {::System::traceCallback i}
- trace add variable ::API::CMDS::HighTemp_F read {::System::traceCallback i}
- trace add variable ::API::CMDS::HighTemp_C read {::System::traceCallback i}
- trace add variable ::API::CMDS::LowTemp_F read {::System::traceCallback i}
- trace add variable ::API::CMDS::LowTemp_C read {::System::traceCallback i}
- trace add variable ::API::CMDS::mode read {::System::traceCallback s}
- }
- }
-
- namespace eval Web {
-
- variable RequestData [dict create]
-
- proc Send {args} {
- # Syntax:
- # -method $METHOD -url $URL -params $PARAMS -suffix $URL-Suffix -port $port -body $body -request $request
- # -request feeds the RequestData Variable to Identify to the Callback
- # Web::Send -method PATCH -url https://www.url.com -params "Hi Hello" -suffix "devices/hello" -body $json -port 9553 would PATCH:
- # https://www.url.com:9553/devices/hello?auth={AUTH CODE}&hi=hello with the value of $json as the body
- LOG "Beginning Web::Send $args"
- set tempDict $args
- if {$::API::FB_Auth == 0} {LOG "OAuth Required Before Continuing"; #TODO: Build Re-Auth Service}
- if {![dict exists $tempDict -suffix]} {LOG "No Suffix Detected"; dict set tempDict -suffix ""}
- if {![dict exists $tempDict -url]} {
- LOG "No URL using Default";
- #if {$::API::redirectURL != ""} {
- #dict set tempDict -url $::API::redirectURL
- #} else {
- dict set tempDict -url $::API::baseURL
- #}
- }
- if {![dict exists $tempDict -method]} {LOG "No Method Defined, Using GET"; dict set tempDict -method "GET"}
- if {[dict exists $tempDict -params]} {LOG "Formatting Query"; dict set tempDict -params "&[::http::formatQuery {*}[dict get $tempDict -params]]"
- } else {LOG "No Params Detected"; dict set tempDict -params ""}
- if {[dict exists $tempDict -port]} {LOG "Custom Port Detected: [dict get $tempDict -port]"; http::register https [dict get $tempDict -port] tls::socket
- } else {http::register https $::API::basePort tls::socket}
- if {![dict exists $tempDict -body]} {LOG "No Body Detected"; dict set tempDict -body ""}
- if {![dict exists $tempDict -request]} {dict set tempDict -request "None"}
- set ::Web::RequestData $tempDict
- http::config -accept text/event-stream
- 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]
- } else {set token [http::geturl [dict get $tempDict -url]/[dict get $tempDict -suffix]?${::API::FB_Auth}[dict get $tempDict -params] \
- -method [dict get $tempDict -method] -query [dict get $tempDict -body] -command ::Web::httpCallback]
- }
- }
-
- proc httpCallback {token} {
- set data $::Web::RequestData
- set response [dict create data [http::data $token] code [http::code $token]]
- LOG "----- RECEIVING DATA -----"
- LOG "$response"
- LOG "--------------------------"
-
- #set ::device_status []
- if {[string match {30[1237]} [::http::ncode $token]]} {LOG "Redirect Required!"; ::Web::startRedirect $token; return
- } elseif {[::http::ncode $token] == 200} {LOG "Successfull Call Detected"
- switch -- [dict get $data -request] {
- Query {LOG "DEV STATUS"; ::Nest::setStatus [::json::json2dict [dict get $response data]]}
- Authorize {LOG "AUTHORIZE"; set response [::json::json2dict [dict get $response data]]}
- default {LOG "Unknown or Undefined Request, Saving Value to ::Nest::StatusUndefined"; set ::Nest::StatusUndefined $data}
- }
- } elseif {[::http::ncode $token] == 429} {LOG "Too Many Requests Error Received"}
- ::http::cleanup $token
- }
-
- proc startRedirect {token} {
- set data $::Web::RequestData
- array set meta [set ${token}(meta)]
- LOG "[parray {*}$token]"
- http::cleanup $token
- if {![info exist meta(Location)]} {LOG "No Redirection Indicator?"; return}
- array set uri [::uri::split $meta(Location)]
- LOG "URI:"; LOG "[parray uri]"
- unset meta
- if {$uri(host) == ""} {set uri(host) $uri(host)}
- set url [eval ::uri::join [array get uri]]
- set ::API::redirectURL "$uri(scheme)://$uri(host):$uri(port)"
- LOG "URL: $url"
- http::config -accept text/event-stream
- if {[dict get $data -method] == "GET"} {set token [http::geturl $url -keepalive 1 -headers {Accept text/stream} -progress ::Web::httpProgress -command ::Web::httpCallback]
- } else {set token [http::geturl $url -method [dict get $data -method] -query [dict get $data -body] -command ::Web::httpCallback]}
- }
-
- proc httpProgress {token total current} {
- upvar #0 $token state
- set body "$state(body)"
- puts $body
- }
-
- proc sendEmail {to msg args} {
- # This Procedure will Prepare and Send an E-Mail to the address(es) provided in
- # the "to" argument. It will include the content in the "msg" argument.
- # Additionally the following arguments can be defined if needed:
- # -footer $txt
-
- }
-
- }
-
- namespace eval Nest {
- variable Status ""
- variable StatusUndefined 0
- variable thermostats [dict create]
- variable devices [dict create thermostats "" protects ""]
-
- proc Authorize {authPIN} {
- # Sends PIN to Nest to retrieve authentication string
- set authURL [string map "|AUTHPIN| $authPIN" $::API::authorizeURL]
- LOG "Authorization URL:\n $authURL"
- Web::Send -method POST -url $authURL -request Authorize
- #TODO: Send the Authorization as POST with Parameters
- }
-
- proc Query {args} {
- # Queries The Nest API Based on Your Parameters
- # If blank (::Nest::Query) it will query all parameters into the main dictionary
- # -thermostat <therm ID or name> -smoke <smokeCO ID or name>
-
- Web::Send -request Query
- #after 10000 {Nest::Query}
- }
-
- proc Set {args} {
- # This Procedure allows you to Control Nest Products using
- # their Official API.
- # -command - Specify the command as a key/value pair. Comaand options and
- # examples can be found in the ::API::CMDS Namespace and should be referenced
- # as that variable.
- # -value - Specify the Value you would like to feed to the Process. This should follow
- # the examples in the "::API::CMDS" namespace.
- # -device {deviceID} - This argument specifies the device's ID which
- # should be manipulated
- # -kind {kind} - Specify what kind of device will be controlled.
- # this is an optional value but specifying it will increase overall speed.
- # Syntax Example:
- # ::Nest::Set $::API::CMDS::Temp(F) 75 -device $thermID -kind thermostat
-
- LOG "Beginning Set Procedure"
- if {[dict exists $args -kind]} {
- LOG "Kind was Specified: [dict get $args -kind]"
- switch -regexp -- [dict get $args -kind] {
- {^thermostats?$|^therms?$} {::Nest::setThermostat [dict get $args -command] [dict get $args -value] [dict get $args -device]}
- {^protects?$|^smokecos?$|^detectors?$} {LOG "Nest Protect Specified\nThere are currently No Official Set Options for Nest Protect"}
- default {puts "No"}
- }
- } else {
- LOG "Kind was Not Specified - Starting ID Trace"
- }
- }
-
- proc setThermostat {cmd value id} {
- LOG "Setting the Thermostat with ID: $id"
- # This Procedure should generally not be called directly, use ::Nest::Set $cmd $value -kind (optional)
- # Currently Adds JSON and sends one Command at a Time - another procedure will be written to send multiple
- # values with a single call.
-
- if {$::API::CMDS::Type == "i"} { set json [::json::write object $cmd $value]
- } elseif {$::API::CMDS::Type == "s"} { set json [::json::write object $cmd [::json::write string $value]]
- }
- LOG "|--- Sending in Patch Body:\n$json"
- LOG "|--- Sending to Suffix:\n${::API::baseURL}${::API::ThermBaseURL}$id"
- ::Web::Send -method PATCH -suffix ${::API::ThermBaseURL}$id -body $json
- }
-
- proc Experimental {args} {
- # This procedure will house non-official API procedures that
- # may not be supported in the long-term. Generally this will
- # emulate features used on the Official Nest App & Web Control
- # app.
-
- }
-
- proc setStatus {newStatus args} {
- LOG "PRINT #1"
- # This Procedure handles Status Setting when a Query is Made. If Status was already present, it will save the
- # "current" Status into the "old" Status Dictionary and the newly received status into the "current" dictionary.
- # This can then be used to evaluate events as needed for Macro Integration
-
- LOG "---- New Nest Status Received!\n$newStatus"
- if {$::Nest::Status != "" && [dict exists $::Nest::Status current]} {dict set ::Nest::Status old [dict get $::Nest::Status current]}
- dict set ::Nest::Status current $newStatus
- getDevices thermostats
- getDevices protects
- getHomes
- }
-
- proc getHomes {} {
- LOG "PRINT #4"
- set tpDict [dict create]
- if {$::Nest::Status == ""} {LOG "Query Never Performed? Attempting Query..."; Nest::Query}
-
- if {[dict exists $::Nest::Status current structures]} {
- set thermHomes [dict keys [dict get $::Nest::Status current structures]]
- LOG "_____THERMOSTATS HOMES: $thermHomes"
-
- foreach id $thermHomes {
- set name [dict get $::Nest::Status current structures $id name]
- dict set tpDict $name $id
- dict set ::Nest::Homes thermostats $tpDict
- LOG "------ HOMES ___: $::Nest::Homes"
- }
- } else {LOG "No Homes Detected - Attempting Query"}
- }
-
- proc getDevices {kind} {
- # Parses the Data from the Queries and returns "kind" to the proper
- # Dictionary with name/id as the key/value pairs
- # Options for Kind are: thermostats, smoke
-
- LOG "PRINT #2"
- set tempDict [dict create]
- if {$::Nest::Status == ""} {LOG "Query Never Performed? Attempting Query..."; Nest::Query}
- switch -regexp -nocase -- $kind {
- {^thermostats?$} {
- LOG "PRINT #3"
- if {[dict exists $::Nest::Status current devices thermostats]} {
- set thermIDs [dict keys [dict get $::Nest::Status current devices thermostats]]
- LOG "_____THERMOSTATS LIST: $thermIDs"
- } else {LOG "No Thermostats Detected - Attempting Query"}
- foreach id $thermIDs {
- set name [dict get $::Nest::Status current devices thermostats $id name]
- dict set tempDict $name $id
- dict set ::Nest::Devices thermostats $tempDict
- incr ::thermNum
- }
- }
- {^protects?$} {
- if {[dict exists $::Nest::Status current devices smoke_co_alarms]} {set protectIDs [dict keys [dict get $::Nest::Status current devices smoke_co_alarms]]
- } else {LOG "No Protects Were Detected"}
- foreach id $protectIDs {
- set name [dict get $::Nest::Status current devices smoke_co_alarms $id name]
- dict set tempDict $name $id
- dict set ::Nest::Devices protects $tempDict
- }
- }
- {^structure?$} {}
- {^all} {}
- default {LOG "Kind was not Recognized - should be thermostats, protects, or all"}
- }
- }
-
- #proc nameFromID {id {kind "thermostats"}} {
- ## Used to Return the "Room Name" of a given "Device ID"
- #dict for {k v} [dict filter [dict get $::Nest::Devices $kind] value $id] {
- #lappend keys $k
- #}
- #return $keys
- #}
-
- proc listRooms {} {
- # This procedure will return the rooms within the project which includes both
- # the Thermostats & the Nest Protects. Returns into List which should be useable
- # for URC's TCL List Objects. Automatically handles duplicate values and only returns
- # unique values.
-
- set l [dict keys [dict get $::Nest::Devices thermostats]]
- lappend l {*}[dict keys [dict get $::Nest::Devices protects]]
- LOG "Raw List is:\n$l"
- set l [lsort -unique $l]
- LOG "Formatted List is:\n$l"
- return $l
- }
-
- proc findDeviceKind {id} {
- # INCOMPLETE!
- # This Procedure will return the "Kind" of Device based on a specified ID
- # Example: ::Nest::findDeviceKind _MXPPHwK669HMC6Mtqb51qtN3uLTVu2B
- # Returns: < thermostat , protect >
-
- }
- }
-
- namespace eval URC {
- proc interfaceServer {} {
- # Interfaces with URC Interfaces
- }
- LOG "-- URC Module Information --"
- #LOG "Module Version: $::varIfVersionNum"
- LOG "Created for: Nest API, Firmware v4.0"
-
- }
-
- namespace eval System {
- proc traceCallback {value args} {
- switch -- $value {
- i {set ::API::CMDS::Type i}
- s {set ::API::CMDS::Type s}
- }
- }
- }
-
- proc setTemp {type num} {
- #DoNothing
- }
-
- proc displayListType {num} {
- #DoNothing
- }
-
- #set thermID "_aLf8zP-ItVL9f8vs_Ej6DSC7Dulq49h"
- #Nest::Query;after 3000 {set i 0};vwait i
- Nest::Query
- #Nest::Set -command $::API::CMDS::Temp_F -value 85 -device $thermID -kind thermostat;after 3000 {set i 0};vwait i
- vwait forever
-