Compare commits

...

18 Commits

Author SHA1 Message Date
Ko Sang Ju
8570c84d37 MSA-835: Navien Room Controller 2016-01-21 23:10:55 -06:00
Jason Botello
5eb33eca19 Merge pull request #387 from SmartThingsCommunity/MSA-757-1
MSA-757: Timevalve Smart
2016-01-20 16:25:05 -08:00
Jason Botello
ec321ce85d Merge pull request #425 from hhhss91/MSA-786-1
MSA-786: Encored Technologies : Smart Energy Service
2016-01-20 16:24:46 -08:00
Jason Botello
7669bec0bc Update timevalve-smart.groovy
Commenting out fingerprint temporarily to avoid potential conflicts with other devices as this devices is specifically for a Korean deployment in AP01 - see DVCSMP-1425
2016-01-20 16:17:32 -08:00
Steve Vlaminck
a20a58bd48 Merge pull request #458 from vlaminck/gentle-wake-up-controller
Added Gentle Wake Up controller, updated SmartApp
2016-01-20 14:03:12 -06:00
vlaminck
efabd07dea Added Gentle Wake Up controller, updated SmartApp 2016-01-20 09:05:47 -06:00
Tyler Lange
1e55f62048 Merge pull request #454 from workingmonk/deprecate_osram_flex
deprecating this DTH as the fingerprints have been moved to ZigBee RGBW
2016-01-19 16:06:54 -08:00
Vinay Rao
12b18eae08 deprecating this DTH as the fingerprints have been moved to ZigBee RGBW Bulb 2016-01-19 16:02:35 -08:00
Tyler Lange
aad82e10ef Merge pull request #451 from workingmonk/zigbee_color_bulb
Adding ZigBee RGBW DTH - generic ZigBee DTH for OSRAM Flex Strip, OSRAM A19 RGBW, OSRAM BR30 RGBW, OSRAM RT5/6
2016-01-19 15:55:07 -08:00
Vinay Rao
b11b362c60 Adding ZigBee RGBW DTH 2016-01-19 15:40:43 -08:00
Tom Manley
e1853b8e50 Merge pull request #443 from tpmanley/feature/sensor_updates
Feature/sensor updates
2016-01-19 13:43:40 -06:00
Tom Manley
968834e33e Use table for battery voltage to percent remaining calculation
The new table based approach yields a more accurate battery percentage
remaining than the old linear calculation.

Resolves:
    https://smartthings.atlassian.net/browse/SMJN-39
2016-01-19 13:40:02 -06:00
Tom Manley
e51a38eb28 Fix whitespace issues - no code changes
Replaced spaces with tabs for indentation and removed some unnecessary
white space.
2016-01-18 11:00:37 -06:00
Tom Manley
553b45a3f3 Merge pull request #441 from tpmanley/bugfix/arrival_readattr_events
arrival: Keep 'read attr' messages from being turned into events
2016-01-15 09:51:39 -06:00
Tom Manley
d9ab3bca00 arrival: Keep 'read attr' messages from being turned into events
Previously parse was returning null which causes the platform to create an event
using the message passed to parse. We don't want that to happen so return
an empty list instead.

Resolves:
    https://smartthings.atlassian.net/browse/SMJN-38
2016-01-14 15:34:08 -06:00
Lee Joonmin
3a7abd6169 Modifying 'Timevalve Smart' 2016-01-12 18:01:23 -06:00
hyeon seok yang
c20026b376 MSA-786: Description from the app : "With visible realtime energy usage status, have good energy habits and enrich your life"
This app is specialized to show energy data which was grabbed from Encored Technologies' device that user installed at their home.
2016-01-08 16:01:05 +09:00
Lee Joonmin
7def620f04 MSA-757: Smart Gas-Lock work with z-wave 2015-12-23 03:44:19 -06:00
13 changed files with 3790 additions and 472 deletions

View File

@@ -0,0 +1,104 @@
/**
* EnerTalk Energy Meter
*
* Copyright 2015 hyeon seok yang
*
* 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.
*
*/
metadata {
definition (name: "EnerTalk Energy Meter", namespace: "Encored Technologies", author: "hyeon seok yang") {
}
simulator {
// TODO: define status and reply messages here
}
tiles(scale:2) {
valueTile("view", "device.view", decoration: "flat") {
state "view", label:' ${currentValue} kWh'
}
valueTile("month", "device.month", width: 6, height : 3, decoration: "flat") {
state "month", label:' ${currentValue}'
}
valueTile("real", "device.real", width: 2, height : 2, decoration: "flat") {
state "real", label:' ${currentValue}'
}
valueTile("tier", "device.tier", width: 2, height : 2, decoration: "flat") {
state "tier", label:' ${currentValue}'
}
valueTile("plan", "device.plan", width: 2, height : 2, decoration: "flat") {
state "plan", label:' ${currentValue}'
}
htmlTile(name:"deepLink", action:"linkApp", whitelist:["code.jquery.com",
"ajax.googleapis.com",
"fonts.googleapis.com",
"code.highcharts.com",
"enertalk-card.encoredtech.com",
"s3-ap-northeast-1.amazonaws.com",
"s3.amazonaws.com",
"ui-hub.encoredtech.com",
"enertalk-auth.encoredtech.com",
"api.encoredtech.com",
"cdnjs.cloudflare.com",
"encoredtech.com",
"itunes.apple.com"], width:2, height:2){}
main (["view"])
details (["month", "real", "tier", "plan", "deepLink"])
}
}
mappings {
path("/linkApp") {action: [ GET: "getLinkedApp" ]}
}
def getLinkedApp() {
def lang = clientLocale?.language
if ("${lang}" == "ko") {
lang = "<p style=\'margin-left:15vw; color: #aeaeb0;\'>기기 설정</p>"
} else {
lang = "<p style=\'margin-left:5vw; color: #aeaeb0;\'>Setup Device</p>"
}
renderHTML() {
head {
"""
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, height=device-height">
<style>
#레이어_1 { margin-left : 17vw; width : 50vw; height : 50vw;}
.st0{fill:#B5B6BB;}
</style>
"""
}
body {
"""
<div id="container">
<a id="st-deep-link" href="#">
<svg version="1.1" id="레이어_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve"><path class="st0" d="M20,0C9,0,0,9,0,20C0,30.5,8,39,18.2,40l3.8-4.8l-3.9-4.8c-4.9-0.9-8.6-5.2-8.6-10.4c0-5.8,4.7-10.5,10.5-10.5
S30.5,14.2,30.5,20c0,5.1-3.7,9.4-8.5,10.3l3.7,4.5L21.8,40C32,39.1,40,30.5,40,20C40,9,31,0,20,0z"/></svg>
</a>
${lang}
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
var ua = navigator.userAgent.toLowerCase();
var isAndroid = ua.indexOf("android") > -1;
if(!isAndroid) {
\$("#st-deep-link").attr("href", "https://itunes.apple.com/kr/app/enertalk-for-home/id1024660780?mt=8");
} else {
\$("#st-deep-link").attr("href", "market://details?id=com.ionicframework.enertalkhome874425");
}
</script>
"""
}
}
}

View File

@@ -76,6 +76,8 @@ def parse(String description) {
if (description?.startsWith('read attr -')) {
handleReportAttributeMessage(description)
}
return []
}
private handleReportAttributeMessage(String description) {
@@ -86,31 +88,37 @@ private handleReportAttributeMessage(String description) {
}
}
private handleBatteryEvent(rawValue) {
def linkText = getLinkText(device)
def eventMap = [
name: 'battery',
value: '--'
]
def volts = rawValue / 10
if (volts > 0){
def minVolts = 2.0
def maxVolts = 2.8
/**
* Create battery event from reported battery voltage.
*
* @param volts Battery voltage in .1V increments
*/
private handleBatteryEvent(volts) {
if (volts == 0 || volts == 255) {
log.debug "Ignoring invalid value for voltage (${volts/10}V)"
}
else {
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
def minVolts = 15
def maxVolts = 28
if (volts < minVolts)
volts = minVolts
else if (volts > maxVolts)
volts = maxVolts
def pct = (volts - minVolts) / (maxVolts - minVolts)
eventMap.value = Math.round(pct * 100)
eventMap.descriptionText = "${linkText} battery was ${eventMap.value}%"
def pct = batteryMap[volts]
if (pct != null) {
def linkText = getLinkText(device)
def eventMap = [
name: 'battery',
value: pct,
descriptionText: "${linkText} battery was ${pct}%"
]
log.debug "Creating battery event for voltage=${volts/10}V: ${eventMap}"
sendEvent(eventMap)
}
}
log.debug "Creating battery event: ${eventMap}"
sendEvent(eventMap)
}
private handlePresenceEvent(present) {

View File

@@ -0,0 +1,126 @@
/**
* Copyright 2016 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.
*
*/
metadata {
definition (name: "Gentle Wake Up Controller", namespace: "smartthings", author: "SmartThings") {
capability "Switch"
capability "Timed Session"
attribute "percentComplete", "number"
command "setPercentComplete", ["number"]
}
simulator {
// TODO: define status and reply messages here
}
tiles(scale: 2) {
multiAttributeTile(name: "richTile", type:"generic", width:6, height:4) {
tileAttribute("sessionStatus", key: "PRIMARY_CONTROL") {
attributeState "cancelled", action: "timed session.start", icon: "http://f.cl.ly/items/322n181j2K3f281r2s0A/playbutton.png", backgroundColor: "#ffffff", nextState: "running"
attributeState "stopped", action: "timed session.start", icon: "http://f.cl.ly/items/322n181j2K3f281r2s0A/playbutton.png", backgroundColor: "#ffffff", nextState: "cancelled"
attributeState "running", action: "timed session.stop", icon: "http://f.cl.ly/items/0B3y3p2V3X2l3P3y3W09/stopbutton.png", backgroundColor: "#79b821", nextState: "cancelled"
}
tileAttribute("timeRemaining", key: "SECONDARY_CONTROL") {
attributeState "timeRemaining", label:'${currentValue} remaining'
}
tileAttribute("percentComplete", key: "SLIDER_CONTROL") {
attributeState "percentComplete", action: "timed session.setTimeRemaining"
}
}
// start/stop
standardTile("sessionStatusTile", "sessionStatus", width: 1, height: 1, canChangeIcon: true) {
state "cancelled", label: "Stopped", action: "timed session.start", backgroundColor: "#ffffff", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
state "stopped", label: "Stopped", action: "timed session.start", backgroundColor: "#ffffff", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
state "running", label: "Running", action: "timed session.stop", backgroundColor: "#79b821", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
}
// duration
valueTile("timeRemainingTile", "timeRemaining", decoration: "flat", width: 2) {
state "timeRemaining", label:'${currentValue} left'
}
controlTile("percentCompleteTile", "percentComplete", "slider", height: 1, width: 3) {
state "percentComplete", action: "timed session.setTimeRemaining"
}
main "sessionStatusTile"
details "richTile"
// details(["richTile", "sessionStatusTile", "timeRemainingTile", "percentCompleteTile"])
}
}
// parse events into attributes
def parse(description) {
log.debug "Parsing '${description}'"
// TODO: handle 'switch' attribute
// TODO: handle 'level' attribute
// TODO: handle 'sessionStatus' attribute
// TODO: handle 'timeRemaining' attribute
}
// handle commands
def on() {
log.debug "Executing 'on'"
startDimming()
}
def off() {
log.debug "Executing 'off'"
stopDimming()
}
def setTimeRemaining(percentComplete) {
log.debug "Executing 'setTimeRemaining' to ${percentComplete}% complete"
parent.jumpTo(percentComplete)
}
def start() {
log.debug "Executing 'start'"
startDimming()
}
def stop() {
log.debug "Executing 'stop'"
stopDimming()
}
def pause() {
log.debug "Executing 'pause'"
// TODO: handle 'pause' command
}
def cancel() {
log.debug "Executing 'cancel'"
stopDimming()
}
def startDimming() {
log.trace "startDimming"
log.debug "parent: ${parent}"
parent.start("controller")
}
def stopDimming() {
log.trace "stopDimming"
log.debug "parent: ${parent}"
parent.stop("controller")
}
def controllerEvent(eventData) {
log.trace "controllerEvent"
sendEvent(eventData)
}

View File

@@ -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}"
}

View File

@@ -4,6 +4,7 @@
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
that issue by using state variables
*/
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
metadata {
definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") {
@@ -23,8 +24,8 @@ metadata {
command "setAdjustedColor"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW"
//fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW"
//fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW"
}

View File

@@ -20,8 +20,8 @@ metadata {
capability "Refresh"
capability "Temperature Measurement"
capability "Water Sensor"
command "enrollResponse"
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor"
@@ -29,9 +29,9 @@ metadata {
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "moisturev4", deviceJoinName: "Water Leak Sensor"
}
simulator {
}
preferences {
@@ -47,7 +47,7 @@ metadata {
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
}
tiles(scale: 2) {
multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){
tileAttribute ("device.water", key: "PRIMARY_CONTROL") {
@@ -78,7 +78,7 @@ metadata {
details(["water", "temperature", "battery", "refresh"])
}
}
def parse(String description) {
log.debug "description: $description"
@@ -92,59 +92,59 @@ def parse(String description) {
else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description)
}
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
log.debug "Parse returned $map"
def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
return result
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
return result
}
private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) {
case 0x0001:
resultMap = getBatteryResult(cluster.data.last())
break
Map resultMap = [:]
def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) {
case 0x0001:
resultMap = getBatteryResult(cluster.data.last())
break
case 0x0402:
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
break
}
}
case 0x0402:
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
break
}
}
return resultMap
return resultMap
}
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage
}
private Map parseReportAttributeMessage(String description) {
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
log.debug "Desc Map: $descMap"
Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value)
@@ -153,10 +153,10 @@ private Map parseReportAttributeMessage(String description) {
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
}
return resultMap
}
private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
@@ -167,42 +167,42 @@ private Map parseCustomMessage(String description) {
}
private Map parseIasMessage(String description) {
List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2]
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry
resultMap = getMoistureResult('dry')
break
List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2]
case '0x0021': // Open/Motion/Wet
resultMap = getMoistureResult('wet')
break
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry
resultMap = getMoistureResult('dry')
break
case '0x0022': // Tamper Alarm
break
case '0x0021': // Open/Motion/Wet
resultMap = getMoistureResult('wet')
break
case '0x0023': // Battery Alarm
break
case '0x0022': // Tamper Alarm
break
case '0x0024': // Supervision Report
log.debug 'dry with tamper alarm'
resultMap = getMoistureResult('dry')
break
case '0x0023': // Battery Alarm
break
case '0x0025': // Restore Report
log.debug 'water with tamper alarm'
resultMap = getMoistureResult('wet')
break
case '0x0024': // Supervision Report
log.debug 'dry with tamper alarm'
resultMap = getMoistureResult('dry')
break
case '0x0026': // Trouble/Failure
break
case '0x0025': // Restore Report
log.debug 'water with tamper alarm'
resultMap = getMoistureResult('wet')
break
case '0x0028': // Test Mode
break
}
return resultMap
case '0x0026': // Trouble/Failure
break
case '0x0028': // Test Mode
break
}
return resultMap
}
def getTemperature(value) {
@@ -215,24 +215,47 @@ def getTemperature(value) {
}
private Map getBatteryResult(rawValue) {
log.debug 'Battery'
log.debug "Battery rawValue = ${rawValue}"
def linkText = getLinkText(device)
def result = [
name: 'battery'
]
def result = [
name: 'battery',
value: '--'
]
def volts = rawValue / 10
def descriptionText
if (volts > 3.5) {
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
}
if (rawValue == 0 || rawValue == 255) {}
else {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "${linkText} battery was ${result.value}%"
if (volts > 3.5) {
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
}
else {
if (device.getDataValue("manufacturer") == "SmartThings") {
volts = rawValue // For the batteryMap to work the key needs to be an int
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
def minVolts = 15
def maxVolts = 28
if (volts < minVolts)
volts = minVolts
else if (volts > maxVolts)
volts = maxVolts
def pct = batteryMap[volts]
if (pct != null) {
result.value = pct
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}
else {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}
}
return result
@@ -267,7 +290,7 @@ private Map getMoistureResult(value) {
def refresh() {
log.debug "Refreshing Temperature and Battery"
def refreshCmds = [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
]
@@ -277,32 +300,32 @@ def refresh() {
def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
return configCmds + refresh() // send refresh cmds as part of config
}
def enrollResponse() {
log.debug "Sending enroll response"
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
]
]
}
private getEndpointId() {
@@ -314,19 +337,19 @@ private hex(value) {
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
reverseArray(hex.decodeHex()).encodeHex()
}
private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
}

View File

@@ -19,17 +19,17 @@ metadata {
capability "Motion Sensor"
capability "Configuration"
capability "Battery"
capability "Temperature Measurement"
capability "Temperature Measurement"
capability "Refresh"
command "enrollResponse"
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
}
simulator {
@@ -85,7 +85,7 @@ metadata {
def parse(String description) {
log.debug "description: $description"
Map map = [:]
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
@@ -96,55 +96,55 @@ def parse(String description) {
else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description)
}
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
log.debug "Parse returned $map"
def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
return result
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
return result
}
private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) {
case 0x0001:
resultMap = getBatteryResult(cluster.data.last())
break
Map resultMap = [:]
def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) {
case 0x0001:
resultMap = getBatteryResult(cluster.data.last())
break
case 0x0402:
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
break
case 0x0402:
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
break
case 0x0406:
log.debug 'motion'
resultMap.name = 'motion'
break
}
}
log.debug 'motion'
resultMap.name = 'motion'
break
}
}
return resultMap
return resultMap
}
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage
}
private Map parseReportAttributeMessage(String description) {
@@ -153,7 +153,7 @@ private Map parseReportAttributeMessage(String description) {
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
log.debug "Desc Map: $descMap"
Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value)
@@ -162,14 +162,14 @@ private Map parseReportAttributeMessage(String description) {
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
}
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
def value = descMap.value.endsWith("01") ? "active" : "inactive"
resultMap = getMotionResult(value)
}
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
def value = descMap.value.endsWith("01") ? "active" : "inactive"
resultMap = getMotionResult(value)
}
return resultMap
}
private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
@@ -180,44 +180,44 @@ private Map parseCustomMessage(String description) {
}
private Map parseIasMessage(String description) {
List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2]
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry
resultMap = getMotionResult('inactive')
break
List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2]
case '0x0021': // Open/Motion/Wet
resultMap = getMotionResult('active')
break
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry
resultMap = getMotionResult('inactive')
break
case '0x0022': // Tamper Alarm
log.debug 'motion with tamper alarm'
resultMap = getMotionResult('active')
break
case '0x0021': // Open/Motion/Wet
resultMap = getMotionResult('active')
break
case '0x0023': // Battery Alarm
break
case '0x0022': // Tamper Alarm
log.debug 'motion with tamper alarm'
resultMap = getMotionResult('active')
break
case '0x0024': // Supervision Report
log.debug 'no motion with tamper alarm'
resultMap = getMotionResult('inactive')
break
case '0x0023': // Battery Alarm
break
case '0x0025': // Restore Report
break
case '0x0024': // Supervision Report
log.debug 'no motion with tamper alarm'
resultMap = getMotionResult('inactive')
break
case '0x0026': // Trouble/Failure
log.debug 'motion with failure alarm'
resultMap = getMotionResult('active')
break
case '0x0025': // Restore Report
break
case '0x0028': // Test Mode
break
}
return resultMap
case '0x0026': // Trouble/Failure
log.debug 'motion with failure alarm'
resultMap = getMotionResult('active')
break
case '0x0028': // Test Mode
break
}
return resultMap
}
def getTemperature(value) {
@@ -230,30 +230,46 @@ def getTemperature(value) {
}
private Map getBatteryResult(rawValue) {
log.debug 'Battery'
log.debug "Battery rawValue = ${rawValue}"
def linkText = getLinkText(device)
log.debug rawValue
def result = [
name: 'battery',
value: '--'
]
def volts = rawValue / 10
def descriptionText
if (rawValue == 0) {}
if (rawValue == 0 || rawValue == 255) {}
else {
if (volts > 3.5) {
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
}
else if (volts > 0){
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "${linkText} battery was ${result.value}%"
else {
if (device.getDataValue("manufacturer") == "SmartThings") {
volts = rawValue // For the batteryMap to work the key needs to be an int
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
def minVolts = 15
def maxVolts = 28
if (volts < minVolts)
volts = minVolts
else if (volts > maxVolts)
volts = maxVolts
def pct = batteryMap[volts]
if (pct != null) {
result.value = pct
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}
else {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}
}
@@ -338,19 +354,19 @@ private hex(value) {
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
reverseArray(hex.decodeHex()).encodeHex()
}
private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
}

View File

@@ -14,28 +14,28 @@
*
*/
metadata {
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Three Axis"
metadata {
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Three Axis"
capability "Battery"
capability "Configuration"
capability "Sensor"
capability "Configuration"
capability "Sensor"
capability "Contact Sensor"
capability "Acceleration Sensor"
capability "Refresh"
capability "Temperature Measurement"
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor"
attribute "status", "string"
}
}
simulator {
simulator {
status "open": "zone report :: type: 19 value: 0031"
status "closed": "zone report :: type: 19 value: 0030"
@@ -52,7 +52,7 @@
status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0"
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000"
}
preferences {
preferences {
section {
image(name: 'educationalcontent', multiple: true, images: [
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
@@ -62,13 +62,13 @@
])
}
section {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
section {
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
}
section {
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
}
}
tiles(scale: 2) {
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
@@ -106,9 +106,9 @@
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:""
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main(["status", "acceleration", "temperature"])
@@ -121,61 +121,61 @@ def parse(String description) {
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
}
else if (description?.startsWith('temperature: ')) {
else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description)
}
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
def result = map ? createEvent(map) : null
def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
else if (description?.startsWith('read attr -')) {
result = parseReportAttributeMessage(description).each { createEvent(it) }
}
else if (description?.startsWith('read attr -')) {
result = parseReportAttributeMessage(description).each { createEvent(it) }
}
return result
}
private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
def cluster = zigbee.parse(description)
log.debug cluster
if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) {
case 0x0001:
resultMap = getBatteryResult(cluster.data.last())
break
private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
def cluster = zigbee.parse(description)
log.debug cluster
if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) {
case 0x0001:
resultMap = getBatteryResult(cluster.data.last())
break
case 0xFC02:
log.debug 'ACCELERATION'
break
case 0xFC02:
log.debug 'ACCELERATION'
break
case 0x0402:
log.debug 'TEMP'
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
break
}
}
case 0x0402:
log.debug 'TEMP'
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
break
}
}
return resultMap
}
return resultMap
}
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage
}
private List parseReportAttributeMessage(String description) {
@@ -202,7 +202,7 @@ private List parseReportAttributeMessage(String description) {
result << parseAxis(threeAxisAttributes)
descMap.value = descMap.value[-2..-1]
}
result << getAccelerationResult(descMap.value)
result << getAccelerationResult(descMap.value)
}
else if (descMap.cluster == "FC02" && descMap.attrId == "0012" && descMap.value.size() == 24) {
// The size is checked to ensure the attribute report contains X, Y and Z values
@@ -231,43 +231,43 @@ private Map parseIasMessage(String description) {
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry
case '0x0020': // Closed/No Motion/Dry
if (garageSensor != "Yes"){
resultMap = getContactResult('closed')
}
break
break
case '0x0021': // Open/Motion/Wet
case '0x0021': // Open/Motion/Wet
if (garageSensor != "Yes"){
resultMap = getContactResult('open')
}
break
break
case '0x0022': // Tamper Alarm
break
case '0x0022': // Tamper Alarm
break
case '0x0023': // Battery Alarm
break
case '0x0023': // Battery Alarm
break
case '0x0024': // Supervision Report
case '0x0024': // Supervision Report
if (garageSensor != "Yes"){
resultMap = getContactResult('closed')
}
break
break
case '0x0025': // Restore Report
case '0x0025': // Restore Report
if (garageSensor != "Yes"){
resultMap = getContactResult('open')
}
break
break
case '0x0026': // Trouble/Failure
break
case '0x0026': // Trouble/Failure
break
case '0x0028': // Test Mode
break
}
return resultMap
case '0x0028': // Test Mode
break
}
return resultMap
}
def updated() {
@@ -302,132 +302,141 @@ def getTemperature(value) {
}
}
private Map getBatteryResult(rawValue) {
log.debug "Battery"
log.debug rawValue
def linkText = getLinkText(device)
private Map getBatteryResult(rawValue) {
log.debug "Battery rawValue = ${rawValue}"
def linkText = getLinkText(device)
def result = [
def result = [
name: 'battery',
value: '--'
]
value: '--'
]
def volts = rawValue / 10
def descriptionText
if (rawValue == 255) {}
else {
if (volts > 3.5) {
def volts = rawValue / 10
if (rawValue == 0 || rawValue == 255) {}
else {
if (volts > 3.5) {
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
}
else {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "${linkText} battery was ${result.value}%"
}}
if (device.getDataValue("manufacturer") == "SmartThings") {
volts = rawValue // For the batteryMap to work the key needs to be an int
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
def minVolts = 15
def maxVolts = 28
return result
}
private Map getTemperatureResult(value) {
log.debug "Temperature"
def linkText = getLinkText(device)
if (tempOffset) {
def offset = tempOffset as int
def v = value as int
value = v + offset
if (volts < minVolts)
volts = minVolts
else if (volts > maxVolts)
volts = maxVolts
def pct = batteryMap[volts]
if (pct != null) {
result.value = pct
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}
else {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
return [
name: 'temperature',
}
return result
}
private Map getTemperatureResult(value) {
log.debug "Temperature"
def linkText = getLinkText(device)
if (tempOffset) {
def offset = tempOffset as int
def v = value as int
value = v + offset
}
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
return [
name: 'temperature',
value: value,
descriptionText: descriptionText
]
}
private Map getContactResult(value) {
log.debug "Contact"
def linkText = getLinkText(device)
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed:false)
sendEvent(name: 'status', value: value, descriptionText: descriptionText)
}
private getAccelerationResult(numValue) {
log.debug "Acceleration"
def name = "acceleration"
def value = numValue.endsWith("1") ? "active" : "inactive"
def linkText = getLinkText(device)
def descriptionText = "$linkText was $value"
def isStateChange = isStateChange(device, name, value)
[
name: name,
value: value,
descriptionText: descriptionText
descriptionText: descriptionText,
isStateChange: isStateChange
]
}
def refresh() {
log.debug "Refreshing Values "
def refreshCmds = []
if (device.getDataValue("manufacturer") == "SmartThings") {
log.debug "Refreshing Values for manufacturer: SmartThings "
refreshCmds = refreshCmds + [
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602)
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
Separating these out in a separate if-else because I do not want to touch Centralite part
as of now.
*/
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 2 0x21 {7602}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
]
} else {
refreshCmds = refreshCmds + [
/* sensitivity - default value (8) */
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
]
}
private Map getContactResult(value) {
log.debug "Contact"
def linkText = getLinkText(device)
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed:false)
sendEvent(name: 'status', value: value, descriptionText: descriptionText)
}
//Common refresh commands
refreshCmds = refreshCmds + [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
private getAccelerationResult(numValue) {
log.debug "Acceleration"
def name = "acceleration"
def value = numValue.endsWith("1") ? "active" : "inactive"
def linkText = getLinkText(device)
def descriptionText = "$linkText was $value"
def isStateChange = isStateChange(device, name, value)
[
name: name,
value: value,
descriptionText: descriptionText,
isStateChange: isStateChange
]
}
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global read 0xFC02 0x0010",
"send 0x${device.deviceNetworkId} 1 1","delay 400"
]
def refresh() {
log.debug "Refreshing Values "
def refreshCmds = []
if (device.getDataValue("manufacturer") == "SmartThings") {
log.debug "Refreshing Values for manufacturer: SmartThings "
refreshCmds = refreshCmds + [
return refreshCmds + enrollResponse()
}
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602)
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
Separating these out in a separate if-else because I do not want to touch Centralite part
as of now.
*/
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 2 0x21 {7602}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
]
} else {
refreshCmds = refreshCmds + [
/* sensitivity - default value (8) */
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
]
}
//Common refresh commands
refreshCmds = refreshCmds + [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global read 0xFC02 0x0010",
"send 0x${device.deviceNetworkId} 1 1","delay 400"
]
return refreshCmds + enrollResponse()
}
def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting"
def configCmds = [
def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting"
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
@@ -455,10 +464,9 @@ def getTemperature(value) {
"zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
]
return configCmds + refresh()
return configCmds + refresh()
}
private getEndpointId() {
@@ -582,5 +590,3 @@ private byte[] reverseArray(byte[] array) {
}
return array
}

View File

@@ -0,0 +1,144 @@
/**
* Copyright 2016 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.
*
* Author: SmartThings
* Date: 2016-01-19
*
* This DTH should serve as the generic DTH to handle RGBW ZigBee HA devices
*/
metadata {
definition (name: "ZigBee RGBW Bulb", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Color Control"
capability "Color Temperature"
capability "Configuration"
capability "Polling"
capability "Refresh"
capability "Switch"
capability "Switch Level"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 RGBW", deviceJoinName: "OSRAM LIGHTIFY LED A19 RGBW"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR RGBW", deviceJoinName: "OSRAM LIGHTIFY LED BR30 RGBW"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT RGBW", deviceJoinName: "OSRAM LIGHTIFY LED RT 5/6 RGBW"
}
// UI tile definitions
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
tileAttribute ("device.color", key: "COLOR_CONTROL") {
attributeState "color", action:"color control.setColor"
}
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: '${currentValue} K'
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main(["switch"])
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
}
}
//Globals
private getATTRIBUTE_HUE() { 0x0000 }
private getATTRIBUTE_SATURATION() { 0x0001 }
private getHUE_COMMAND() { 0x00 }
private getSATURATION_COMMAND() { 0x03 }
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def finalResult = zigbee.getEvent(description)
if (finalResult) {
log.debug finalResult
sendEvent(finalResult)
}
else {
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
log.trace "zigbeeMap : $zigbeeMap"
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
sendEvent(name: "hue", value: hueValue, displayed:false)
}
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
sendEvent(name: "saturation", value: saturationValue, displayed:false)
}
}
else {
log.info "DID NOT PARSE MESSAGE for description : $description"
}
}
}
def on() {
zigbee.on()
}
def off() {
zigbee.off()
}
def refresh() {
zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION) + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
}
def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
}
def setColorTemperature(value) {
zigbee.setColorTemperature(value)
}
def setLevel(value) {
zigbee.setLevel(value)
}
def setColor(value){
log.trace "setColor($value)"
zigbee.on() + setHue(value.hue) + "delay 300" + setSaturation(value.saturation)
}
def setHue(value) {
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
}
def setSaturation(value) {
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") //payload-> sat value, transition time
}

View File

@@ -0,0 +1,243 @@
metadata {
definition (name: "Timevalve Smart", namespace: "timevalve.gaslock.t-08", author: "ruinnel") {
capability "Valve"
capability "Refresh"
capability "Battery"
capability "Temperature Measurement"
command "setRemaining"
command "setTimeout"
command "setTimeout10"
command "setTimeout20"
command "setTimeout30"
command "setTimeout40"
command "remainingLevel"
attribute "remaining", "number"
attribute "remainingText", "String"
attribute "timeout", "number"
//raw desc : 0 0 0x1006 0 0 0 7 0x5E 0x86 0x72 0x5A 0x73 0x98 0x80
//fingerprint deviceId:"0x1006", inClusters:"0x5E, 0x86, 0x72, 0x5A, 0x73, 0x98, 0x80"
}
tiles (scale: 2) {
multiAttributeTile(name:"statusTile", type:"generic", width:6, height:4) {
tileAttribute("device.contact", key: "PRIMARY_CONTROL") {
attributeState "open", label: '${name}', action: "close", icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
attributeState "closed", label:'${name}', action: "", icon:"st.contact.contact.closed", backgroundColor:"#79b821"
}
tileAttribute("device.remainingText", key: "SECONDARY_CONTROL") {
attributeState "open", label: '${currentValue}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
attributeState "closed", label:'', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
}
}
standardTile("refreshTile", "command.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
controlTile("remainingSliderTile", "device.remaining", "slider", inactiveLabel: false, range:"(0..590)", height: 2, width: 4) {
state "level", action:"remainingLevel"
}
valueTile("setRemaining", "device.remainingText", inactiveLabel: false, decoration: "flat", height: 2, width: 2){
state "remainingText", label:'${currentValue}\nRemaining'//, action: "setRemaining"//, icon: "st.Office.office6"
}
standardTile("setTimeout10", "device.remaining", inactiveLabel: false, decoration: "flat") {
state "default", label:'10Min', action: "setTimeout10", icon:"st.Health & Wellness.health7", defaultState: true
state "10", label:'10Min', action: "setTimeout10", icon:"st.Office.office13"
}
standardTile("setTimeout20", "device.remaining", inactiveLabel: false, decoration: "flat") {
state "default", label:'20Min', action: "setTimeout20", icon:"st.Health & Wellness.health7", defaultState: true
state "20", label:'20Min', action: "setTimeout20", icon:"st.Office.office13"
}
standardTile("setTimeout30", "device.remaining", inactiveLabel: false, decoration: "flat") {
state "default", label:'30Min', action: "setTimeout30", icon:"st.Health & Wellness.health7", defaultState: true
state "30", label:'30Min', action: "setTimeout30", icon:"st.Office.office13"
}
standardTile("setTimeout40", "device.remaining", inactiveLabel: false, decoration: "flat") {
state "default", label:'40Min', action: "setTimeout40", icon:"st.Health & Wellness.health7", defaultState: true
state "40", label:'40Min', action: "setTimeout40", icon:"st.Office.office13"
}
valueTile("batteryTile", "device.battery", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "battery", label:'${currentValue}% battery', unit:""
}
main (["statusTile"])
// details (["statusTile", "remainingSliderTile", "setRemaining", "setTimeout10", "setTimeout20", "batteryTile", "refreshTile", "setTimeout30", "setTimeout40"])
// details (["statusTile", "batteryTile", "setRemaining", "refreshTile"])
details (["statusTile", "batteryTile", "refreshTile"])
}
}
def parse(description) {
// log.debug "parse - " + description
def result = null
if (description.startsWith("Err 106")) {
state.sec = 0
result = createEvent(descriptionText: description, isStateChange: true)
} else if (description != "updated") {
def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x71: 1, 0x98: 1])
if (cmd) {
log.debug "parsed cmd = " + cmd
result = zwaveEvent(cmd)
//log.debug("'$description' parsed to $result")
} else {
log.debug("Couldn't zwave.parse '$description'")
}
}
return result
}
// 복호화 후 zwaveEvent() 호출
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
//log.debug "SecurityMessageEncapsulation - " + cmd
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x70: 1, 0x71: 1, 0x98: 1])
if (encapsulatedCommand) {
state.sec = 1
log.debug "encapsulatedCommand = " + encapsulatedCommand
zwaveEvent(encapsulatedCommand)
}
}
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
//log.debug "switch status - " + cmd.value
createEvent(name:"contact", value: cmd.value ? "open" : "closed")
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
def map = [ name: "battery", unit: "%" ]
if (cmd.batteryLevel == 0xFF) { // Special value for low battery alert
map.value = 1
map.descriptionText = "${device.displayName} has a low battery"
map.isStateChange = true
} else {
map.value = cmd.batteryLevel
}
log.debug "battery - ${map.value}${map.unit}"
// Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
state.lastbatt = new Date().time
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
//log.debug "zwaveEvent - ${device.displayName}: ${cmd}"
createEvent(descriptionText: "${device.displayName}: ${cmd}")
}
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
def result = []
log.info "zwave.configurationV1.configurationGet - " + cmd
def array = cmd.configurationValue
def value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60)
if (device.currentValue("contact") == "open") {
value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60)
} else {
value = 0
}
if (device.currentValue('contact') == 'open') {
def hour = value.intdiv(60);
def min = (value % 60).toString().padLeft(2, '0');
def text = "${hour}:${min}M"
log.info "remain - " + text
result.add( createEvent(name: "remaining", value: value, displayed: false, isStateChange: true) )
result.add( createEvent(name: "remainingText", value: text, displayed: false, isStateChange: true) )
} else {
result.add( createEvent(name: "timeout", value: value, displayed: false, isStateChange: true) )
}
return result
}
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
def type = cmd.notificationType
if (type == cmd.NOTIFICATION_TYPE_HEAT) {
log.info "NotificationReport - ${type}"
createEvent(name: "temperature", value: 999, unit: "C", descriptionText: "${device.displayName} is over heat!", displayed: true, isStateChange: true)
}
}
def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) {
def type = cmd.alarmType
def level = cmd.alarmLevel
log.info "AlarmReport - type : ${type}, level : ${level}"
def msg = "${device.displayName} is over heat!"
def result = createEvent(name: "temperature", value: 999, unit: "C", descriptionText: msg, displayed: true, isStateChange: true)
if (sendPushMessage) {
sendPushMessage(msg)
}
return result
}
// remote open not allow
def open() {}
def close() {
// log.debug 'cmd - close()'
commands([
zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00),
zwave.switchBinaryV1.switchBinaryGet()
])
}
def setTimeout10() { setTimeout(10) }
def setTimeout20() { setTimeout(20) }
def setTimeout30() { setTimeout(30) }
def setTimeout40() { setTimeout(40) }
def setTimeout(value) {
// log.debug "setDefaultTime($value)"
commands([
zwave.configurationV1.configurationSet(parameterNumber: 0x01, size: 4, scaledConfigurationValue: value * 60),
zwave.configurationV1.configurationGet(parameterNumber: 0x01)
]);
}
def remainingLevel(value) {
// log.debug "remainingLevel($value)"
def hour = value.intdiv(60);
def min = (value % 60).toString().padLeft(2, '0');
def text = "${hour}:${min}M"
sendEvent(name: "remaining", value: value, displayed: false, isStateChange: true)
sendEvent(name: "remainingText", value: text, displayed: false, isStateChange: true)
}
def setRemaining() {
def remaining = device.currentValue("remaining")
// log.debug "setConfiguration() - remaining : $remaining"
commands([
zwave.configurationV1.configurationSet(parameterNumber: 0x03, size: 4, scaledConfigurationValue: remaining * 60),
zwave.configurationV1.configurationGet(parameterNumber: 0x03)
]);
}
private command(physicalgraph.zwave.Command cmd) {
if (state.sec != 0 && !(cmd instanceof physicalgraph.zwave.commands.batteryv1.BatteryGet)) {
log.debug "cmd = " + cmd + ", encapsulation"
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
} else {
log.debug "cmd = " + cmd + ", plain"
cmd.format()
}
}
private commands(commands, delay=200) {
delayBetween(commands.collect{ command(it) }, delay)
}
def refresh() {
// log.debug 'cmd - refresh()'
commands([
zwave.batteryV1.batteryGet(),
zwave.switchBinaryV1.switchBinaryGet(),
zwave.configurationV1.configurationGet(parameterNumber: 0x01),
zwave.configurationV1.configurationGet(parameterNumber: 0x03)
], 400)
}

View File

@@ -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)
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2015 SmartThings
* Copyright 2016 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:
@@ -38,37 +38,75 @@ preferences {
page(name: "schedulingPage")
page(name: "completionPage")
page(name: "numbersPage")
page(name: "controllerExplanationPage")
}
def rootPage() {
dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {
section {
section("What to dim") {
input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true)
if (dimmers) {
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
}
}
if (dimmers) {
section {
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
section("Gentle Wake Up Has A Controller") {
href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
}
section {
href(name: "toSchedulingPage", page: "schedulingPage", title: "Rules For Automatically Dimming Your Lights", description: schedulingHrefDescription(), state: schedulingHrefDescription() ? "complete" : "")
}
section {
href(name: "toCompletionPage", title: "Completion Actions (Optional)", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription())
section("Rules For Dimming") {
href(name: "toSchedulingPage", page: "schedulingPage", title: "Automation", description: schedulingHrefDescription() ?: "Set rules for when to start", state: schedulingHrefDescription() ? "complete" : "")
input(name: "manualOverride", type: "enum", options: ["cancel": "Cancel dimming", "jumpTo": "Jump to the end"], title: "When one of the dimmers is manually turned off…", description: "dimming will continue", required: false, multiple: false)
href(name: "toCompletionPage", title: "Completion Actions", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription() ?: "Set rules for what to do when dimming completes")
}
section {
// TODO: fancy label
label(title: "Label this SmartApp", required: false, defaultValue: "")
label(title: "Label This SmartApp", required: false, defaultValue: "", description: "Highly recommended", submitOnChange: true)
}
}
}
}
def controllerExplanationPage() {
dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") {
section("With other SmartApps", hideable: true, hidden: false) {
paragraph "When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!"
paragraph "The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!"
paragraph "Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up."
}
section("More about the controller", hideable: true, hidden: true) {
paragraph "You can find the controller with your other 'Things'. It will look like this."
image "http://f.cl.ly/items/2O0v0h41301U14042z3i/GentleWakeUpController-tile-stopped.png"
paragraph "You can start and stop Gentle Wake up by tapping the control on the right."
image "http://f.cl.ly/items/3W323J3M1b3K0k0V3X3a/GentleWakeUpController-tile-running.png"
paragraph "If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls."
image "http://f.cl.ly/items/291s3z2I2Q0r2q0x171H/GentleWakeUpController-richTile-stopped.png"
paragraph "The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep."
image "http://f.cl.ly/items/0F0N2G0S3v1q0L0R3J3Y/GentleWakeUpController-richTile-running.png"
paragraph "In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle."
paragraph "Of course, you may also tap the middle to start or stop the dimming cycle at any time."
}
section("Starting and stopping the SmartApp itself", hideable: true, hidden: true) {
paragraph "Tap the 'play' button on the SmartApp to start or stop dimming."
image "http://f.cl.ly/items/0R2u1Z2H30393z2I2V3S/GentleWakeUp-appTouch2.png"
}
section("Turning off devices while dimming", hideable: true, hidden: true) {
paragraph "It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option."
paragraph "If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings."
paragraph "Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop."
paragraph "That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)"
}
}
}
def numbersPage() {
dynamicPage(name:"numbersPage", title:"") {
@@ -128,24 +166,33 @@ def endLevelLabel() {
return "${endLevel}%"
}
def weekdays() {
["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
}
def weekends() {
["Saturday", "Sunday"]
}
def schedulingPage() {
dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
section {
input(name: "days", type: "enum", title: "Allow Automatic Dimming On These Days", description: "Every day", required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"])
section("Use Other SmartApps!") {
href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
}
section {
input(name: "modeStart", title: "Start when entering this mode", type: "mode", required: false, mutliple: false, submitOnChange: true)
section("Allow Automatic Dimming") {
input(name: "days", type: "enum", title: "On These Days", description: "Every day", required: false, multiple: true, options: weekdays() + weekends())
}
section("Start Dimming...") {
input(name: "startTime", type: "time", title: "At This Time", description: null, required: false)
input(name: "modeStart", title: "When Entering This Mode", type: "mode", required: false, mutliple: false, submitOnChange: true, description: null)
if (modeStart) {
input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
}
}
section {
input(name: "startTime", type: "time", title: "Start Dimming At This Time", description: null, required: false)
}
}
}
@@ -194,11 +241,16 @@ def updated() {
log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
unschedule()
def controller = getController()
if (controller) {
controller.label = app.label
}
initialize()
}
private initialize() {
stop()
stop("settingsChange")
if (startTime) {
log.debug "scheduling dimming routine to run at $startTime"
@@ -209,15 +261,27 @@ private initialize() {
subscribe(app, appHandler)
subscribe(location, locationHandler)
if (manualOverride) {
subscribe(dimmers, "switch.off", stopDimmersHandler)
}
if (!getAllChildDevices()) {
// create controller device and set name to the label used here
def dni = "${new Date().getTime()}"
log.debug "app.label: ${app.label}"
addChildDevice("smartthings", "Gentle Wake Up Controller", dni, null, ["label": app.label])
state.controllerDni = dni
}
}
def appHandler(evt) {
log.debug "appHandler evt: ${evt.value}"
if (evt.value == "touch") {
if (atomicState.running) {
stop()
stop("appTouch")
} else {
start()
start("appTouch")
}
}
}
@@ -233,26 +297,47 @@ def locationHandler(evt) {
def modeStopIsTrue = (modeStop && modeStop != "false")
if (isSpecifiedMode && canStartAutomatically()) {
start()
start("modeChange")
} else if (!isSpecifiedMode && modeStopIsTrue) {
stop()
stop("modeChange")
}
}
def stopDimmersHandler(evt) {
log.trace "stopDimmersHandler evt: ${evt.value}"
def percentComplete = completionPercentage()
// Often times, the first thing we do is turn lights on or off so make sure we don't stop as soon as we start
if (percentComplete > 2 && percentComplete < 98) {
if (manualOverride == "cancel") {
log.debug "STOPPING in stopDimmersHandler"
stop("manualOverride")
} else if (manualOverride == "jumpTo") {
def end = dynamicEndLevel()
log.debug "Jumping to 99% complete in stopDimmersHandler"
jumpTo(99)
}
} else {
log.debug "not stopping in stopDimmersHandler"
}
}
// ========================================================
// Scheduling
// ========================================================
def scheduledStart() {
if (canStartAutomatically()) {
start()
start("schedule")
}
}
def start() {
public def start(source) {
log.trace "START"
sendStartEvent(source)
setLevelsInState()
atomicState.running = true
@@ -263,9 +348,11 @@ def start() {
increment()
}
def stop() {
public def stop(source) {
log.trace "STOP"
sendStopEvent(source)
atomicState.running = false
atomicState.start = 0
@@ -282,6 +369,110 @@ private healthCheck() {
increment()
}
// ========================================================
// Controller
// ========================================================
def sendStartEvent(source) {
log.trace "sendStartEvent(${source})"
def eventData = [
name: "sessionStatus",
value: "running",
descriptionText: "${app.label} has started dimming",
displayed: true,
linkText: app.label,
isStateChange: true
]
if (source == "modeChange") {
eventData.descriptionText += " because of a mode change"
} else if (source == "schedule") {
eventData.descriptionText += " as scheduled"
} else if (source == "appTouch") {
eventData.descriptionText += " because you pressed play on the app"
} else if (source == "controller") {
eventData.descriptionText += " because you pressed play on the controller"
}
sendControllerEvent(eventData)
}
def sendStopEvent(source) {
log.trace "sendStopEvent(${source})"
def eventData = [
name: "sessionStatus",
value: "stopped",
descriptionText: "${app.label} has stopped dimming",
displayed: true,
linkText: app.label,
isStateChange: true
]
if (source == "modeChange") {
eventData.descriptionText += " because of a mode change"
eventData.value += "cancelled"
} else if (source == "schedule") {
eventData.descriptionText = "${app.label} has finished dimming"
} else if (source == "appTouch") {
eventData.descriptionText += " because you pressed play on the app"
eventData.value += "cancelled"
} else if (source == "controller") {
eventData.descriptionText += " because you pressed stop on the controller"
eventData.value += "cancelled"
} else if (source == "settingsChange") {
eventData.descriptionText += " because the settings have changed"
eventData.value += "cancelled"
} else if (source == "manualOverride") {
eventData.descriptionText += " because the dimmer was manually turned off"
eventData.value += "cancelled"
}
sendControllerEvent(eventData)
sendTimeRemainingEvent(0)
}
def sendTimeRemainingEvent(percentComplete) {
log.trace "sendTimeRemainingEvent(${percentComplete})"
def percentCompleteEventData = [
name: "percentComplete",
value: percentComplete as int,
displayed: true,
isStateChange: true
]
sendControllerEvent(percentCompleteEventData)
def duration = sanitizeInt(duration, 30)
def timeRemaining = duration - (duration * (percentComplete / 100))
def timeRemainingEventData = [
name: "timeRemaining",
value: displayableTime(timeRemaining),
displayed: true,
isStateChange: true
]
sendControllerEvent(timeRemainingEventData)
}
def sendControllerEvent(eventData) {
def controller = getController()
if (controller) {
controller.controllerEvent(eventData)
}
}
def getController() {
def dni = state.controllerDni
if (!dni) {
log.warn "no controller dni"
return null
}
def controller = getChildDevice(dni)
if (!controller) {
log.warn "no controller"
return null
}
log.debug "controller: ${controller}"
return controller
}
// ========================================================
// Setting levels
// ========================================================
@@ -349,6 +540,8 @@ def updateDimmers(percentComplete) {
}
}
sendTimeRemainingEvent(percentComplete)
}
int dynamicLevel(dimmer, percentComplete) {
@@ -377,7 +570,7 @@ private completion() {
return
}
stop()
stop("schedule")
handleCompletionSwitches()
@@ -385,6 +578,7 @@ private completion() {
handleCompletionModesAndPhrases()
sendTimeRemainingEvent(100)
}
private handleCompletionSwitches() {
@@ -493,22 +687,65 @@ def completionPercentage() {
return
}
int now = new Date().getTime()
int diff = now - atomicState.start
int totalRunTime = totalRunTimeMillis()
int percentOfRunTime = (diff / totalRunTime) * 100
log.debug "percentOfRunTime: ${percentOfRunTime}"
def now = new Date().getTime()
def timeElapsed = now - atomicState.start
def totalRunTime = totalRunTimeMillis()
def percentComplete = timeElapsed / totalRunTime * 100
log.debug "percentComplete: ${percentComplete}"
percentOfRunTime
return percentComplete
}
int totalRunTimeMillis() {
int minutes = sanitizeInt(duration, 30)
convertToMillis(minutes)
}
int convertToMillis(minutes) {
def seconds = minutes * 60
def millis = seconds * 1000
return millis as int
return millis
}
def timeRemaining(percentComplete) {
def normalizedPercentComplete = percentComplete / 100
def duration = sanitizeInt(duration, 30)
def timeElapsed = duration * normalizedPercentComplete
def timeRemaining = duration - timeElapsed
return timeRemaining
}
int millisToEnd(percentComplete) {
convertToMillis(timeRemaining(percentComplete))
}
String displayableTime(timeRemaining) {
def timeString = "${timeRemaining}"
def parts = timeString.split(/\./)
if (!parts.size()) {
return "0:00"
}
def minutes = parts[0]
if (parts.size() == 1) {
return "${minutes}:00"
}
def fraction = "0.${parts[1]}" as double
def seconds = "${60 * fraction as int}".padRight(2, "0")
return "${minutes}:${seconds}"
}
def jumpTo(percentComplete) {
def millisToEnd = millisToEnd(percentComplete)
def endTime = new Date().getTime() + millisToEnd
def duration = sanitizeInt(duration, 30)
def durationMillis = convertToMillis(duration)
def shiftedStart = endTime - durationMillis
atomicState.start = shiftedStart
updateDimmers(percentComplete)
sendTimeRemainingEvent(percentComplete)
}
int dynamicEndLevel() {
if (usesOldSettings()) {
if (direction && direction == "Down") {
@@ -673,7 +910,13 @@ def schedulingHrefDescription() {
def descriptionParts = []
if (days) {
descriptionParts << "On ${fancyString(days)},"
if (days == weekdays()) {
descriptionParts << "On weekdays,"
} else if (days == weekends()) {
descriptionParts << "On weekends,"
} else {
descriptionParts << "On ${fancyString(days)},"
}
}
descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
@@ -759,15 +1002,15 @@ def completionHrefDescription() {
def numbersPageHrefDescription() {
def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
if (colorize) {
def colorDimmers = dimmersWithSetColorCommand()
if (colorDimmers == dimmers) {
title += " and will gradually change color."
} else {
title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
}
}
return title
if (colorize) {
def colorDimmers = dimmersWithSetColorCommand()
if (colorDimmers == dimmers) {
title += " and will gradually change color."
} else {
title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
}
}
return title
}
def hueSatToHex(h, s) {