From d3afa1423e38ea7da984b7ef5728a9d4f1233be2 Mon Sep 17 00:00:00 2001 From: Edvald Eysteinsson Date: Sat, 6 May 2017 19:12:01 +0200 Subject: [PATCH] Restuctured repo --- .../ikea-tradfri.src/ikea_tradfri.groovy | 266 ++++++++++++++++++ .../testdevicehandler.groovy | 39 +++ images/next_color.png | Bin 0 -> 11350 bytes 3 files changed, 305 insertions(+) create mode 100644 devicetypes/edvaldeysteinsson/ikea-tradfri.src/ikea_tradfri.groovy create mode 100644 devicetypes/edvaldeysteinsson/testdevicehandler.src/testdevicehandler.groovy create mode 100644 images/next_color.png diff --git a/devicetypes/edvaldeysteinsson/ikea-tradfri.src/ikea_tradfri.groovy b/devicetypes/edvaldeysteinsson/ikea-tradfri.src/ikea_tradfri.groovy new file mode 100644 index 0000000..f5efb9e --- /dev/null +++ b/devicetypes/edvaldeysteinsson/ikea-tradfri.src/ikea_tradfri.groovy @@ -0,0 +1,266 @@ +/** + * Copyright 2017 Edvald Eysteinsson + * + * 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. + * + * IKEA Trådfri + * + * This handler is written so that the bulbs behave a bit more like traditional halogen bulbs and the ones i modeled it + * after is https://www.osram.com/osram_com/products/lamps/halogen-lamps/halopar/halopar-16-gu10gz10-star/index.jsp + * they have a color rendering index of 100 at full brightness and that is equivalent to 3200 kelvin. The level at 1% + * will use 2200 kelvin and each percent will increse the temperature by 10 ending up at 3190 at 100% + * + * Author: Edvald Eysteinsson + * Date: 2017-03-18 + */ +metadata { + definition (name: "IKEA Trådfri", namespace: "edvaldeysteinsson", author: "Edvald Eysteinsson") { + capability "Actuator" + capability "Color Temperature" + capability "Configuration" + capability "Health Check" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Light" + + attribute "colorName", "string" + + command "setColorName" + command "setColorRelax" + command "setColorEveryday" + command "setColorFocus" + command "nextColor" + + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 WS�opal 980lm", deviceJoinName: "TRÅDFRI bulb E27 WS opal 980lm" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 WS opal 980lm", deviceJoinName: "TRÅDFRI bulb E27 WS opal 980lm" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 WS clear 950lm", deviceJoinName: "TRÅDFRI bulb E27 WS clear 950lm" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 WS�opal 980lm", deviceJoinName: "TRÅDFRI bulb E26 WS opal 980lm" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 WS opal 980lm", deviceJoinName: "TRÅDFRI bulb E26 WS opal 980lm" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 WS clear 950lm", deviceJoinName: "TRÅDFRI bulb E26 WS clear 950lm" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E14 WS opal 400lm", deviceJoinName: "TRÅDFRI bulb E14 WS opal 400lm" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E12 WS opal 400lm", deviceJoinName: "TRÅDFRI bulb E12 WS opal 400lm" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb GU10 WS 400lm", deviceJoinName: "TRÅDFRI bulb GU10 WS 400lm" + } + + preferences { + input name: "linkLevelAndColor", type: "bool", title: "Link level change with color temperature?", defaultValue: true, displayDuringSetup: true, required: false + input name: "delay", type: "number", title: "Delay between level and color temperature change in milliseconds", defaultValue: 0, displayDuringSetup: true, required: false + input name: "colorTempMin", type: "number", title: "Color temperature at lowest level(1%)", defaultValue: 2200, range: "2200..4000", displayDuringSetup: true, required: false + input name: "colorTempMax", type: "number", title: "Color temperature at highest level(100%)", defaultValue: 3200, range: "2200..4000", displayDuringSetup: true, required: false + input name: "colorNameAsKelvin", type: "bool", title: "Display color temperature as kelvin", defaultValue: false, displayDuringSetup: true, required: false + } + + // 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.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"setLevel" + } + } + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 1, inactiveLabel: false, range:"(2200..4000)") { + state "colorTemperature", action:"setColorTemperature" + } + + valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "colorName", label: '${currentValue}' + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + standardTile("nextColor", "device.default", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label:"", action:"nextColor", icon:"https://github.com/edvaldeysteinsson/SmartThingsResources/raw/master/images/next_color.png" + } + + standardTile("colorRelax", "device.default", inactiveLabel: false, width: 2, height: 2) { + state "default", label:"", action:"setColorRelax", backgroundColor:"#ECCF73" + } + + standardTile("colorEveryday", "device.default", inactiveLabel: false, width: 2, height: 2) { + state "default", label:"", action:"setColorEveryday", backgroundColor:"#FBECCB" + } + + standardTile("colorFocus", "device.default", inactiveLabel: false, width: 2, height: 2) { + state "default", label:"", action:"setColorFocus", backgroundColor:"#F5FBFB" + } + + main(["switch"]) + details(["switch", "colorTempSliderControl", "colorName", "refresh", "nextColor", "colorRelax", "colorEveryday", "colorFocus"]) + } +} + +// parse events into attributes +def parse_new(description) { + def results = [] + + def map = description + if (description instanceof String) { + map = stringToMap(description) + } + + if (map?.name && map?.value) { + results << createEvent(name: "${map?.name}", value: "${map?.value}") + } + + results +} + +// Parse incoming device messages to generate events +def parse(String description) { + def event = zigbee.getEvent(description) + + if (event) { + if (event.name != "level" || (event.name=="level" && event.value > 0)) { + if (event.name=="colorTemperature") { + setColorName(event.value) + } + sendEvent(event) + } + } else { + def cluster = zigbee.parse(description) + + if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) { + if (cluster.data[0] == 0x00) { + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } else { + log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}" + } + } else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug "${cluster}" + } + } +} + +def off() { + zigbee.off() +} + +def on() { + zigbee.on() +} + +def setLevel(value) { +// In case the level is 0 we dont want to do anything with the color temperature + if(value == 0){ + zigbee.setLevel(value) + } else { + if(linkLevelAndColor ?: false){ + def colorTempMin = colorTempMin ?: 2200; + def colorTempMax = colorTempMax ?: 3200; + def stepSize = (colorTempMax - colorTempMin) / 99; + int colorTemperature = Math.ceil((colorTempMin - stepSize) + (stepSize*value)); + + // this will set the color temperature based on the level, default color temperatures are + // 2200(1%) to 3200(100%) but they can be set in preferences. + // This is a bit more like how a traditional bulb behaves, it will turn warmer at lower levels. + // There is nothing preventing a user from doing the opposite, 4000 at 1% and 2200 at 100% if + // they feel like it. + delayBetween([ + zigbee.setLevel(value), + zigbee.setColorTemperature(colorTemperature) + ], delay ?: 0) + } else { + zigbee.setLevel(value) + } + } +} + +def setColorRelax() { + setColorTemperature(2200) +} + +def setColorEveryday() { + setColorTemperature(2700) +} + +def setColorFocus() { + setColorTemperature(4000) +} + +def setColorTemperature(value) { + // This is added here just in case something calls this with a value that is out of range for the bulbs + if(value > 4000){ + value = 4000; + } else if(value < 2200){ + value = 2200; + } + + setColorName(value) + zigbee.setColorTemperature(value) +} + +def setColorName(value){ + state.colourTemperature = value + + if(colorNameAsKelvin ?: false){ + sendEvent(name: "colorName", value: "${value} K" ) + } else { + if (value != null) { + def genericName + + if (value < 2450) { + genericName = "Relax" // 2200 is named Relax by IKEA so i use that for 2200-2449 + } else if (value < 2950) { + genericName = "Everyday" // 2700 is named Everyday by IKEA so i use that for 2450-2949 + } else if (value <= 4000) { + genericName = "Focus" // 4000 is named Focus by IKEA so i use that for 2950-4000 + } + + sendEvent(name: "colorName", value: genericName) + } + } +} + +def nextColor() { + if(state.colourTemperature < 2450) { + setColorEveryday() + } else if (state.colourTemperature < 2950) { + setColorFocus() + } else { + setColorRelax() + } +} + +/** +* PING is used by Device-Watch in attempt to reach the Device +* */ +def ping() { + return zigbee.onOffRefresh() +} + +def refresh() { + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() +} + +def configure() { + // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity + refresh() +} + +def installed() { + if ((device.currentState("level")?.value == null) || (device.currentState("level")?.value == 0)) { + sendEvent(name: "level", value: 100) + } +} diff --git a/devicetypes/edvaldeysteinsson/testdevicehandler.src/testdevicehandler.groovy b/devicetypes/edvaldeysteinsson/testdevicehandler.src/testdevicehandler.groovy new file mode 100644 index 0000000..492c489 --- /dev/null +++ b/devicetypes/edvaldeysteinsson/testdevicehandler.src/testdevicehandler.groovy @@ -0,0 +1,39 @@ +metadata { + definition (name: "TestDeviceHandler", namespace: "edvaldeysteinsson", author: "Edvald Eysteinsson") { + capability "Switch" + capability "Light" + } + + // simulator metadata + simulator { + } + + // UI tile definitions + tiles { + multiAttributeTile(name:"switch", type: "lighting", width: 3, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'On', action:"switch.off", icon:"st.Appliances.appliances14", backgroundColor:"#79b821", nextState:"off" + attributeState "off", label:'Off', action:"switch.on", icon:"st.Appliances.appliances14", backgroundColor:"#ffffff", nextState:"on" + } + } + + main(["switch"]) + } +} + +def parse(String description) { +} + +def on() { + log.info "Switch On" + makeCall(1) +} + +def off() { + log.info "Switch Off" + makeCall(0) +} + +def makeCall(level) { + +} diff --git a/images/next_color.png b/images/next_color.png new file mode 100644 index 0000000000000000000000000000000000000000..3f48b00f831141e92588c8d857b567281337b123 GIT binary patch literal 11350 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