Posted to tcl by Napier at Thu Jul 03 15:56:55 GMT 2014view raw
- # --------------------------------------------
- # Nest & Smoke+CO TCL Integration Library
- # Connects to the Nest API for control &
- # monitoring of NEST Products.
- #
- # Author: Automation Apps
- # --------------------------------------------
- # Main Command Syntax:
- # -fanTimer <start,stop> -mode <off, heat, cool, heat-cool> -
- #
-
- 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.DWjy5mnOvgHcJ1NdKisooOZEIyO5dpVacU8tC5B9Me6QtkQ7vtHOqwYMdneHz8pnNf3raKOQlVjaIVmyQCZFnAxC5H7vKGP5WK3IKTTX3l8Gd0pBB0B1th2Q5cOWZFNWOJiq8IPHZPv6U5HB"
- variable authState $::varIfMRXMAC
- variable baseURL "https://developer-api.nest.com"
- 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 maxEvents 25
- # variable forecast "https://home.nest.com/api/0.1/weather/forecast/${zip},${country}"
-
-
- 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}
- }
- }
-
- set bodyTXT ""
- 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"; 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]} {LOG "No Request Detected"; dict set tempDict -request "None"
- } elseif {[dict get $tempDict -request] == "Query"} {LOG "Set Event Stream Header"; http::config -accept "text/event-stream"
- } else {LOG "Set Accept All Header"; http::config -accept "/*"}
- set ::Web::RequestData $tempDict
- 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]
- } 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 "--------------------------"
- # LOG "Token: $token"
- if {[string match {30[1237]} [::http::ncode $token]]} {LOG "Redirect Required!"; ::Web::startRedirect $token
- } elseif {[::http::ncode $token] == 200} {LOG "Successfull Call Detected"
- #http::cleanup $token
- #switch -- [dict get $data -request] {
- # Authorize {LOG "Authorize Data Assumed"; set response [::json::json2dict [dict get $response data]]}
- # default {LOG "Unknown or Undefined Request, Saving Value to ::Nest::StatusUndefined"; set ::Nest::StatusUndefined $data}
- #}
- # Query {LOG "Query Data Assumed"; ::Nest::setStatus [::json::json2dict [dict get $response data]]}
- } elseif {[::http::ncode $token] == 429} {LOG "!!!! ---- |||| Too Many Requests Error Received |||| ----- !!!!"}
- }
-
- proc httpProgress {token total current} {
- LOG "Start Progress"; LOG "Total: $total"; LOG "Current: $current"
- # The Following if Statement is used to parse out the random "e" received
- # at the start of the Event Stream Data.
- if {$current == 1} {LOG "END"; return}
- upvar #0 $token state
- set body "$state(body)"
- set occurrences [regsub -all {\s*\n\s*\n(\s*\n)*} $body "\n" body]
- set body [split $body \n]
- if {[dict get [lindex $body 0] "event:"] == "auth_revoked"} {::Nest::AuthRevoked}
- # The occurrences variable actively tracks how many events are currently saved
- # in the $state(body) variable. It is likely a good idea to flush this out at
- # a set interval by closing (http::cleanup) and re-initiating the event.
- LOG "Event Occurences: $occurrences"
- if {$occurrences >= $::API::maxEvents} {LOG "Flushing Events"
- # Process to Flush Events & Re-Initiate the Event Stream
- # should go here.
- }
- if {$::Nest::Status == ""} {
- set startData [::System::convertJSON [lindex $body 1]]
- LOG "Start Data:\n$startData"
- ::Nest::setStatus [dict get $startData data]
- } else {
- set newData [::System::convertJSON [lindex $body end-1]]
- if {$newData != ""} {
- LOG "New Data:\n$newData"
- if {[dict exists $::Nest::Status current] && [dict get $::Nest::Status current] != [dict get $newData data]} {
- LOG "Data is Different, Setting Nest Status Variable"; ::Nest::setStatus [dict get $newData data]
- } else {LOG "New Data has Not Changed, Do Nothing"}
- }
- }
- }
-
- 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]]
- LOG "URL: $url"
- http::config -accept "text/event-stream"
- if {[dict get $data -method] == "GET"} {set token [http::geturl $url -keepalive 1 -progress ::Web::httpProgress]
- } else {set token [http::geturl $url -method [dict get $data -method] -query [dict get $data -body] -command ::Web::httpCallback]}
- }
-
- 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 "" structures ""]
- variable presence [dict create]
-
-
- 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 AuthRevoked {args} {
- # This Procedure is called when the "Auth_Revoked" Event
- # is Received from Nest. You will need to re-authenticate
- # and get a new "Auth Token" when this occurs.
- }
-
- proc Query {args} {
- # Queries The Nest API Based on Your Parameters
- # If blank (::Nest::Query) it will query all parameters into the main dictionary
- # Do NOT do this Often as Nest limits the amount of calls you can do per
- # month per token.
- # All data will be kept up-to-date using the Rest Streaming Feature of the API.
- # -thermostat <therm ID or name> -smoke <smokeCO ID or name>
- Web::Send -request "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} {
- # 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 all
- }
-
- 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
-
- set tempDict [dict create]
- LOG "Current Status Var Value"
- if {$::Nest::Status == ""} {LOG "Query Never Performed? Attempting Query..."; Nest::Query}
- switch -regexp -nocase -- $kind {
- {^thermostats?$} {
- LOG "Getting Thermostats"
- if {[dict exists $::Nest::Status current devices thermostats]} {set thermIDs [dict keys [dict get $::Nest::Status current devices thermostats]]
- } else {LOG "No Thermostats Detected"; return -1}
- LOG "Therm IDs: $thermIDs"
- foreach id $thermIDs {
- set name [dict get $::Nest::Status current devices thermostats $id name]
- dict set tempDict $name $id
- }
- if {[dict exists $::Nest::devices thermostats] && [dict get $::Nest::devices thermostats] == $tempDict} {LOG "No Change to Thermostats, Do Nothing"; return}
- LOG "Change Detected to Thermostats, Setting Devices Dictionary"
- dict set ::Nest::devices thermostats $tempDict
- return
- }
- {^protects?$} {
- LOG "Getting 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"; return -1}
- foreach id $protectIDs {
- set name [dict get $::Nest::Status current devices smoke_co_alarms $id name]
- dict set tempDict $name $id
- }
- if {[dict exists $::Nest::devices protects] && [dict get $::Nest::devices protects] == $tempDict} {LOG "No Change to Protects, Do Nothing"; return}
- LOG "Change Detected to Protects, Setting Devices Dictionary"
- dict set ::Nest::devices protects $tempDict
- return
- }
- {^structures?$|^homes?$|^buildings?$} {
- LOG "Getting Structures"
- if {[dict exists $::Nest::Status current structures]} {set structIDs [dict keys [dict get $::Nest::Status current structures]]
- } else {LOG "No Structured Were Detected"; return -1}
- foreach id $structIDs {
- set name [dict get $::Nest::Status current structures $id name]; LOG "Structure Name: $name"
- if {[dict exists $::Nest::Status current structures $id thermostats]} {set therms [dict get $::Nest::Status current structures $id thermostats]
- } else {LOG "No Thermostats for Structure $name"; set therms ""}
- if {[dict exists $::Nest::Status current structures $id smoke_co_alarms]} {set protects [dict get $::Nest::Status current structures $id smoke_co_alarms]
- } else {LOG "No Protects for Structure: $name"; set protects ""}
- # Check Presence Status for Structures and Report to the System if it has changed
- dict set tempDict $name [dict create id $id thermostats $therms protects $protects]
- set presence [dict get $::Nest::Status current structures $id away]
- if {![dict exists $::Nest::presence $id]} {LOG "Creating Presence for First Time"; dict set ::Nest::presence $id [dict create status $presence startup 1]
- } 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}}
- }
- if {[dict exists $::Nest::devices structures] && [dict get $::Nest::devices structures] == $tempDict} {LOG "No Change to Structures, Do Nothing"; return}
- LOG "Change Detected to Structures, Setting Devices Dictionary"
- dict set ::Nest::devices structures $tempDict
- return
- }
- {^all} {LOG "Getting All Devices & Structures"
- getDevices thermostats; getDevices protects; getDevices structures
- }
- default {LOG "Kind was not Recognized - should be thermostats, protects, or all"}
- }
- }
-
- proc nameFromID {id {kind "thermostats"}} {
- # Used to Return the "Room Name" or "Structure Name" of a given "ID"
-
- if {$kind != "structures"} {
- LOG "Parsing ID"
- dict for {k v} [dict filter [dict get $::Nest::devices $kind] value $id] {
- set keys $k
- }
- } else {
- LOG "Getting Structure Name"
- set structNames [dict keys [dict get $::Nest::devices structures]]
- foreach struct $structNames {
- if {$id == [dict get $::Nest::devices structures $struct id]} {
- set keys $struct
- } else {continue}
- }
- }
- LOG "Returning: $keys"
- return $keys
- }
-
- proc DeviceNamesByStructure {struct {kind "thermostats"}} {
- # Will return a list with the thermostat names for a given Structure.
- # Structure can be the Structure ID or Structure Name.
- set names ""
- if {![dict exists $::Nest::devices structures $struct $kind]} {
- LOG "Getting Name from Structure ID"; set struct [::Nest::nameFromID $struct structures]
- }
- foreach deviceID [dict get $::Nest::devices structures $struct $kind] {lappend names [::Nest::nameFromID $deviceID $kind]}
- LOG "$kind in Room: $names"
- return $names
- }
-
- proc listRooms {{struct "all"}} {
- # 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.
- # By default listRooms will list all rooms in the System regardless of what Nest
- # structure they are within. Providing a Structure Name or ID as an argument will
- # list rooms only for that structure.
- # This differentiates itself from ::Nest::DeviceNamesByStructure because it returns
- # a unique list of rooms. It also automatically checks all room names that any
- # device is placed into.
- # Syntax:
- # ::Nest::listRooms "Home"
-
- set l ""
- if {$struct == "all"} {
- set l [dict keys [dict get $::Nest::devices thermostats]]
- lappend l {*}[dict keys [dict get $::Nest::devices protects]]
- } else {
- if {![dict exists $::Nest::devices structures $struct]} {
- LOG "Getting Name from Structure ID"; set struct [::Nest::nameFromID $struct structures]
- }
- foreach deviceID [dict get $::Nest::devices structures $struct thermostats] {lappend l [::Nest::nameFromID $deviceID thermostats]}
- foreach deviceID [dict get $::Nest::devices structures $struct protects] {lappend l [::Nest::nameFromID $deviceID protects]}
- }
- LOG "Raw List is:\n$l"
- set l [lsort -unique $l]
- LOG "Formatted List is:\n$l"
- return $l
- }
-
- proc findDeviceKind {id} {
- # This Procedure will return the "Kind" of Device based on a specified ID
- # Example: ::Nest::findDeviceKind _MXPPHwK669HMC6Mtqb51qtN3uLTVu2B
- # Returns: < thermostats , protects, structures >
- # You can then call ::Nest::nameFromID $response to get the name/room/structure.
- # set name [::Nest::nameFromID $id [::Nest::findDeviceKind $id]]
-
- set l [dict values [dict get $::Nest::devices thermostats]]
- if {[string match *$id* $l]} {return thermostats}
- set l [dict values [dict get $::Nest::devices protects]]
- if {[string match *$id* $l]} {return protects}
- set l ""
- set structNames [dict keys [dict get $::Nest::devices structures]]
- foreach struct $structNames {
- lappend l {*}[dict get $::Nest::devices structures $struct id]
- }
- if {[string match *$id* $l]} {return structures}
- }
-
- # Trace the Devices Variable to trigger changes to UI when new devices are added or changed
- # in any way.
- trace add variable ::Nest::devices write {::System::traceCallback devices}
- trace add variable ::Nest::Status write {::System::traceCallback status}
- trace add variable ::Nest::presence write {::System::traceCallback presence}
- }
-
- namespace eval URC {
-
- proc UpdateUI {eventType} {
- # This Procedure is called when a setting needs to be
- # updated to URC User Interfaces.
- # eventType = <devices, status>
-
- }
-
- proc onPresenceChange {id status} {
- # Called when Presence change is considered valid.
- # Will include the Structure ID of the Status Change
- # as well as the status.
- # It is safe to Execute Event at this point.
- # Status Will be Either: <away, home>
-
- LOG "Presence Changed for ID: $id\nChanged to: $status"
-
- }
-
- proc interfaceServer {} {
- # Interfaces with URC Interfaces
- }
- LOG "-- URC Module Information --"
- # LOG "Module Version: $::varIfVersionNum"
- LOG "Created for: Nest API, Firmware v4.1"
-
- }
-
- namespace eval System {
-
- proc traceCallback {value args} {
- LOG "Trace Callback Initiated"
- LOG "Value: $value"
- LOG "Args:\n$args"
-
- switch -- $value {
- status {
- LOG "Nest Status has been Updated!"
- ::URC::UpdateUI status
- }
- i {set ::API::CMDS::Type i}
- s {set ::API::CMDS::Type s}
- devices {
- LOG "|---- NEW Devices Variable Value:\n$::Nest::devices"
- ::URC::UpdateUI devices
- }
- presence {
- LOG "|---- Presence Change Detected"
- dict for {id val} $::Nest::presence {
- if {[dict get $::Nest::presence $id startup] == 0} {
- LOG "Presence Change Validated"
- dict set ::Nest::presence $id startup -1
- ::URC::onPresenceChange $id [dict get $::Nest::presence $id status]
- }
- }
-
-
- }
- default {LOG "ERROR in traceCallback"}
- }
- }
-
- proc convertJSON {data} {
- LOG "|--- Convert JSON Starts"
- set newData [string map {"data: " ""} $data]
- if {$newData == "null"} {LOG "Heartbeat Detected, Ending Parse"; return}
- # set newData [split $data " "]
- set newData [json::json2dict $newData]
- return $newData
- }
- }
-
- vwait forever