mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-15 13:10:51 +00:00
668 lines
19 KiB
Groovy
668 lines
19 KiB
Groovy
/**
|
|
* Copyright 2015 SmartThings
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
* in compliance with the License. You may obtain a copy of the License at:
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing permissions and limitations under the License.
|
|
*
|
|
* Navien Service Manager
|
|
*
|
|
* Author: sangju
|
|
* Date: 2015-11-01
|
|
*
|
|
*/
|
|
definition(
|
|
name: "나비엔 스마트톡 연동",
|
|
namespace: "smartthings",
|
|
author: "나비엔 스마트톡",
|
|
description: "SmartThings에서 나비엔 스마트톡의 온도조절기를 연결합니다.",
|
|
category: "SmartThings Labs",
|
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Navien/navien.png",
|
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Navien/navien@2x.png",
|
|
singleInstance: true
|
|
) {
|
|
appSetting "clientId"
|
|
appSetting "serverUrl"
|
|
appSetting "bcd"
|
|
}
|
|
|
|
preferences {
|
|
page(name: "loginPage", title: "나비엔 스마트톡 등록")
|
|
page(name: "navienAuth", title: "나비엔 스마트톡 등록")
|
|
page(name: "navienDeviceList", title: "나비엔 스마트톡 등록", install: true )
|
|
}
|
|
|
|
def refreshToken
|
|
def authToken
|
|
def userName
|
|
|
|
def loginPage(){
|
|
log.debug "authPage()"
|
|
|
|
def showUninstall = username != null && password != null
|
|
return dynamicPage(name: "loginPage", title: "경동나비엔", nextPage:"navienAuth", uninstall:false) {
|
|
section("나비엔 스마트톡 등록"){
|
|
input "username", "text", title: "나비엔 스마트톡 아이디", required: true, autoCorrect:false
|
|
input "password", "password", title: "나비엔 스마트톡 패스워드", required: true, autoCorrect:false
|
|
}
|
|
//section("To use Navien, SmartThings encrypts and securely stores your Navien credentials.") {}
|
|
}
|
|
}
|
|
|
|
def navienAuth(){
|
|
log.debug "navienAuth()"
|
|
|
|
def loginResult = forceLogin()
|
|
if(loginResult.success)
|
|
{
|
|
return dynamicPage(name: "navienAuth", title: "나비엔 스마트톡 인증", nextPage:"navienDeviceList", uninstall:false) {
|
|
section(){
|
|
paragraph "나비엔 스마트톡 인증 성공"
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return dynamicPage(name: "navienAuth", title: "나비엔 스마트톡 인증", nextPage:null, uninstall:false) {
|
|
section("Login failed"){
|
|
paragraph "나비엔 스마트톡 인증 실패"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def navienDeviceList(){
|
|
log.debug "navienDeviceList()"
|
|
|
|
def connectResult = navienConnecting()
|
|
def p
|
|
if(connectResult.success)
|
|
{
|
|
statusSetting(state.status)
|
|
|
|
def stats = getNavienThermostats()
|
|
log.debug "device list: $stats"
|
|
|
|
p = dynamicPage(name: "navienDeviceList", title: "나비엔 스마트톡 선택", install:true) {
|
|
section(""){
|
|
paragraph "나비엔 스마트톡 계정에서 사용할 수 있는 온도 조절 장치의 목록을 확인하고 SmartTings에 연결하려는 목록을 선택하려면 아래에서 선택해 주세요."
|
|
input(name: "thermostats", title:"", type: "enum", required:true, multiple:true, description: "Tap to choose", metadata:[values:stats])
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
p = dynamicPage(name: "navienDeviceList", title: "나비엔 스마트톡 선택", nextPage:null, uninstall:false) {
|
|
section("나비엔 스마트톡 온도 조절 장치의 연결 상태를 확인하시기 바랍니다."){
|
|
paragraph ""
|
|
}
|
|
}
|
|
}
|
|
log.debug "list p: $p"
|
|
return p
|
|
}
|
|
|
|
def getNavienThermostats() {
|
|
def stats = [:]
|
|
def dni = [getChildName()].join('.')
|
|
|
|
if(state.boilerType == "01") stats[dni] = "스마트톡"
|
|
else if(state.boilerType == "02") stats[dni] = "콘덴싱톡"
|
|
else stats[dni] = "----"
|
|
|
|
return stats
|
|
}
|
|
|
|
def installed() {
|
|
log.debug "Installed with settings: ${settings}"
|
|
initialize()
|
|
}
|
|
|
|
def updated() {
|
|
log.debug "Updated with settings: ${settings}"
|
|
|
|
unsubscribe()
|
|
initialize()
|
|
}
|
|
|
|
def uninstalled() {
|
|
def devices = getChildDevices()
|
|
if(devices != null) log.trace "deleting ${devices.size()} device"
|
|
}
|
|
|
|
def initialize() {
|
|
log.debug "initialize"
|
|
|
|
def d = getChildDevice(getChildName())
|
|
|
|
if(!d)
|
|
{
|
|
d = addChildDevice(getChildNamespace(), getChildName(), getChildName())
|
|
log.debug "created ${d.displayName} with id $dni"
|
|
}
|
|
else
|
|
{
|
|
log.debug "found ${d.displayName} with id $dni already exists"
|
|
}
|
|
|
|
def devices = d
|
|
|
|
log.debug "created ${devices.size()} thermostats"
|
|
|
|
def delete
|
|
// Delete any that are no longer in settings
|
|
|
|
if(!thermostats)
|
|
{
|
|
log.debug "If delete thermostats"
|
|
delete = getAllChildDevices()
|
|
}
|
|
else
|
|
{
|
|
log.debug "Else delete thermostats"
|
|
if(it != null) delete = getChildDevices().findAll { !thermostats.contains(it.deviceNetworkId) }
|
|
}
|
|
|
|
if(delete != null) log.debug "deleting ${delete.size()} thermostats"
|
|
if(it != null) delete.each { deleteChildDevice(it.deviceNetworkId) }
|
|
|
|
atomicState.thermostatData = [:]
|
|
|
|
pollHandler()
|
|
}
|
|
|
|
def pollHandler() {
|
|
log.debug ("pollHandler.")
|
|
pollChildren()
|
|
|
|
atomicState.thermostats.each {stat ->
|
|
def dni = stat.key
|
|
|
|
log.debug ("DNI = ${dni}")
|
|
debugEvent ("DNI = ${dni}")
|
|
|
|
def d = getChildDevice(dni)
|
|
if(d)
|
|
{
|
|
log.debug ("Found Child Device.")
|
|
debugEvent ("Found Child Device.")
|
|
debugEvent("Event Data before generate event call = ${stat}")
|
|
|
|
d.generateEvent(atomicState.thermostats[dni].data)
|
|
}
|
|
}
|
|
}
|
|
|
|
def pollChildren()
|
|
{
|
|
log.trace "polling children"
|
|
|
|
def pollParams = [
|
|
uri: getServerUrl()+"/api/SmartTokApi?bcd="+getBCD()+"&mid=${state.mid}&did=36&subid=1&cmd=01&data=0",
|
|
headers: ["Authorization": "Bearer ${state.authToken}"]
|
|
]
|
|
|
|
log.trace "Before HTTPGET to navien."
|
|
def jsonData
|
|
try{
|
|
httpGet(pollParams) { resp ->
|
|
if (resp.status == 200)
|
|
{
|
|
log.debug "poll results returned"
|
|
//atomicState.thermostats = resp.data.Status.inject([:]) { collector, stat ->
|
|
atomicState.thermostats = "1".inject([:]) { collector, stat ->
|
|
def dni = [getChildName()].join('.')
|
|
|
|
log.debug "updating dni $dni"
|
|
|
|
def data = statusSetting(resp.data.Status)
|
|
|
|
log.debug ("Event Data = ${data}")
|
|
|
|
collector[dni] = [data:data]
|
|
return collector
|
|
}
|
|
|
|
log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
|
|
}
|
|
else
|
|
{
|
|
log.error "polling children & got http status ${resp.status}"
|
|
|
|
//refresh the auth token
|
|
if(resp.status == 400)
|
|
{
|
|
log.debug "Bad Request Description"
|
|
}
|
|
else if(resp.status == 401)
|
|
{
|
|
log.debug "Unauthorized Description"
|
|
}
|
|
else if(resp.status == 500)
|
|
{
|
|
log.debug "InternalServerError Description"
|
|
atomicState.action = "pollChildren";
|
|
refreshAuthToken()
|
|
}
|
|
else
|
|
{
|
|
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch(all)
|
|
{
|
|
log.debug "___exception polling children: "
|
|
//refreshAuthToken()
|
|
}
|
|
}
|
|
|
|
def getPollRateMillis() { return 2 * 60 * 1000 }
|
|
|
|
def pollChild( child )
|
|
{
|
|
log.debug "poll child"
|
|
debugEvent ("poll child")
|
|
def now = new Date().time
|
|
log.debug "now ====> ${now}"
|
|
|
|
debugEvent ("Last Poll Millis = ${atomicState.lastPollMillis}")
|
|
def last = atomicState.lastPollMillis ?: 0
|
|
def next = last + pollRateMillis
|
|
|
|
log.debug "pollChild( ${child.device.deviceNetworkId} ): $now > $next ?? w/ current state: ${atomicState.thermostats}"
|
|
debugEvent ("pollChild( ${child.device.deviceNetworkId} ): $now > $next ?? w/ current state: ${atomicState.thermostats}")
|
|
|
|
// if( now > next )
|
|
if( true ) // for now let's always poll/refresh
|
|
{
|
|
log.debug "polling children because $now > $next"
|
|
debugEvent("polling children because $now > $next")
|
|
|
|
pollChildren()
|
|
|
|
log.debug "polled children and looking for ${child.device.deviceNetworkId} from ${atomicState.thermostats}"
|
|
debugEvent ("polled children and looking for ${child.device.deviceNetworkId} from ${atomicState.thermostats}")
|
|
|
|
def currentTime = new Date().time
|
|
debugEvent ("Current Time = ${currentTime}")
|
|
atomicState.lastPollMillis = currentTime
|
|
|
|
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
|
|
|
if(!tData)
|
|
{
|
|
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
|
|
|
// TODO: flag device as in error state
|
|
// child.errorState = true
|
|
|
|
return null
|
|
}
|
|
|
|
tData.updated = currentTime
|
|
|
|
return tData.data
|
|
}
|
|
else if(atomicState.thermostats[child.device.deviceNetworkId] != null)
|
|
{
|
|
log.debug "not polling children, found child ${child.device.deviceNetworkId} "
|
|
|
|
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
|
if(!tData.updated)
|
|
{
|
|
// we have pulled new data for this thermostat, but it has not asked us for it
|
|
// track it and return the data
|
|
tData.updated = new Date().time
|
|
return tData.data
|
|
}
|
|
return null
|
|
}
|
|
else if(atomicState.thermostats[child.device.deviceNetworkId] == null)
|
|
{
|
|
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}"
|
|
|
|
// TODO: flag device as in error state
|
|
// child.errorState = true
|
|
|
|
return null
|
|
}
|
|
else
|
|
{
|
|
// it's not time to poll again and this thermostat already has its latest values
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
def childRequest( child, subid, cmd, data )
|
|
{
|
|
getControlSend(subid, cmd, data)
|
|
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
|
|
|
if(!tData)
|
|
{
|
|
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
|
return null
|
|
}
|
|
tData.updated = currentTime
|
|
|
|
return tData.data
|
|
}
|
|
|
|
def getControlSend(subid, cmd, data)
|
|
{
|
|
log.trace "getParams"
|
|
|
|
def pollParams = [
|
|
uri: getServerUrl()+"/api/SmartTokApi?bcd="+getBCD()+"&mid=${state.mid}&did=36&subid=${subid}&cmd=${cmd}&data=${data}",
|
|
headers: ["Authorization": "Bearer ${state.authToken}"]
|
|
]
|
|
|
|
log.trace "Before Control HTTPGET to navien."
|
|
def jsonData
|
|
try{
|
|
httpGet(pollParams) { resp ->
|
|
debugEvent ("Response (resp.data.Staus) : = ${resp.data.Staus}", true)
|
|
if (resp.status == 200)
|
|
{
|
|
log.debug "poll results returned"
|
|
//atomicState.thermostats = resp.data.Status.inject([:]) { collector, stat ->
|
|
atomicState.thermostats = "1".inject([:]) { collector, stat ->
|
|
def dni = [getChildName()].join('.')
|
|
|
|
log.debug "updating dni $dni"
|
|
|
|
def response = statusSetting(resp.data.Status)
|
|
|
|
log.debug ("Event Data = ${response}")
|
|
|
|
collector[dni] = [data:response]
|
|
return collector
|
|
}
|
|
|
|
log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
|
|
}
|
|
else
|
|
{
|
|
log.error "polling children & got http status ${resp.status}"
|
|
|
|
//refresh the auth token
|
|
if(resp.status == 400)
|
|
{
|
|
log.debug "Bad Request Description"
|
|
}
|
|
else if(resp.status == 401)
|
|
{
|
|
log.debug "Unauthorized Description"
|
|
}
|
|
else if(resp.status == 500)
|
|
{
|
|
log.debug "InternalServerError Description"
|
|
atomicState.action = "pollChildren";
|
|
refreshAuthToken()
|
|
}
|
|
else
|
|
{
|
|
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch(all)
|
|
{
|
|
log.debug "___exception polling children: "
|
|
//refreshAuthToken()
|
|
}
|
|
}
|
|
|
|
private refreshAuthToken() {
|
|
log.trace "refreshing auth token"
|
|
|
|
if(!atomicState.refreshToken) {
|
|
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
|
|
} else {
|
|
def refreshParams = [
|
|
uri: getServerUrl(),
|
|
path: "/Token",
|
|
headers: ['Content-Type': "application/x-www-form-urlencoded"],
|
|
body: [grant_type: "refresh_token", refresh_token: "${state.refreshToken}"]
|
|
]
|
|
|
|
log.debug refreshParams
|
|
|
|
try {
|
|
def jsonMap
|
|
httpPost(refreshParams) { resp ->
|
|
|
|
if(resp.status == 200) {
|
|
log.debug "Token refreshed...calling saved RestAction now! ${resp}"
|
|
|
|
if(resp.data)
|
|
{
|
|
jsonMap = resp.data
|
|
state.refreshToken = jsonMap.refresh_token
|
|
state.authToken = jsonMap.access_token
|
|
|
|
if(atomicState.action && atomicState.action != "") {
|
|
log.debug "Executing next action: ${atomicState.action}"
|
|
"{atomicState.action}"()
|
|
|
|
//remove saved action
|
|
atomicState.action = ""
|
|
}
|
|
}
|
|
atomicState.action = ""
|
|
}
|
|
else
|
|
{
|
|
log.debug "refresh failed ${resp.status} : ${resp.status.code}"
|
|
}
|
|
}
|
|
}
|
|
catch(Exception e) {
|
|
log.debug "caught exception refreshing auth token: " + e.getStackTrace()
|
|
}
|
|
}
|
|
}
|
|
|
|
def statusSetting(status){
|
|
/*
|
|
log.debug "state.status ====> ${state.status}"
|
|
log.debug "제품아이디 ====> ${state.status.substring(0, 16)}" // 제품아이디
|
|
log.debug "보일러모델타입 ====> ${state.status.substring(26, 28)}" // 보일러모델타입
|
|
log.debug "에러코드 ====> ${state.status.substring(32, 36)}" // 에러코드
|
|
log.debug "온수설정온도 ====> ${state.status.substring(36, 38)}" // 온수설정온도
|
|
log.debug "state.status ====> ${state.status.substring(38, 40)}" // 난방세기
|
|
log.debug "state.status ====> ${state.status.substring(40, 42)}" // 옵션기능
|
|
log.debug "작동모드 ====> ${state.status.substring(42, 44)}" // 작동모드
|
|
log.debug "현재실내온도 ====> ${state.status.substring(44, 46)}" // 현재실내온도
|
|
log.debug "실내난방설정온도 ====> ${state.status.substring(46, 48)}" // 실내난방설정온도
|
|
log.debug "온돌난방설정온도 ====> ${state.status.substring(48, 50)}" // 온돌난방설정온도
|
|
*/
|
|
state.mid = status.substring(0, 16)
|
|
state.boilerType = status.substring(26, 28)
|
|
state.errorCode = status.substring(32, 36)
|
|
state.hotWater = convertHexToInt(status.substring(36, 38))
|
|
|
|
def s = status.substring(42, 44)
|
|
if(s == "01") state.thermostatStatus = "전원 OFF"
|
|
else if(s == "02") state.thermostatStatus = "외출 ON"
|
|
else if(s == "03") state.thermostatStatus = "실내난방"
|
|
else if(s == "04") state.thermostatStatus = "온돌난방"
|
|
else if(s == "05") state.thermostatStatus = "반복예약난방"
|
|
else if(s == "06") state.thermostatStatus = "24시간예약난방"
|
|
else if(s == "07") state.thermostatStatus = "간편예약난방"
|
|
else if(s == "08") state.thermostatStatus = "온수전용"
|
|
else if(s == "09") state.thermostatStatus = "빠른온수"
|
|
else state.thermostatStatus = "---"
|
|
|
|
state.temperature = convertHexToInt(status.substring(44, 46))
|
|
state.roomTemp = convertHexToInt(status.substring(46, 48))
|
|
state.ondolTemp = convertHexToInt(status.substring(48, 50))
|
|
|
|
def data = [
|
|
mid: state.mid,
|
|
boilerType: state.boilerType,
|
|
errorCode: state.errorCode,
|
|
hotWater: state.hotWater,
|
|
thermostatStatus: state.thermostatStatus,
|
|
temperature: state.temperature,
|
|
roomTemp: state.roomTemp,
|
|
ondolTemp: state.ondolTemp
|
|
]
|
|
return data
|
|
}
|
|
|
|
def navienConnecting(){
|
|
log.debug "navienConnecting()"
|
|
|
|
def connectParams = [
|
|
uri: getServerUrl()+"/api/SmartTokApi?bcd="+getBCD()+"&uid=${state.userName}&scd=2",
|
|
headers: ["Authorization": "Bearer ${state.authToken}"]
|
|
]
|
|
|
|
def result = [success:false]
|
|
def jsonData
|
|
|
|
httpGet(connectParams) { resp ->
|
|
if (resp.status == 200)
|
|
{
|
|
jsonData = resp.data
|
|
result.success = true
|
|
state.status = jsonData.Status
|
|
}
|
|
else if(resp.status == 400)
|
|
{
|
|
result.reason = "Bad Request"
|
|
}
|
|
else if(resp.status == 401)
|
|
{
|
|
result.reason = "Unauthorized"
|
|
}
|
|
else if(resp.status == 500)
|
|
{
|
|
result.reason = "Internal ServerError"
|
|
}
|
|
else
|
|
{
|
|
result.reason = "Bad Connect"
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
private forceLogin(){
|
|
log.debug "forceLogin()"
|
|
|
|
updateCookie(null)
|
|
login()
|
|
}
|
|
|
|
private updateCookie(String cookie){
|
|
atomicState.cookie = cookie
|
|
state.cookie = cookie
|
|
}
|
|
|
|
private login(){
|
|
if(getCookieValueIsValid())
|
|
{
|
|
return [success:true]
|
|
}
|
|
return doLogin()
|
|
}
|
|
|
|
private doLogin() {
|
|
log.debug "doLogin()"
|
|
|
|
def loginParams = [
|
|
uri: getServerUrl(),
|
|
path: "/Token",
|
|
headers: ['Content-Type': "application/x-www-form-urlencoded"],
|
|
body: [grant_type: "password", username: username, password: password]
|
|
]
|
|
|
|
def result = [success:false]
|
|
def jsonMap
|
|
|
|
try
|
|
{
|
|
httpPost(loginParams) { resp ->
|
|
if (resp.status == 200 && resp.headers.'Content-Type'.contains("application/json"))
|
|
{
|
|
log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
|
|
def cookie = resp?.headers?.'Set-Cookie'?.split(";")?.getAt(0)
|
|
if (cookie) {
|
|
log.debug "login setting cookie to $cookie"
|
|
updateCookie(cookie)
|
|
result.success = true
|
|
|
|
jsonMap = resp.data
|
|
state.refreshToken = jsonMap.refresh_token
|
|
state.authToken = jsonMap.access_token
|
|
state.userName = jsonMap.userName
|
|
}
|
|
else
|
|
{
|
|
// ERROR: any more information we can give?
|
|
result.reason = "Bad login"
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result.reason = "Bad login"
|
|
}
|
|
}
|
|
}
|
|
catch(groovyx.net.http.HttpResponseException hre)
|
|
{
|
|
result.reason = "Exception"
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
private Boolean getCookieValueIsValid()
|
|
{
|
|
// TODO: make a call with the cookie to verify that it works
|
|
return getCookieValue()
|
|
}
|
|
|
|
private getCookieValue(){
|
|
state.cookie
|
|
}
|
|
|
|
def getChildNamespace() { "smartthings" }
|
|
def getChildName() { "Navien Room Controller" }
|
|
|
|
def getChildDeviceIdsString()
|
|
{
|
|
log.debug "thermostats ====> ${thermostats}"
|
|
return thermostats.collect { it.split(/\./).last() }.join(',')
|
|
}
|
|
|
|
def getServerUrl() { return appSettings.serverUrl }
|
|
def getSmartThingsClientId() { return appSettings.clientId }
|
|
def getBCD() { return appSettings.bcd }
|
|
|
|
def debugEvent(message, displayEvent = true) {
|
|
|
|
def results = [
|
|
name: "appdebug",
|
|
descriptionText: message,
|
|
displayed: displayEvent
|
|
]
|
|
log.debug "Generating AppDebug Event: ${results}"
|
|
sendEvent (results)
|
|
}
|
|
|
|
private convertHexToInt(hex) {
|
|
return (Integer.parseInt(hex,16) / 2)
|
|
} |