Compare commits

..

5 Commits

Author SHA1 Message Date
Sung Hoon Kim
b363725165 MSA-860: test 2016-02-02 04:21:53 -06:00
Kristofer Schaller
3b89368d45 Merge pull request #467 from davidsulpy/master
New Initial State Event Streamer: removed buffering and sched-tasks
2016-01-25 13:08:51 -08:00
David Sulpy
12f6039de5 New Initial State Event Streamer: removed buffering and scheduled tasks for flushing buffer 2016-01-22 19:23:37 -06:00
Vinay Rao
65c9da32e7 Merge pull request #464 from workingmonk/smartpower_v1
[DVCSMP-1430] updating fingerprint for the problem device
2016-01-21 23:29:43 -08:00
Vinay Rao
7147770e2d updating fingerprint for the problem device 2016-01-21 18:20:03 -08:00
4 changed files with 71 additions and 1109 deletions

View File

@@ -1,348 +0,0 @@
/**
* 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}"
}

View File

@@ -5,7 +5,7 @@ metadata {
capability "Switch"
capability "Sensor"
fingerprint profileId: "0104", inClusters: "0000,0003,0006", outClusters: "0019"
fingerprint profileId: "0104", inClusters: "0006, 0004, 0003, 0000, 0005", outClusters: "0019", manufacturer: "Compacta International, Ltd", model: "ZBMPlug15", deviceJoinName: "SmartPower Outlet V1"
}
// simulator metadata

View File

@@ -1,7 +1,7 @@
/**
* Initial State Event Streamer
*
* Copyright 2015 David Sulpy
* Copyright 2016 David Sulpy
*
* 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:
@@ -77,6 +77,62 @@ mappings {
}
}
def getAccessKey() {
log.trace "get access key"
if (atomicState.accessKey == null) {
httpError(404, "Access Key Not Found")
} else {
[
accessKey: atomicState.accessKey
]
}
}
def getBucketKey() {
log.trace "get bucket key"
if (atomicState.bucketKey == null) {
httpError(404, "Bucket key Not Found")
} else {
[
bucketKey: atomicState.bucketKey,
bucketName: atomicState.bucketName
]
}
}
def setBucketKey() {
log.trace "set bucket key"
def newBucketKey = request.JSON?.bucketKey
def newBucketName = request.JSON?.bucketName
log.debug "bucket name: $newBucketName"
log.debug "bucket key: $newBucketKey"
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
atomicState.bucketKey = "$newBucketKey"
atomicState.bucketName = "$newBucketName"
atomicState.isBucketCreated = false
}
tryCreateBucket()
}
def setAccessKey() {
log.trace "set access key"
def newAccessKey = request.JSON?.accessKey
def newGrokerSubdomain = request.JSON?.grokerSubdomain
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
atomicState.grokerSubdomain = "$newGrokerSubdomain"
atomicState.isBucketCreated = false
}
if (newAccessKey && newAccessKey != atomicState.accessKey) {
atomicState.accessKey = "$newAccessKey"
atomicState.isBucketCreated = false
}
}
def subscribeToEvents() {
if (accelerometers != null) {
subscribe(accelerometers, "acceleration", genericHandler)
@@ -169,85 +225,27 @@ def subscribeToEvents() {
}
}
def getAccessKey() {
log.trace "get access key"
if (atomicState.accessKey == null) {
httpError(404, "Access Key Not Found")
} else {
[
accessKey: atomicState.accessKey
]
}
}
def getBucketKey() {
log.trace "get bucket key"
if (atomicState.bucketKey == null) {
httpError(404, "Bucket key Not Found")
} else {
[
bucketKey: atomicState.bucketKey,
bucketName: atomicState.bucketName
]
}
}
def setBucketKey() {
log.trace "set bucket key"
def newBucketKey = request.JSON?.bucketKey
def newBucketName = request.JSON?.bucketName
log.debug "bucket name: $newBucketName"
log.debug "bucket key: $newBucketKey"
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
atomicState.bucketKey = "$newBucketKey"
atomicState.bucketName = "$newBucketName"
atomicState.isBucketCreated = false
}
tryCreateBucket()
}
def setAccessKey() {
log.trace "set access key"
def newAccessKey = request.JSON?.accessKey
def newGrokerSubdomain = request.JSON?.grokerSubdomain
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
atomicState.grokerSubdomain = "$newGrokerSubdomain"
atomicState.isBucketCreated = false
}
if (newAccessKey && newAccessKey != atomicState.accessKey) {
atomicState.accessKey = "$newAccessKey"
atomicState.isBucketCreated = false
}
}
def installed() {
atomicState.version = "1.0.18"
atomicState.version = "1.1.0"
atomicState.isBucketCreated = false
atomicState.grokerSubdomain = "groker"
subscribeToEvents()
atomicState.isBucketCreated = false
atomicState.grokerSubdomain = "groker"
atomicState.eventBuffer = []
runEvery15Minutes(flushBuffer)
log.debug "installed (version $atomicState.version)"
}
def updated() {
atomicState.version = "1.0.18"
atomicState.version = "1.1.0"
unsubscribe()
if (atomicState.bucketKey != null && atomicState.accessKey != null) {
atomicState.isBucketCreated = false
}
if (atomicState.eventBuffer == null) {
atomicState.eventBuffer = []
}
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
atomicState.grokerSubdomain = "groker"
}
@@ -327,37 +325,17 @@ def genericHandler(evt) {
eventHandler(key, value)
}
// This is a handler function for flushing the event buffer
// after a specified amount of time to reduce the load on ST servers
def flushBuffer() {
def eventBuffer = atomicState.eventBuffer
log.trace "About to flush the buffer on schedule"
if (eventBuffer != null && eventBuffer.size() > 0) {
atomicState.eventBuffer = []
tryShipEvents(eventBuffer)
}
}
def eventHandler(name, value) {
def epoch = now() / 1000
def eventBuffer = atomicState.eventBuffer ?: []
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
if (eventBuffer.size() >= 10) {
// Clear eventBuffer right away since we've already pulled it off of atomicState to reduce the risk of missing
// events. This assumes the grokerSubdomain, accessKey, and bucketKey are set correctly to avoid the eventBuffer
// from growing unbounded.
atomicState.eventBuffer = []
tryShipEvents(eventBuffer)
} else {
// Make sure we persist the updated eventBuffer with the new event added back to atomicState
atomicState.eventBuffer = eventBuffer
}
log.debug "Event added to buffer: " + eventBuffer
def event = new JsonSlurper().parseText("{\"key\": \"$name\", \"value\": \"$value\", \"epoch\": \"$epoch\"}")
tryShipEvents(event)
log.debug "Shipped Event: " + event
}
// a helper function for shipping the atomicState.eventBuffer to Initial State
def tryShipEvents(eventBuffer) {
def tryShipEvents(event) {
def grokerSubdomain = atomicState.grokerSubdomain
// can't ship events if there is no grokerSubdomain
@@ -380,7 +358,7 @@ def tryShipEvents(eventBuffer) {
"X-IS-AccessKey": "${accessKey}",
"Accept-Version": "0.0.2"
],
body: eventBuffer
body: event
]
try {

View File

@@ -1,668 +0,0 @@
/**
* 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)
}