diff --git a/devicetypes/smartthings/navien-room-controller.src/navien-room-controller.groovy b/devicetypes/smartthings/navien-room-controller.src/navien-room-controller.groovy new file mode 100644 index 0000000..5774fb8 --- /dev/null +++ b/devicetypes/smartthings/navien-room-controller.src/navien-room-controller.groovy @@ -0,0 +1,348 @@ +/** + * Navien Thermostat + * + * Author: Navien Within + * Date: 2015-11-02 + */ +metadata { + definition (name: "Navien Room Controller", namespace: "smartthings", author: "Navien") { + capability "Thermostat" + capability "Polling" + capability "Refresh" + + command "generateEvent" + command "powerONOFF" + command "setRoomTemp" + command "setOndolTemp" + command "controlMode1" + command "controlMode2" + + attribute "setRoomSlider", "NUMBER" + attribute "setOndolSlider", "NUMBER" + } + + // simulator metadata + simulator { } + + //tiles(scale: 2) { + tiles { + /* + multiAttributeTile(name: "temperature", type: "thermostat", canChangeIcon: true, canChangeBackground: true) { + tileAttribute("temperature", key: "PRIMARY_CONTROL") { + attributeState "temperature", label: '${currentValue}°', icon:"st.Home.home1", backgroundColor:"#FFA81E" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label: 'statusText' + } + tileAttribute("button", key: "VALUE_CONTROL"){ + attributeState "setUp" + attributeState "setDown" + } + } + */ + + valueTile("thermostatStatus", "device.thermostatStatus", width: 3, height: 2) { + state "전원 OFF", label:'${currentValue}', icon:"st.Navien.bgs_power_off", backgroundColor:"#BDBDBD" + state "외출 ON", label:'${currentValue}', icon:"st.Navien.bgs_out", backgroundColor:"#FF8C17" + state "실내난방", label:'${currentValue}', icon:"st.Navien.bgs_indoor", backgroundColor:"#FF8C17" + state "온돌난방", label:'${currentValue}', icon:"st.Navien.bgs_ondol", backgroundColor:"#FF8C17" + state "반복예약난방", label:'${currentValue}', icon:"st.Navien.bgs_heating_again", backgroundColor:"#FF8C17" + state "24시간예약난방", label:'${currentValue}', icon:"st.Navien.bgs_24heat", backgroundColor:"#FF8C17" + state "간편예약난방", label:'${currentValue}', icon:"st.Navien.bgs_heat", backgroundColor:"#FF8C17" + state "온수전용", label:'${currentValue}', icon:"st.Navien.bgs_water", backgroundColor:"#FF8C17" + state "빠른온수", label:'${currentValue}', icon:"st.Navien.bgs_water_fast", backgroundColor:"#FF8C17" + } + + valueTile("temperature", "device.temperature", width: 1, height: 1, inactiveLabel: false) { + state "OFF", label:'', unit:"C", icon:"st.Navien.bg_recent_off" + state "default", label:'${currentValue}°', unit:"C", icon:"st.Navien.bg_recent" + } + + valueTile("hotWater", "device.hotWater", width: 1, height: 1) { + state "OFF", label:'', unit:"C", icon:"st.Navien.bg_water_off" + state "default", label:'${currentValue}°', unit:"C", icon:"st.Navien.bg_water" + } + + standardTile("refresh", "device.refresh", , width: 1, height: 1, inactiveLabel: false) { + state "default", action:"refresh.refresh", icon:"st.Navien.but_refresh" + } + + valueTile("setRoomTemp", "device.setRoomTemp", width: 1, height:1) { + state "OFF", label:'', icon:"st.Navien.bg_indoor_off" + state "default", label:'${currentValue}°', icon:"st.Navien.bg_indoor" + } + + controlTile("setRoomSlider", "device.setRoomSlider", "slider", height: 1, width: 2, inactiveLabel: false, range:"(10..40)") { + state "default", action:"setRoomTemp", backgroundColor:"#F08C00" + } + + valueTile("setOndolTemp", "device.setOndolTemp", width: 1, height:1) { + state "OFF", label:'', icon:"st.Navien.bg_ondol" + state "default", label:'${currentValue}°', icon:"st.Navien.bg_ondol_off" + } + + controlTile("setOndolSlider", "device.setOndolSlider", "slider", height: 1, width: 2, inactiveLabel: false, range:"(40..83)") { + state "default", action:"setOndolTemp", backgroundColor:"#F08C00" + } + + standardTile("power", "device.power", width: 1, height: 1, inactiveLabel: false) { + state "ON", label:'', action:"powerONOFF", icon:"st.Navien.but_power" + state "default", label:'', action:"powerONOFF", icon:"st.Navien.but_power_off" + } + + standardTile("controlMode1", "device.controlMode1", width: 1, height: 1, inactiveLabel: false) { + state "실내난방 ON", label:'', action:"controlMode1", icon:"st.Navien.but_indoor" + state "온돌난방 ON", label:'', action:"controlMode1", icon:"st.Navien.but_ondol" + state "default", label:'', icon:"st.Navien.but_indoor_off" + } + + standardTile("controlMode2", "device.controlMode2", width: 1, height: 1, inactiveLabel: false) { + state "외출해제", label:'', action:"controlMode2", icon:"st.Navien.but_out_off" + state "외출설정", label:'', action:"controlMode2", icon:"st.Navien.but_out" + state "default", label:'', icon:"st.Navien.but_out_dis" + } + + valueTile("herotile", "device.herotile", width: 3, height:1) { + state "default", label:'', icon:"st.Navien.bg_herotile" + } + + main "thermostatStatus" + details(["thermostatStatus", "temperature", "hotWater", "refresh", "setRoomTemp", "setRoomSlider", "setOndolTemp", "setOndolSlider", "power", "controlMode1", "controlMode2", "herotile"]) + } +} + +def powerONOFF() +{ + log.debug "powerONOFF called" + def powerStatus = device.currentValue("power") + def results + if(powerStatus == "ON") + { + results = parent.childRequest(this, "1", "33", "0") + } + else if(powerStatus == "전원 OFF") + { + results = parent.childRequest(this, "1", "34", "0") + } + generateEvent(results) + log.debug "powerONOFF ended" +} + +def setRoomTemp(degrees) +{ + def degreesInteger = degrees as Integer + sendEvent("name":"setRoomSlider", "value":degreesInteger) + + def status = device.currentValue("thermostatStatus") + if(status == "실내난방") + { + def results = parent.childRequest(this, "1", "44", degreesInteger*2) + generateEvent(results) + } +} + +def setOndolTemp(degrees) +{ + def degreesInteger = degrees as Integer + sendEvent("name":"setOndolSlider", "value":degreesInteger) + + def status = device.currentValue("thermostatStatus") + if(status == "온돌난방") + { + def results = parent.childRequest(this, "1", "36", degreesInteger*2) + generateEvent(results) + } +} + +def controlMode1() +{ + log.debug "controlMode1" + def control = device.currentValue("controlMode1") + controlMode(control) +} + +def controlMode2() +{ + log.debug "controlMode2" + def control = device.currentValue("controlMode2") + controlMode(control) +} + +def controlMode(control) +{ + if(control == "실내난방 ON") + { + log.debug "실내난방 ON : 제어" + def value = device.currentValue("setRoomSlider")*2 + def results = parent.childRequest(this, "1", "43", value) + generateEvent(results) + + } + else if(control == "온돌난방 ON") + { + log.debug "온돌난방 ON : 제어" + def value = device.currentValue("setOndolSlider")*2 + def results = parent.childRequest(this, "1", "35", value) + generateEvent(results) + + } + else if(control == "외출해제") + { + log.debug "외출해제 : 제어" + def results = parent.childRequest(this, "1", "46", "0") + generateEvent(results) + + } + else if(control == "외출설정") + { + log.debug "외출설정 : 제어" + def results = parent.childRequest(this, "1", "46", "1") + generateEvent(results) + } +} + +def refresh() +{ + log.debug "refresh called" + poll() + log.debug "refresh ended" +} + +void poll() { + log.debug "Executing 'poll' using parent SmartApp" + + def results = parent.pollChild(this) + generateEvent(results) +} + +void generateEvent(Map results) +{ + log.debug "parsing data $results" + + if(results) + { + results.each { name, value -> + def linkText = getLinkText(device) + def isChange = true + def isDisplayed = true + + if (name=="temperature" || name=="hotWater") + { + //isChange = isTemperatureStateChange(device, name, value.toString()) + isDisplayed = isChange + + sendEvent( + name: name, + value: value, + unit: "C", + linkText: linkText, + descriptionText: getThermostatDescriptionText(name, value, linkText), + handlerName: name, + isStateChange: isChange, + displayed: isDisplayed) + } + else if (name=="thermostatStatus") + { + isChange = isStateChange(device, name, value.toString()) + isDisplayed = isChange + + sendEvent( + name: name, + value: value.toString(), + linkText: linkText, + descriptionText: getThermostatDescriptionText(name, value, linkText), + handlerName: name, + isStateChange: isChange, + displayed: isDisplayed) + } + } + generateStatusEvent(results) + } +} + +private getThermostatDescriptionText(name, value, linkText) +{ + if(name == "temperature") + { + return "$linkText was $value°C" + } + else if(name == "roomTemp") + { + return "latest roomTemp setpoint was $value°C" + } + else if(name == "ondolTemp") + { + return "latest ondolTemp setpoint was $value°C" + } + else if (name == "thermostatMode") + { + return "thermostat mode is ${value}" + } + else + { + return "${name} = ${value}" + } +} + +def generateStatusEvent(Map results) { + log.debug "generateStatusEvent" + + def status = results.thermostatStatus + def setRoomTemp = results.roomTemp + def setOndolTemp = results.ondolTemp + + log.debug "status ===> ${status}" + + if (status == "전원 OFF") { + sendEvent("name":"temperature", "value":"OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"hotWater", "value":"OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"setRoomTemp", "value":"OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"setRoomSlider", "value":"0", displayed: true, isStateChange: true) + sendEvent("name":"setOndolTemp", "value":"OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"setOndolSlider", "value":"0", displayed: true, isStateChange: true) + sendEvent("name":"power", "value":"ON", "description":"전원 ON", displayed: true, isStateChange: true) + sendEvent("name":"controlMode1", "value":"OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"controlMode2", "value":"OFF", "description":"OFF", displayed: true, isStateChange: true) + + + }else if(status == "온수전용"){ + sendEvent("name":"setRoomTemp", "value":"OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"setRoomSlider", "value":"0", displayed: true, isStateChange: true) + sendEvent("name":"setOndolTemp", "value":"OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"setOndolSlider", "value":"0", displayed: true, isStateChange: true) + sendEvent("name":"power", "value":"전원 OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"controlMode1", "value":"실내 ON", "description":"ON", displayed: true, isStateChange: true) + sendEvent("name":"controlMode2", "value":"온돌 ON", "description":"ON", displayed: true, isStateChange: true) + + }else if(status == "외출 ON"){ + sendEvent("name":"setRoomTemp", "value":"OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"setRoomSlider", "value":"0", displayed: true, isStateChange: true) + sendEvent("name":"setOndolTemp", "value":"OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"setOndolSlider", "value":"0", displayed: true, isStateChange: true) + sendEvent("name":"power", "value":"전원 OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"controlMode1", "value":"OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"controlMode2", "value":"외출해제", "description":"외출해제", displayed: true, isStateChange: true) + + }else if(status == "실내난방"){ + sendEvent("name":"setRoomTemp", "value":"${setRoomTemp}", "description":"설정온도", displayed: true, isStateChange: true) + sendEvent("name":"setRoomSlider", "value":setRoomTemp, displayed: true, isStateChange: true) + sendEvent("name":"setOndolTemp", "value":"${setOndolTemp}", "description":"설정온도", displayed: true, isStateChange: true) + sendEvent("name":"setOndolSlider", "value":setOndolTemp, displayed: true, isStateChange: true) + sendEvent("name":"power", "value":"전원 OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"controlMode1", "value":"온돌난방 ON", "description":"온돌난방 ON", displayed: true, isStateChange: true) + sendEvent("name":"controlMode2", "value":"외출설정", "description":"외출설정", displayed: true, isStateChange: true) + + } + else if(status == "온돌난방"){ + sendEvent("name":"setRoomTemp", "value":"${setRoomTemp}", "description":"설정온도", displayed: true, isStateChange: true) + sendEvent("name":"setRoomSlider", "value":setRoomTemp, displayed: true, isStateChange: true) + sendEvent("name":"setOndolTemp", "value":"${setOndolTemp}", "description":"설정온도", displayed: true, isStateChange: true) + sendEvent("name":"setOndolSlider", "value":setOndolTemp, displayed: true, isStateChange: true) + sendEvent("name":"power", "value":"전원 OFF", "description":"OFF", displayed: true, isStateChange: true) + sendEvent("name":"controlMode1", "value":"실내난방 ON", "description":"실내난방 ON", displayed: true, isStateChange: true) + sendEvent("name":"controlMode2", "value":"외출설정", "description":"외출설정", displayed: true, isStateChange: true) + + } + + log.debug "Generate Status Event = ${status}" +} \ No newline at end of file diff --git a/smartapps/smartthings/-.src/-.groovy b/smartapps/smartthings/-.src/-.groovy new file mode 100644 index 0000000..e50a48d --- /dev/null +++ b/smartapps/smartthings/-.src/-.groovy @@ -0,0 +1,668 @@ +/** + * 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) +} \ No newline at end of file