├── imgs ├── README.md └── tuya-packet-capture.jpg ├── LICENSE ├── devicetypes ├── fison67 │ ├── tuya-thermostat.src │ │ └── tuya-thermostat.groovy │ ├── tuya-plug.src │ │ └── tuya-plug.groovy │ ├── tuya-power-strip-child.src │ │ └── tuya-power-strip-child.groovy │ ├── tuya-switch.src │ │ └── tuya-switch.groovy │ ├── tuya-power-strip.src │ │ └── tuya-power-strip.groovy │ ├── tuya-switch-child.src │ │ └── tuya-switch-child.groovy │ ├── tuya-curtain.src │ │ └── tuya-curtain.groovy │ ├── tuya-plug-color.src │ │ └── tuya-plug-color.groovy │ └── tuya-color-light.src │ │ └── tuya-color-light.groovy └── streamorange58819 │ └── tuya-thermostat-new-app.src │ └── tuya-thermostat-new-app.groovy ├── README.md └── smartapps └── fison67 └── ty-connector.src └── ty-connector.groovy /imgs/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /imgs/tuya-packet-capture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fison67/TY-Connector/HEAD/imgs/tuya-packet-capture.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 chals 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /devicetypes/fison67/tuya-thermostat.src/tuya-thermostat.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Tuya Thermostat(v.0.0.1) 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2020 fison67@nate.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | 32 | metadata { 33 | definition (name: "Tuya Thermostat", namespace: "fison67", author: "fison67", mnmn:"fison67", vid: "9d1f02f1-771a-3f2e-86b6-6063281a9ba3", ocfDeviceType: "oic.d.thermostat") { 34 | capability "Actuator" 35 | capability "Temperature Measurement" 36 | capability "Thermostat Heating Setpoint" 37 | capability "Thermostat Operating State" 38 | capability "Thermostat Mode" 39 | capability "Refresh" 40 | } 41 | 42 | preferences { 43 | input name: "powerIndex", title:"Power Index" , type: "number", required: false, defaultValue: 1 44 | input name: "targetTempIndex", title:"Target Temperature Index" , type: "number", required: false, defaultValue: 2 45 | input name: "temperatureIndex", title:"Temperature Index" , type: "number", required: false, defaultValue: 3 46 | input name: "childLockIndex", title:"ChildLock Index" , type: "number", required: false, defaultValue: 6 47 | 48 | } 49 | 50 | } 51 | 52 | // parse events into attributes 53 | def parse(String description) { 54 | log.debug "Parsing '${description}'" 55 | } 56 | 57 | def installed(){ 58 | sendEvent(name: "supportedThermostatModes", value: ["heat", "off"]) 59 | sendEvent(name: "temperature", value: 15, unit: "C") 60 | } 61 | 62 | def updated() { 63 | log.debug "updated" 64 | } 65 | 66 | def refresh() { 67 | log.debug "refresh" 68 | sendEvent(name: "thermostatOperatingState", value: "heating") 69 | } 70 | 71 | def setInfo(String app_url, String address) { 72 | log.debug "${app_url}, ${address}" 73 | state.app_url = app_url 74 | state.id = address 75 | } 76 | 77 | def setStatus(data){ 78 | log.debug data 79 | def powerStatus = data[_getPowerIndex().toString()] 80 | if(powerStatus != null){ 81 | sendEvent(name:"thermostatMode", value: powerStatus ? "heat" : "off" ) 82 | } 83 | 84 | def targetTemperatureStatus = data[_getTargetTempIndex().toString()] 85 | if(targetTemperatureStatus != null){ 86 | sendEvent(name:"heatingSetpoint", value: (targetTemperatureStatus/2), unit: "C" ) 87 | } 88 | 89 | def temperature = data[_getTempIndex().toString()] 90 | if(temperature != null){ 91 | sendEvent(name:"temperature", value: (temperature/2), unit: "C" ) 92 | } 93 | sendEvent(name:"thermostatOperatingState", value: (targetTemperatureStatus >= temperature ? "heating" : "idle") ) 94 | 95 | } 96 | 97 | def setThermostatMode(mode){ 98 | if(mode == "heat"){ 99 | heat() 100 | }else if(mode == "off"){ 101 | off() 102 | } 103 | } 104 | 105 | def heat(){ 106 | log.debug "heat" 107 | processCommand("power", "on", _getPowerIndex().toString()) 108 | } 109 | 110 | def off(){ 111 | log.debug "off" 112 | processCommand("power", "off", _getPowerIndex().toString()) 113 | } 114 | 115 | def setHeatingSetpoint(setpoint){ 116 | processCommand("targetTemperature", setpoint, _getTargetTempIndex().toString()) 117 | } 118 | 119 | def processCommand(cmd, data, idx){ 120 | def body = [ 121 | "id": state.id, 122 | "cmd": cmd, 123 | "data": data, 124 | "idx": idx 125 | ] 126 | def options = makeCommand(body) 127 | log.debug options 128 | sendCommand(options, null) 129 | } 130 | 131 | def sendCommand(options, _callback){ 132 | def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: _callback]) 133 | sendHubCommand(myhubAction) 134 | } 135 | 136 | def makeCommand(body){ 137 | def options = [ 138 | "method": "POST", 139 | "path": "/api/control", 140 | "headers": [ 141 | "HOST": state.app_url, 142 | "Content-Type": "application/json" 143 | ], 144 | "body":body 145 | ] 146 | return options 147 | } 148 | 149 | def _getPowerIndex(){ 150 | if(powerIndex){ 151 | return powerIndex 152 | } 153 | return 1 154 | } 155 | 156 | def _getTargetTempIndex(){ 157 | if(targetTempIndex){ 158 | return targetTempIndex 159 | } 160 | return 2 161 | } 162 | 163 | def _getTempIndex(){ 164 | if(temperatureIndex){ 165 | return temperatureIndex 166 | } 167 | return 3 168 | } 169 | 170 | def _getChildLockIndex(){ 171 | if(childLockIndex){ 172 | return childLockIndex 173 | } 174 | return 6 175 | } 176 | 177 | 178 | -------------------------------------------------------------------------------- /devicetypes/fison67/tuya-plug.src/tuya-plug.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Tuya Plug (v.0.0.1) 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2019 fison67@nate.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | 32 | metadata { 33 | definition (name: "Tuya Plug", namespace: "fison67", author: "fison67") { 34 | capability "Switch" 35 | capability "Outlet" 36 | capability "Power Meter" 37 | capability "Energy Meter" 38 | capability "Refresh" 39 | 40 | attribute "lastCheckin", "Date" 41 | } 42 | 43 | simulator { } 44 | 45 | preferences { 46 | input name: "powerIDX", title:"Power Index" , type: "number", required: false, defaultValue: 1 47 | input name: "meterIDX", title:"Meter Index" , type: "number", required: false 48 | input name: "energyIDX", title:"Energy Index" , type: "number", required: false 49 | } 50 | 51 | tiles { 52 | multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){ 53 | tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { 54 | attributeState "on", label:'${name}', action:"off", icon:"https://github.com/fison67/DW-Connector/blob/master/icons/dawon-on.png?raw=true", backgroundColor:"#00a0dc", nextState:"turningOff" 55 | attributeState "off", label:'${name}', action:"on", icon:"https://github.com/fison67/DW-Connector/blob/master/icons/dawon-off.png?raw=true", backgroundColor:"#ffffff", nextState:"turningOn" 56 | 57 | attributeState "turningOn", label:'${name}', action:"off", icon:"https://github.com/fison67/DW-Connector/blob/master/icons/dawon-on.png?raw=true", backgroundColor:"#00a0dc", nextState:"turningOff" 58 | attributeState "turningOff", label:'${name}', action:"on", icon:"https://github.com/fison67/DW-Connector/blob/master/icons/dawon-off.png?raw=true", backgroundColor:"#ffffff", nextState:"turningOn" 59 | } 60 | 61 | tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") { 62 | attributeState("default", label:'Updated: ${currentValue}',icon: "st.Health & Wellness.health9") 63 | } 64 | } 65 | valueTile("power", "device.power", width:2, height:2, inactiveLabel: false, decoration: "flat" ) { 66 | state "power", label: 'Current\n${currentValue} w', defaultState: true 67 | } 68 | valueTile("energy", "device.energy", width:2, height:2, inactiveLabel: false, decoration: "flat" ) { 69 | state "energy", label: 'Total\n${currentValue}kWh', defaultState: true 70 | } 71 | main(["switch"]) 72 | details(["switch", "power", "energy"]) 73 | } 74 | } 75 | 76 | // parse events into attributes 77 | def parse(String description) { 78 | log.debug "Parsing '${description}'" 79 | } 80 | 81 | def setInfo(String app_url, String id) { 82 | log.debug "${app_url}, ${id}" 83 | state.app_url = app_url 84 | state.id = id 85 | } 86 | 87 | def setStatus(data){ 88 | log.debug data 89 | 90 | sendEvent(name: "switch", value: (data[powerIDX.toString()] ? "on" : "off")) 91 | 92 | if(meterIDX > 0){ 93 | sendEvent(name:"power", value: data[meterIDX.toString()] / 10) 94 | } 95 | if(energyIDX > 0){ 96 | sendEvent(name:"energy", value: data[energyIDX.toString()] / 10) 97 | } 98 | 99 | def now = new Date().format("yyyy-MM-dd HH:mm:ss", location.timeZone) 100 | sendEvent(name: "lastCheckin", value: now, displayed: false) 101 | } 102 | 103 | def on(){ 104 | log.debug "on" 105 | processCommand("power", "on", powerIDX.toString()) 106 | } 107 | 108 | def off(){ 109 | log.debug "off" 110 | processCommand("power", "off", powerIDX.toString()) 111 | } 112 | 113 | def timer(data, second){ 114 | log.debug "child Timer >> ${data} >> ${second} second" 115 | processCommand("timer", second, data) 116 | } 117 | 118 | def processCommand(cmd, data, idx){ 119 | def body = [ 120 | "id": state.id, 121 | "cmd": cmd, 122 | "data": data, 123 | "idx": idx 124 | ] 125 | def options = makeCommand(body) 126 | sendCommand(options, null) 127 | } 128 | 129 | def callback(physicalgraph.device.HubResponse hubResponse){ 130 | def msg 131 | try { 132 | msg = parseLanMessage(hubResponse.description) 133 | def jsonObj = new JsonSlurper().parseText(msg.body) 134 | } catch (e) { 135 | log.error "Exception caught while parsing data: "+e; 136 | } 137 | } 138 | 139 | def refresh(){} 140 | 141 | def updated(){} 142 | 143 | def sendCommand(options, _callback){ 144 | def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: _callback]) 145 | sendHubCommand(myhubAction) 146 | } 147 | 148 | def makeCommand(body){ 149 | def options = [ 150 | "method": "POST", 151 | "path": "/api/control", 152 | "headers": [ 153 | "HOST": state.app_url, 154 | "Content-Type": "application/json" 155 | ], 156 | "body":body 157 | ] 158 | return options 159 | } 160 | -------------------------------------------------------------------------------- /devicetypes/fison67/tuya-power-strip-child.src/tuya-power-strip-child.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Tuya Power Strip Child (v.0.0.1) 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2019 fison67@nate.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | 32 | metadata { 33 | definition (name: "Tuya Power Strip Child", namespace: "fison67", author: "fison67") { 34 | capability "Switch" 35 | attribute "lastCheckin", "Date" 36 | command "setTimer", ["number"] 37 | command "stop" 38 | } 39 | 40 | simulator { } 41 | 42 | preferences { } 43 | 44 | tiles { 45 | multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){ 46 | tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { 47 | attributeState "on", label:'${name}', action:"off", icon:"https://github.com/fison67/mi_connector/blob/master/icons/powerStrip_on.png?raw=true", backgroundColor:"#00a0dc", nextState:"turningOff" 48 | attributeState "off", label:'${name}', action:"on", icon:"https://github.com/fison67/mi_connector/blob/master/icons/powerStrip_off.png?raw=true", backgroundColor:"#ffffff", nextState:"turningOn" 49 | 50 | attributeState "turningOn", label:'${name}', action:"off", icon:"https://github.com/fison67/mi_connector/blob/master/icons/powerStrip_on.png?raw=true", backgroundColor:"#00a0dc", nextState:"turningOff" 51 | attributeState "turningOff", label:'${name}', action:"on", icon:"https://github.com/fison67/mi_connector/blob/master/icons/powerStrip_off.png?raw=true", backgroundColor:"#ffffff", nextState:"turningOn" 52 | } 53 | 54 | tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") { 55 | attributeState("default", label:'Updated: ${currentValue}',icon: "st.Health & Wellness.health9") 56 | } 57 | } 58 | valueTile("lastOn_label", "", decoration: "flat") { 59 | state "default", label:'Last\nOn' 60 | } 61 | valueTile("lastOn", "device.lastOn", decoration: "flat", width: 3, height: 1) { 62 | state "default", label:'${currentValue}' 63 | } 64 | standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { 65 | state "default", label:"" 66 | } 67 | valueTile("lastOff_label", "", decoration: "flat") { 68 | state "default", label:'Last\nOff' 69 | } 70 | valueTile("lastOff", "device.lastOff", decoration: "flat", width: 3, height: 1) { 71 | state "default", label:'${currentValue}' 72 | } 73 | valueTile("timer_label", "device.leftTime", decoration: "flat", width: 4, height: 1) { 74 | state "default", label:'Set a Timer\n${currentValue}' 75 | } 76 | controlTile("time", "device.time", "slider", height: 1, width: 1, range:"(0..120)") { 77 | state "time", action:"setTimer" 78 | } 79 | standardTile("tiemr0", "device.timeRemaining") { 80 | state "default", label: "OFF", action: "stop", icon:"st.Health & Wellness.health7", backgroundColor:"#c7bbc9" 81 | } 82 | main(["switch"]) 83 | details(["switch", "lastOn_label", "lastOn", "refresh", "lastOff_label", "lastOff", "timer_label", "time", "tiemr0"]) 84 | 85 | } 86 | } 87 | 88 | // parse events into attributes 89 | def parse(String description) { 90 | log.debug "Parsing '${description}'" 91 | } 92 | 93 | def setID(id){ 94 | state.id = id 95 | } 96 | 97 | def setStatus(type, data){ 98 | log.debug "Type[${type}], data >> ${data}" 99 | def now = new Date().format("yyyy-MM-dd HH:mm:ss", location.timeZone) 100 | sendEvent(name: "lastCheckin", value: now, displayed: false) 101 | 102 | switch(type){ 103 | case "power": 104 | if(device.currentValue("switch") != (data ? "on" : "off")){ 105 | sendEvent(name: (data ? "lastOn" : "lastOff"), value: now, displayed: false ) 106 | } 107 | sendEvent(name: "switch", value: data ? "on" : "off") 108 | break 109 | case "timer": 110 | def timeStr = msToTime(data) 111 | sendEvent(name:"leftTime", value: "${timeStr}", displayed: false) 112 | sendEvent(name:"time", value: Math.round(data/60), displayed: false) 113 | break 114 | } 115 | } 116 | 117 | def getID(){ 118 | return state.id 119 | } 120 | 121 | def on(){ 122 | parent.childOn(state.id) 123 | } 124 | 125 | def off(){ 126 | parent.childOff(state.id) 127 | } 128 | 129 | def stop(){ 130 | parent.childTimer(((state.id as int) + 8).toString(), 0) 131 | } 132 | 133 | def setTimer(second){ 134 | parent.childTimer(((state.id as int) + 8).toString(), second * 60) 135 | } 136 | 137 | def msToTime(duration) { 138 | def seconds = (duration%60).intValue() 139 | def minutes = ((duration/60).intValue() % 60).intValue() 140 | def hours = ( (duration/(60*60)).intValue() %24).intValue() 141 | 142 | hours = (hours < 10) ? "0" + hours : hours 143 | minutes = (minutes < 10) ? "0" + minutes : minutes 144 | seconds = (seconds < 10) ? "0" + seconds : seconds 145 | 146 | return hours + ":" + minutes + ":" + seconds 147 | } 148 | -------------------------------------------------------------------------------- /devicetypes/fison67/tuya-switch.src/tuya-switch.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Tuya Switch (v.0.0.2) 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2019 fison67@nate.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | 32 | metadata { 33 | definition (name: "Tuya Switch", namespace: "fison67", author: "fison67") { 34 | capability "Switch" 35 | capability "Refresh" 36 | 37 | attribute "lastCheckin", "Date" 38 | } 39 | 40 | simulator { } 41 | 42 | preferences { 43 | 44 | } 45 | 46 | tiles { 47 | multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){ 48 | tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { 49 | attributeState "on", label:'${name}', action:"off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" 50 | attributeState "off", label:'${name}', action:"on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" 51 | 52 | attributeState "turningOn", label:'${name}', action:"off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" 53 | attributeState "turningOff", label:'${name}', action:"on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" 54 | } 55 | 56 | tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") { 57 | attributeState("default", label:'Updated: ${currentValue}',icon: "st.Health & Wellness.health9") 58 | } 59 | } 60 | 61 | childDeviceTile("child-1", "child-1", height: 1, width: 6) 62 | childDeviceTile("child-2", "child-2", height: 1, width: 6) 63 | childDeviceTile("child-3", "child-3", height: 1, width: 6) 64 | childDeviceTile("child-4", "child-4", height: 1, width: 6) 65 | childDeviceTile("child-5", "child-5", height: 1, width: 6) 66 | childDeviceTile("child-6", "child-6", height: 1, width: 6) 67 | 68 | main(["switch"]) 69 | details(["switch", "child-1", "child-2", "child-3", "child-4", "child-5", "child-6"]) 70 | } 71 | } 72 | 73 | // parse events into attributes 74 | def parse(String description) { 75 | log.debug "Parsing '${description}'" 76 | } 77 | 78 | def setInfo(String app_url, String id) { 79 | log.debug "${app_url}, ${id}" 80 | state.app_url = app_url 81 | state.id = id 82 | } 83 | 84 | def installChild(data){ 85 | for(def i=0; i 100 | def dni = child.deviceNetworkId 101 | def id = dni.split("-")[3] 102 | log.debug "iD >> " + id 103 | if(data[id] != null){ 104 | child.setStatus("power", data[id]) 105 | if(data[id] == true){ 106 | powerOnCount++ 107 | } 108 | } 109 | } 110 | 111 | sendEvent(name: "switch", value: (powerOnCount > 0 ? "on" : "off")) 112 | 113 | def now = new Date().format("yyyy-MM-dd HH:mm:ss", location.timeZone) 114 | sendEvent(name: "lastCheckin", value: now, displayed: false) 115 | } 116 | 117 | def on(){ 118 | log.debug "on" 119 | processCommand("power", "on", "0") 120 | } 121 | 122 | def off(){ 123 | log.debug "off" 124 | processCommand("power", "off", "0") 125 | } 126 | 127 | def childOn(data){ 128 | log.debug "child On >> ${data}" 129 | processCommand("power", "on", data) 130 | } 131 | 132 | def childOff(data){ 133 | log.debug "child Off >> ${data}" 134 | processCommand("power", "off", data) 135 | } 136 | 137 | def processCommand(cmd, data, idx){ 138 | def body = [ 139 | "id": state.id, 140 | "cmd": cmd, 141 | "data": data, 142 | "idx": idx 143 | ] 144 | def options = makeCommand(body) 145 | sendCommand(options, null) 146 | } 147 | 148 | def callback(physicalgraph.device.HubResponse hubResponse){ 149 | def msg 150 | try { 151 | msg = parseLanMessage(hubResponse.description) 152 | def jsonObj = new JsonSlurper().parseText(msg.body) 153 | } catch (e) { 154 | log.error "Exception caught while parsing data: "+e; 155 | } 156 | } 157 | 158 | def refresh(){} 159 | 160 | def updated(){} 161 | 162 | def sendCommand(options, _callback){ 163 | def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: _callback]) 164 | sendHubCommand(myhubAction) 165 | } 166 | 167 | def makeCommand(body){ 168 | def options = [ 169 | "method": "POST", 170 | "path": "/api/control", 171 | "headers": [ 172 | "HOST": state.app_url, 173 | "Content-Type": "application/json" 174 | ], 175 | "body":body 176 | ] 177 | return options 178 | } 179 | -------------------------------------------------------------------------------- /devicetypes/streamorange58819/tuya-thermostat-new-app.src/tuya-thermostat-new-app.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Tuya Thermostat New App(v.0.0.1) 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2020 fison67@nate.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | 32 | metadata { 33 | definition (name: "Tuya Thermostat New App", namespace: "streamorange58819", author: "fison67", mnmn:"fison67", vid:"93a5623f-85a1-343a-b3f3-da5eeca220d2", ocfDeviceType: "oic.d.thermostat") { 34 | capability "Temperature Measurement" 35 | capability "Thermostat Cooling Setpoint" 36 | capability "Thermostat Heating Setpoint" 37 | capability "Thermostat Operating State" 38 | capability "Thermostat Mode" 39 | capability "streamorange58819.childlock" 40 | } 41 | 42 | preferences { 43 | input name: "powerIndex", title:"Power Index" , type: "number", required: false, defaultValue: 1 44 | input name: "targetTempIndex", title:"Target Temperature Index" , type: "number", required: false, defaultValue: 2 45 | input name: "temperatureIndex", title:"Temperature Index" , type: "number", required: false, defaultValue: 3 46 | input name: "ecoIndex", title:"Eco Index" , type: "number", required: false, defaultValue: 5 47 | input name: "childLockIndex", title:"ChildLock Index" , type: "number", required: false, defaultValue: 6 48 | } 49 | 50 | } 51 | 52 | // parse events into attributes 53 | def parse(String description) { 54 | log.debug "Parsing '${description}'" 55 | } 56 | 57 | def installed(){ 58 | sendEvent(name: "supportedThermostatModes", value: ["heat", "off", "eco"]) 59 | } 60 | 61 | def updated() { 62 | log.debug "updated" 63 | sendEvent(name: "supportedThermostatModes", value: ["heat", "off", "eco"]) 64 | } 65 | 66 | def refresh() { 67 | log.debug "refresh" 68 | } 69 | 70 | def childLock(){ 71 | log.debug "childlock" 72 | processCommand("childLock", "on", _getChildLockIndex().toString()) 73 | } 74 | 75 | def childUnlock(){ 76 | log.debug "childlock" 77 | processCommand("childLock", "off", _getChildLockIndex().toString()) 78 | } 79 | 80 | def setChildLock(lock){ 81 | log.debug "setChildLock: " + lock 82 | if(lock == "locked"){ 83 | childLock() 84 | }else if(lock == "unlocked"){ 85 | childUnlock() 86 | } 87 | } 88 | 89 | def setInfo(String app_url, String address) { 90 | log.debug "${app_url}, ${address}" 91 | state.app_url = app_url 92 | state.id = address 93 | } 94 | 95 | def setStatus(data){ 96 | log.debug data 97 | def powerStatus = data[_getPowerIndex().toString()] 98 | def eco = data[_getEcoIndex().toString()] 99 | if(powerStatus != null){ 100 | if(powerStatus && eco){ 101 | sendEvent(name:"thermostatMode", value: "eco" ) 102 | }else { 103 | sendEvent(name:"thermostatMode", value: powerStatus ? "heat" : "off" ) 104 | } 105 | } 106 | 107 | def targetTemperatureStatus = data[_getTargetTempIndex().toString()] 108 | if(targetTemperatureStatus != null){ 109 | sendEvent(name:"heatingSetpoint", value: (targetTemperatureStatus/2), unit: "C" ) 110 | } 111 | 112 | def temperature = data[_getTempIndex().toString()] 113 | if(temperature != null){ 114 | sendEvent(name:"temperature", value: (temperature/2), unit: "C" ) 115 | } 116 | sendEvent(name:"thermostatOperatingState", value: (targetTemperatureStatus >= temperature ? "heating" : "idle") ) 117 | 118 | def childLock = data[_getChildLockIndex().toString()] 119 | if(childLock != null){ 120 | sendEvent(name:"childlock", value: childLock ? "locked" : "unlocked" ) 121 | } 122 | 123 | } 124 | 125 | def setThermostatMode(mode){ 126 | if(mode == "heat"){ 127 | heat() 128 | }else if(mode == "off"){ 129 | off() 130 | }else if(mode == "eco"){ 131 | eco("on") 132 | } 133 | } 134 | 135 | def heat(){ 136 | log.debug "heat" 137 | if(device.currentValue("thermostatMode") == "eco"){ 138 | eco("off") 139 | } 140 | processCommand("power", "on", _getPowerIndex().toString()) 141 | } 142 | 143 | def off(){ 144 | log.debug "off" 145 | processCommand("power", "off", _getPowerIndex().toString()) 146 | } 147 | 148 | def eco(power){ 149 | log.debug "eco: " + power 150 | processCommand("eco", power, _getEcoIndex().toString()) 151 | } 152 | 153 | def setHeatingSetpoint(setpoint){ 154 | processCommand("targetTemperature", setpoint, _getTargetTempIndex().toString()) 155 | } 156 | 157 | def processCommand(cmd, data, idx){ 158 | def body = [ 159 | "id": state.id, 160 | "cmd": cmd, 161 | "data": data, 162 | "idx": idx 163 | ] 164 | def options = makeCommand(body) 165 | sendCommand(options, null) 166 | } 167 | 168 | def sendCommand(options, _callback){ 169 | def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: _callback]) 170 | sendHubCommand(myhubAction) 171 | } 172 | 173 | def makeCommand(body){ 174 | def options = [ 175 | "method": "POST", 176 | "path": "/api/control", 177 | "headers": [ 178 | "HOST": state.app_url, 179 | "Content-Type": "application/json" 180 | ], 181 | "body":body 182 | ] 183 | return options 184 | } 185 | 186 | def _getPowerIndex(){ 187 | if(powerIndex){ 188 | return powerIndex 189 | } 190 | return 1 191 | } 192 | 193 | def _getTargetTempIndex(){ 194 | if(targetTempIndex){ 195 | return targetTempIndex 196 | } 197 | return 2 198 | } 199 | 200 | def _getTempIndex(){ 201 | if(temperatureIndex){ 202 | return temperatureIndex 203 | } 204 | return 3 205 | } 206 | 207 | def _getChildLockIndex(){ 208 | if(childLockIndex){ 209 | return childLockIndex 210 | } 211 | return 6 212 | } 213 | 214 | def _getEcoIndex(){ 215 | if(ecoIndex){ 216 | return ecoIndex 217 | } 218 | return 5 219 | } 220 | -------------------------------------------------------------------------------- /devicetypes/fison67/tuya-power-strip.src/tuya-power-strip.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Tuya Power Strip (v.0.0.2) 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2019 fison67@nate.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | 32 | metadata { 33 | definition (name: "Tuya Power Strip", namespace: "fison67", author: "fison67") { 34 | capability "Switch" 35 | capability "Outlet" 36 | capability "Refresh" 37 | 38 | attribute "lastCheckin", "Date" 39 | } 40 | 41 | simulator { } 42 | 43 | preferences { 44 | 45 | } 46 | 47 | tiles { 48 | multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){ 49 | tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { 50 | attributeState "on", label:'${name}', action:"off", icon:"https://github.com/fison67/mi_connector/blob/master/icons/powerStrip_on.png?raw=true", backgroundColor:"#00a0dc", nextState:"turningOff" 51 | attributeState "off", label:'${name}', action:"on", icon:"https://github.com/fison67/mi_connector/blob/master/icons/powerStrip_off.png?raw=true", backgroundColor:"#ffffff", nextState:"turningOn" 52 | 53 | attributeState "turningOn", label:'${name}', action:"off", icon:"https://github.com/fison67/mi_connector/blob/master/icons/powerStrip_on.png?raw=true", backgroundColor:"#00a0dc", nextState:"turningOff" 54 | attributeState "turningOff", label:'${name}', action:"on", icon:"https://github.com/fison67/mi_connector/blob/master/icons/powerStrip_off.png?raw=true", backgroundColor:"#ffffff", nextState:"turningOn" 55 | } 56 | 57 | tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") { 58 | attributeState("default", label:'Updated: ${currentValue}',icon: "st.Health & Wellness.health9") 59 | } 60 | } 61 | 62 | childDeviceTile("child-1", "child-1", height: 1, width: 6) 63 | childDeviceTile("child-2", "child-2", height: 1, width: 6) 64 | childDeviceTile("child-3", "child-3", height: 1, width: 6) 65 | childDeviceTile("child-4", "child-4", height: 1, width: 6) 66 | childDeviceTile("child-usb", "child-usb", height: 1, width: 6) 67 | 68 | main(["switch"]) 69 | details(["switch", "child-1", "child-2", "child-3", "child-4", "child-usb"]) 70 | } 71 | } 72 | 73 | // parse events into attributes 74 | def parse(String description) { 75 | log.debug "Parsing '${description}'" 76 | } 77 | 78 | def setInfo(String app_url, String id) { 79 | log.debug "${app_url}, ${id}" 80 | state.app_url = app_url 81 | state.id = id 82 | } 83 | 84 | def installChild(data){ 85 | for(def i=0; i 107 | def dni = child.deviceNetworkId 108 | def id = dni.split("-")[3] 109 | if(data[id] != null){ 110 | child.setStatus("power", data[id]) 111 | if(data[id] == true){ 112 | powerOnCount++ 113 | } 114 | } 115 | 116 | def leftSecond = data[((id as int) + 8).toString()] 117 | if(leftSecond != null){ 118 | child.setStatus("timer", leftSecond) 119 | } 120 | } 121 | 122 | sendEvent(name: "switch", value: (powerOnCount > 0 ? "on" : "off")) 123 | 124 | def now = new Date().format("yyyy-MM-dd HH:mm:ss", location.timeZone) 125 | sendEvent(name: "lastCheckin", value: now, displayed: false) 126 | } 127 | 128 | def on(){ 129 | log.debug "on" 130 | processCommand("power", "on", "0") 131 | } 132 | 133 | def off(){ 134 | log.debug "off" 135 | processCommand("power", "off", "0") 136 | } 137 | 138 | def childOn(data){ 139 | log.debug "child On >> ${data}" 140 | processCommand("power", "on", data) 141 | } 142 | 143 | def childOff(data){ 144 | log.debug "child Off >> ${data}" 145 | processCommand("power", "off", data) 146 | } 147 | 148 | def childTimer(data, second){ 149 | log.debug "child Timer >> ${data} >> ${second} second" 150 | processCommand("timer", second, data) 151 | } 152 | 153 | def processCommand(cmd, data, idx){ 154 | def body = [ 155 | "id": state.id, 156 | "cmd": cmd, 157 | "data": data, 158 | "idx": idx 159 | ] 160 | def options = makeCommand(body) 161 | log.debug options 162 | sendCommand(options, null) 163 | } 164 | 165 | def callback(physicalgraph.device.HubResponse hubResponse){ 166 | def msg 167 | try { 168 | msg = parseLanMessage(hubResponse.description) 169 | def jsonObj = new JsonSlurper().parseText(msg.body) 170 | } catch (e) { 171 | log.error "Exception caught while parsing data: "+e; 172 | } 173 | } 174 | 175 | def refresh(){} 176 | 177 | def updated(){} 178 | 179 | def sendCommand(options, _callback){ 180 | def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: _callback]) 181 | sendHubCommand(myhubAction) 182 | } 183 | 184 | def makeCommand(body){ 185 | def options = [ 186 | "method": "POST", 187 | "path": "/api/control", 188 | "headers": [ 189 | "HOST": state.app_url, 190 | "Content-Type": "application/json" 191 | ], 192 | "body":body 193 | ] 194 | return options 195 | } 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tuya Connector 2 | 3 | Connector for Tuya devices with [SmartThings](https://www.smartthings.com/getting-started) 4 | 5 | Simplifies the setup of Tuya devices with SmartThings.
6 | 7 | If Tuya Connector is installed, virtual devices are registered automatically by the Tuya Connector SmartApp.
8 | 9 | You don't have to do anything to add Tuya devices in SmartThings IDE. 10 | 11 | ## Docker 12 | fison67/ty-connector:0.0.3
13 | fison67/ty-connector-rasp:0.0.3
14 | 15 | ## Support Type 16 | Smart Power Strip
17 | Smart Plug
18 | Smart Wall Switch

19 | 20 | ## Prerequisites 21 | * SmartThings account, Tuya account 22 | * Local server (Synology NAS, Raspberry Pi, Linux Server) with Docker installed 23 | * Tuya Device id, key ( Take it from packet on Android ) 24 |

25 | 26 | ## Donation 27 | If this project helps you, you can give me a cup of coffee
28 | 29 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/fison67) 30 |

31 | 32 | ## Install 33 | 34 | ### Take a localkey, devid, local ip address 35 | ``` 36 | Install a Packet Capture app on Android 37 | https://play.google.com/store/apps/details?id=app.greyshirts.sslcapture&hl=ko 38 | If you capture a packet, you can get these information. [ devid, localkey ] 39 | You have to get a local ip address from router. 40 | ``` 41 | ![total](./imgs/tuya-packet-capture.jpg) 42 |
43 | #### How to use app 44 | https://www.lynda.com/Android-tutorials/Capturing-packets-your-mobile-app/614303/635544-4.html 45 |

46 | 47 | ### Docker 48 | 49 | #### Synology NAS 50 | > Docker must be installed and running before continuing the installation.
51 | 52 | ``` 53 | 1. Open Docker app in Synology Web GUI 54 | 2. Select the Registry tab in the left menu 55 | 3. Search for "fison67" 56 | 4. Select and download the "fison67/ty-connector" image 57 | 5. Select the Image tab in the left menu and wait for the image to fully download 58 | 6. Select the downloaded image and click on the Launch button 59 | 7. Give the Container a sensible name (e.g. "ty-connector") 60 | 8. Click on Advanced Settings 61 | 9. Check the "auto-restart" checkbox in the Advanced Settings tab 62 | 10. Click on Add Folder in the Volume tab and create a new folder (e.g. /docker/ty-connector) for the configuration files. Fill in "/config" in the Mount path field. 63 | 11. Check the "Use the same network as Docker Host" checkbox in the Network tab 64 | 12. Click on Apply => Next => Apply 65 | ``` 66 | 67 | #### Raspberry Pi 68 | > Docker must be installed and running before continuing the installation. 69 | 70 | ``` 71 | sudo mkdir /docker 72 | sudo mkdir /docker/ty-connector 73 | sudo chown -R pi:pi /docker 74 | docker pull fison67/ty-connector-rasp:0.0.3 75 | docker run -d --restart=always -v /docker/ty-connector:/config -v /etc/localtime:/etc/localtime:ro --name=ty-connector-rasp --net=host fison67/ty-connector-rasp:0.0.3 76 | ``` 77 | 78 | ### Linux x86 x64 79 | 80 | > Docker must be installed and running before continuing the installation. 81 | 82 | ``` 83 | sudo mkdir /docker 84 | sudo mkdir /docker/ty-connector 85 | docker pull fison67/ty-connector:0.0.3 86 | docker run -d --restart=always -v /docker/ty-connector:/config -v /etc/localtime:/etc/localtime:ro --name=ty-connector --net=host fison67/ty-connector:0.0.3 87 | ``` 88 | 89 | 90 | #### TY Connector configuration 91 | ``` 92 | 1. Open TY Connector web settings page (http://X.X.X.X:30110/settings) default id/pwd [admin/12345] 93 | 2. Select a system address & Press a register button 94 | 3. Restart TY Connector Docker container 95 | 4. Open TY Connector smartapp & Fill in the server address & Press a Save button 96 | 5. Open TY Connector web settings page again. Check if there are smartthings api info is filled 97 | ``` 98 |

99 | ### Device Type Handler (DTH) 100 | 101 | #### Manual install 102 | ``` 103 | Go to the SmartThings IDE 104 | Click My Device Handlers 105 | Click Create New Device Handlers 106 | Copy content of file in the devicetypes/fison67 folder to the area 107 | Click Create 108 | Loop until all of file is registered 109 | ``` 110 | 111 | #### Install DTH using the GitHub Repo integration 112 | 113 | > Enable the GitHub integration before continuing the installation. Perform step 1 and 2 in the [SmartThings guide](https://docs.smartthings.com/en/latest/tools-and-ide/github-integration.html#step-1-enable-github-integration) to enable the GitHub integration for your SmartThings account. 114 | 115 | ``` 116 | 1. Go to the SmartThings IDE 117 | 2. Select the My Device Handlers tab 118 | 3. Click on the "Settings" button 119 | 4. Click on the "Add new repository" option and fill in the following information: 120 | Owner: fison67 121 | Name: TY-Connector 122 | Branch: master 123 | 5. Click on the "Save" button 124 | 6. Click on the "Update from Repo" button and select the "TY-Connector (master)" option 125 | 7. Check the checkbox for the device types you need (or all of them) in the "New (only in GitHub)" column 126 | 8. Check the "Publish" checkbox and click on the "Execute Update" button 127 | ``` 128 |

129 | 130 | ### SmartApp 131 | 132 | #### Manual install 133 | ``` 134 | Connect to the SmartThings IDE 135 | Click My SmartApps 136 | Click New SmartApp 137 | Click From Code 138 | Copy content of ty-connector.groovy & Paste 139 | Click Create 140 | Click My SmartApps & Edit properties (TY Connector) 141 | Enable OAuth 142 | Update Click 143 | ``` 144 | 145 | #### Install SmartApp using the GitHub Repo integration 146 | > Enable the GitHub integration before continuing the installation. Perform step 1 and 2 in the [SmartThings guide](https://docs.smartthings.com/en/latest/tools-and-ide/github-integration.html#step-1-enable-github-integration) to enable the GitHub integration for your SmartThings account. 147 | 148 | ``` 149 | 1. Go to the SmartThings IDE 150 | 2. Select the My SmartApps tab 151 | 3. Click on the Settings button 152 | 4. Click on the "Add new repository" option and fill in the following information: 153 | 154 | Owner: fison67 155 | Name: TY-Connector 156 | Branch: master 157 | 5. Click on the "Save" button 158 | 6. Click on the "Update from Repo" button and select the "TY-Connector (master)" option 159 | 7. Check the checkbox for the device types you need (or all of them) in the "New (only in GitHub)" column 160 | 8. Check the "Publish" checkbox and click on the "Execute Update" button 161 | 9. Select the My SmartApps tab 162 | 10. Click on the "Edit Properties" button for the TY Connector SmartApp (fison67 : TY Connector) 163 | 11. Click on the "OAuth" option and click on the "Enable OAuth" button 164 | 12. Click on the "Update" button 165 | ``` 166 | Step 3 and 4 are only needed if the repo has not been added earlier (e.g. in the DTH installation). 167 | 168 | 169 | 170 |

171 | 172 | ## Troubleshooting 173 | 174 | ### No update from device 175 | > If data can not be polled from the device: 176 | 1. Don't use a app anymore as Tuya or SmartLife. 177 | Cuz this TY Connector is working locally. It cann't be work together at same time. 178 | 179 | > If device's MCU firmware version is higher than 1.0.4: 180 | 1. Select a version 3.3(Not 3.1) on device add page. 181 | 182 | 183 | # Library 184 | - https://github.com/codetheweb/tuyapi 185 | 186 | -------------------------------------------------------------------------------- /devicetypes/fison67/tuya-switch-child.src/tuya-switch-child.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Tuya Switch Child (v.0.0.1) 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2019 fison67@nate.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | 32 | metadata { 33 | definition (name: "Tuya Switch Child", namespace: "fison67", author: "fison67") { 34 | capability "Switch" 35 | attribute "lastCheckin", "Date" 36 | command "setTimer", ["number"] 37 | command "stop" 38 | } 39 | 40 | simulator { } 41 | 42 | preferences { 43 | input name: "powerIDX", title:"Power Index" , type: "number", required: false 44 | } 45 | 46 | tiles { 47 | multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){ 48 | tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { 49 | attributeState "on", label:'${name}', action:"off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" 50 | attributeState "off", label:'${name}', action:"on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" 51 | 52 | attributeState "turningOn", label:'${name}', action:"off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" 53 | attributeState "turningOff", label:'${name}', action:"on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" 54 | } 55 | 56 | tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") { 57 | attributeState("default", label:'Updated: ${currentValue}',icon: "st.Health & Wellness.health9") 58 | } 59 | } 60 | valueTile("lastOn_label", "", decoration: "flat") { 61 | state "default", label:'Last\nOn' 62 | } 63 | valueTile("lastOn", "device.lastOn", decoration: "flat", width: 3, height: 1) { 64 | state "default", label:'${currentValue}' 65 | } 66 | standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { 67 | state "default", label:"" 68 | } 69 | valueTile("lastOff_label", "", decoration: "flat") { 70 | state "default", label:'Last\nOff' 71 | } 72 | valueTile("lastOff", "device.lastOff", decoration: "flat", width: 3, height: 1) { 73 | state "default", label:'${currentValue}' 74 | } 75 | valueTile("timer_label", "device.leftTime", decoration: "flat", width: 4, height: 1) { 76 | state "default", label:'Set a Timer\n${currentValue}' 77 | } 78 | controlTile("time", "device.time", "slider", height: 1, width: 1, range:"(0..120)") { 79 | state "time", action:"setTimer" 80 | } 81 | standardTile("tiemr0", "device.timeRemaining") { 82 | state "default", label: "OFF", action: "stop", icon:"st.Health & Wellness.health7", backgroundColor:"#c7bbc9" 83 | } 84 | main(["switch"]) 85 | details(["switch", "lastOn_label", "lastOn", "refresh", "lastOff_label", "lastOff", "timer_label", "time", "tiemr0"]) 86 | 87 | } 88 | } 89 | 90 | // parse events into attributes 91 | def parse(String description) { 92 | log.debug "Parsing '${description}'" 93 | } 94 | 95 | def setID(id){ 96 | state.id = id 97 | } 98 | 99 | def setStatus(type, data){ 100 | log.debug "Type[${type}], data >> ${data}" 101 | def now = new Date().format("yyyy-MM-dd HH:mm:ss", location.timeZone) 102 | sendEvent(name: "lastCheckin", value: now, displayed: false) 103 | 104 | switch(type){ 105 | case "power": 106 | if(device.currentValue("switch") != (data ? "on" : "off")){ 107 | sendEvent(name: (data ? "lastOn" : "lastOff"), value: now, displayed: false ) 108 | } 109 | sendEvent(name: "switch", value: data ? "on" : "off") 110 | break 111 | case "timer": 112 | def timeStr = msToTime(data) 113 | sendEvent(name:"leftTime", value: "${timeStr}", displayed: false) 114 | sendEvent(name:"time", value: Math.round(data/60), displayed: false) 115 | break 116 | } 117 | } 118 | 119 | def getID(){ 120 | return state.id 121 | } 122 | 123 | def on(){ 124 | parent.childOn(state.id) 125 | } 126 | 127 | def off(){ 128 | parent.childOff(state.id) 129 | } 130 | 131 | def stop() { 132 | unschedule() 133 | state.timerCount = 0 134 | updateTimer() 135 | } 136 | 137 | def timer(){ 138 | if(state.timerCount > 0){ 139 | state.timerCount = state.timerCount - 30; 140 | if(state.timerCount <= 0){ 141 | if(device.currentValue("switch") == "on"){ 142 | off() 143 | } 144 | }else{ 145 | runIn(30, timer) 146 | } 147 | updateTimer() 148 | } 149 | } 150 | 151 | def updateTimer(){ 152 | def timeStr = msToTime(state.timerCount) 153 | sendEvent(name:"leftTime", value: "${timeStr}") 154 | sendEvent(name:"timeRemaining", value: Math.round(state.timerCount/60)) 155 | } 156 | 157 | def processTimer(second){ 158 | if(state.timerCount == null){ 159 | state.timerCount = second; 160 | runIn(30, timer) 161 | }else if(state.timerCount == 0){ 162 | state.timerCount = second; 163 | runIn(30, timer) 164 | }else{ 165 | state.timerCount = second 166 | } 167 | updateTimer() 168 | } 169 | 170 | def setTimer(time) { 171 | if(time > 0){ 172 | log.debug "Set a Timer ${time}Mins" 173 | processTimer(time * 60) 174 | setPowerByStatus(true) 175 | } 176 | } 177 | def msToTime(duration) { 178 | def seconds = (duration%60).intValue() 179 | def minutes = ((duration/60).intValue() % 60).intValue() 180 | def hours = ( (duration/(60*60)).intValue() %24).intValue() 181 | 182 | hours = (hours < 10) ? "0" + hours : hours 183 | minutes = (minutes < 10) ? "0" + minutes : minutes 184 | seconds = (seconds < 10) ? "0" + seconds : seconds 185 | 186 | return hours + ":" + minutes + ":" + seconds 187 | } 188 | 189 | def setPowerByStatus(turnOn){ 190 | if(device.currentValue("switch") == (turnOn ? "off" : "on")){ 191 | if(turnOn){ 192 | on() 193 | }else{ 194 | off() 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /devicetypes/fison67/tuya-curtain.src/tuya-curtain.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Tuya Plug (v.0.0.1) 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2019 fison67@nate.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | 32 | metadata { 33 | definition (name: "Tuya Curtain", namespace: "fison67", author: "fison67") { 34 | capability "Actuator" 35 | capability "Switch Level" 36 | capability "windowShade" 37 | capability "Refresh" 38 | 39 | attribute "lastCheckin", "Date" 40 | 41 | command "stop" 42 | } 43 | 44 | simulator { } 45 | 46 | preferences { 47 | input name: "controlIndex", title:"Curtain Control Index" , type: "number", required: false, defaultValue: 1 48 | input name: "levelIndex", title:"Curtain Level Index" , type: "number", required: false, defaultValue: 2 49 | input name: "levelStatusIndex", title:"Curtain Level Status Index" , type: "number", required: false, defaultValue: 3 50 | input name: "curtainStatusIndex", title:"Curtain Status Index" , type: "number", required: false, defaultValue: 7 51 | } 52 | 53 | tiles { 54 | multiAttributeTile(name:"windowShade", type: "windowShade", width: 6, height: 4, canChangeIcon: true){ 55 | tileAttribute ("device.windowShade", key: "PRIMARY_CONTROL") { 56 | attributeState "closed", label: 'closed', action: "open", icon: "st.doors.garage.garage-closed", backgroundColor: "#A8A8C6", nextState: "opening" 57 | attributeState "open", label: 'open', action: "close", icon: "st.doors.garage.garage-open", backgroundColor: "#F7D73E", nextState: "closing" 58 | attributeState "closing", label: '${name}', action: "open", icon: "st.contact.contact.closed", backgroundColor: "#B9C6A8" 59 | attributeState "opening", label: '${name}', action: "close", icon: "st.contact.contact.open", backgroundColor: "#D4CF14" 60 | attributeState "partially open", label: 'partially\nopen', action: "close", icon: "st.doors.garage.garage-closing", backgroundColor: "#D4ACEE", nextState: "closing" 61 | } 62 | 63 | tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") { 64 | attributeState("default", label:'Updated: ${currentValue}') 65 | } 66 | 67 | tileAttribute ("device.level", key: "SLIDER_CONTROL") { 68 | attributeState "level", action:"setLevel" 69 | } 70 | } 71 | 72 | standardTile("open", "device", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { 73 | state("on", label: 'open', action: "open", icon: "st.doors.garage.garage-open") 74 | } 75 | 76 | standardTile("stop", "device", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { 77 | state("stop", label: 'stop', action: "stop", icon: "st.illuminance.illuminance.dark") 78 | } 79 | 80 | standardTile("close", "device", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { 81 | state("off", label: 'close', action: "close", icon: "st.doors.garage.garage-closed") 82 | } 83 | 84 | standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { 85 | state "default", label:"", action:"refresh", icon:"st.secondary.refresh" 86 | } 87 | } 88 | } 89 | 90 | // parse events into attributes 91 | def parse(String description) { 92 | log.debug "Parsing '${description}'" 93 | } 94 | 95 | def setInfo(String app_url, String id) { 96 | log.debug "${app_url}, ${id}" 97 | state.app_url = app_url 98 | state.id = id 99 | } 100 | 101 | def setStatus(data){ 102 | log.debug data 103 | 104 | def curtainLevelStatus = data[_getLevelStatusIndex().toString()] 105 | if(curtainLevelStatus != null){ 106 | sendEvent(name:"windowShade", value: (curtainLevelStatus == 0 ? "closed" : ( curtainLevelStatus == 100 ? "open" : "partially open" )) ) 107 | sendEvent(name:"level", value: curtainLevelStatus ) 108 | } 109 | 110 | def now = new Date().format("yyyy-MM-dd HH:mm:ss", location.timeZone) 111 | sendEvent(name: "lastCheckin", value: now, displayed: false) 112 | } 113 | 114 | def setLevel(level){ 115 | processCommand("curtainLevel", level, _getLevelIndex()) 116 | // sendEvent(name:"windowShade", value: (level < 50 ? "opening" : "closing") ) 117 | } 118 | 119 | def on(){ 120 | processCommand("control", "open", _getControlIndex()) 121 | // sendEvent(name:"windowShade", value: "opening") 122 | } 123 | 124 | def off(){ 125 | processCommand("control", "close", _getControlIndex()) 126 | // sendEvent(name:"windowShade", value: "closing") 127 | } 128 | 129 | def open(){ 130 | on() 131 | } 132 | 133 | def close(){ 134 | off() 135 | } 136 | 137 | def stop(){ 138 | processCommand("control", "stop", _getControlIndex()) 139 | } 140 | 141 | def timer(data, second){ 142 | log.debug "child Timer >> ${data} >> ${second} second" 143 | processCommand("timer", second, data) 144 | } 145 | 146 | def processCommand(cmd, data, idx){ 147 | log.debug "processCommand: " + cmd 148 | def body = [ 149 | "id": state.id, 150 | "cmd": cmd, 151 | "data": data, 152 | "idx": idx 153 | ] 154 | def options = makeCommand(body) 155 | log.debug options 156 | sendCommand(options, null) 157 | } 158 | 159 | def callback(physicalgraph.device.HubResponse hubResponse){ 160 | def msg 161 | try { 162 | msg = parseLanMessage(hubResponse.description) 163 | def jsonObj = new JsonSlurper().parseText(msg.body) 164 | } catch (e) { 165 | log.error "Exception caught while parsing data: "+e; 166 | } 167 | } 168 | 169 | def refresh(){} 170 | 171 | def updated(){} 172 | 173 | def sendCommand(options, _callback){ 174 | def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: _callback]) 175 | sendHubCommand(myhubAction) 176 | } 177 | 178 | def makeCommand(body){ 179 | def options = [ 180 | "method": "POST", 181 | "path": "/api/control", 182 | "headers": [ 183 | "HOST": state.app_url, 184 | "Content-Type": "application/json" 185 | ], 186 | "body":body 187 | ] 188 | log.debug options 189 | return options 190 | } 191 | 192 | def _getControlIndex(){ 193 | if(controlIndex){ 194 | return controlIndex 195 | } 196 | return 1 197 | } 198 | 199 | def _getLevelIndex(){ 200 | if(levelIndex){ 201 | return levelIndex 202 | } 203 | return 2 204 | } 205 | 206 | def _getLevelStatusIndex(){ 207 | if(levelStatusIndex){ 208 | return levelStatusIndex 209 | } 210 | return 3 211 | } 212 | 213 | def _getStatusIndex(){ 214 | if(curtainStatusIndex){ 215 | return curtainStatusIndex 216 | } 217 | return 7 218 | } 219 | -------------------------------------------------------------------------------- /devicetypes/fison67/tuya-plug-color.src/tuya-plug-color.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Tuya Plug Color (v.0.0.1) 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2019 fison67@nate.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | 32 | metadata { 33 | definition (name: "Tuya Plug Color", namespace: "fison67", author: "fison67") { 34 | capability "Switch" 35 | capability "Outlet" 36 | capability "Color Control" 37 | capability "Power Meter" 38 | capability "Energy Meter" 39 | capability "Refresh" 40 | 41 | attribute "lastCheckin", "Date" 42 | attribute "led", "string" 43 | 44 | command "ledOn" 45 | command "ledOff" 46 | 47 | command "setTimer", ["number"] 48 | command "stop" 49 | } 50 | 51 | simulator { } 52 | 53 | preferences { 54 | input name: "powerIDX", title:"Power Index" , type: "number", required: false, defaultValue: 1 55 | input name: "colorIDX", title:"Color Index" , type: "number", required: false, defaultValue: 5 56 | input name: "meterIDX", title:"Meter Index" , type: "number", required: false 57 | input name: "energyIDX", title:"Energy Index" , type: "number", required: false 58 | input name: "ledIDX", title:"LED Index" , type: "number", required: false 59 | input name: "timerIDX", title:"Timer Index" , type: "number", required: false 60 | } 61 | 62 | tiles { 63 | multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){ 64 | tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { 65 | attributeState "on", label:'${name}', action:"off", icon:"https://github.com/fison67/DW-Connector/blob/master/icons/dawon-on.png?raw=true", backgroundColor:"#00a0dc", nextState:"turningOff" 66 | attributeState "off", label:'${name}', action:"on", icon:"https://github.com/fison67/DW-Connector/blob/master/icons/dawon-off.png?raw=true", backgroundColor:"#ffffff", nextState:"turningOn" 67 | 68 | attributeState "turningOn", label:'${name}', action:"off", icon:"https://github.com/fison67/DW-Connector/blob/master/icons/dawon-on.png?raw=true", backgroundColor:"#00a0dc", nextState:"turningOff" 69 | attributeState "turningOff", label:'${name}', action:"on", icon:"https://github.com/fison67/DW-Connector/blob/master/icons/dawon-off.png?raw=true", backgroundColor:"#ffffff", nextState:"turningOn" 70 | } 71 | 72 | tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") { 73 | attributeState("default", label:'Updated: ${currentValue}',icon: "st.Health & Wellness.health9") 74 | } 75 | 76 | tileAttribute ("device.level", key: "SLIDER_CONTROL") { 77 | attributeState "level", action:"switch level.setLevel" 78 | } 79 | 80 | tileAttribute ("device.color", key: "COLOR_CONTROL") { 81 | attributeState "color", action:"setColor" 82 | } 83 | } 84 | 85 | standardTile("led", "device.led", inactiveLabel: false, width: 2, height: 2, canChangeIcon: true) { 86 | state "on", label:'${name}', action:"ledOff", backgroundColor:"#00a0dc", nextState:"turningOff" 87 | state "off", label:'${name}', action:"ledOn", backgroundColor:"#ffffff", nextState:"turningOn" 88 | 89 | state "turningOn", label:'....', action:"ledOff", backgroundColor:"#00a0dc", nextState:"turningOff" 90 | state "turningOff", label:'....', action:"ledOn", backgroundColor:"#ffffff", nextState:"turningOn" 91 | } 92 | valueTile("power", "device.power", width:2, height:2, inactiveLabel: false, decoration: "flat" ) { 93 | state "power", label: 'Current\n${currentValue} w', defaultState: true 94 | } 95 | valueTile("energy", "device.energy", width:2, height:2, inactiveLabel: false, decoration: "flat" ) { 96 | state "energy", label: 'Total\n${currentValue}kWh', defaultState: true 97 | } 98 | valueTile("timer_label", "device.leftTime", decoration: "flat", width: 4, height: 1) { 99 | state "default", label:'Set a Timer\n${currentValue}' 100 | } 101 | controlTile("time", "device.time", "slider", height: 1, width: 1, range:"(0..120)") { 102 | state "time", action:"setTimer" 103 | } 104 | standardTile("tiemr0", "device.timeRemaining") { 105 | state "default", label: "OFF", action: "stop", icon:"st.Health & Wellness.health7", backgroundColor:"#c7bbc9" 106 | } 107 | main(["switch"]) 108 | details(["switch", "led", "power", "energy", "timer_label", "time", "tiemr0"]) 109 | } 110 | } 111 | 112 | // parse events into attributes 113 | def parse(String description) { 114 | log.debug "Parsing '${description}'" 115 | } 116 | 117 | def setInfo(String app_url, String id) { 118 | log.debug "${app_url}, ${id}" 119 | state.app_url = app_url 120 | state.id = id 121 | } 122 | 123 | def setStatus(data){ 124 | log.debug data 125 | 126 | sendEvent(name: "switch", value: (data[powerIDX.toString()] ? "on" : "off")) 127 | 128 | if(meterIDX > 0){ 129 | sendEvent(name:"power", value: data[meterIDX.toString()] / 10) 130 | } 131 | if(energyIDX > 0){ 132 | sendEvent(name:"energy", value: data[energyIDX.toString()] / 10) 133 | } 134 | if(ledIDX > 0){ 135 | sendEvent(name:"led", value: (data[ledIDX.toString()] ? "on" : "off")) 136 | } 137 | if(timerIDX > 0){ 138 | def timeStr = msToTime(data[timerIDX.toString()]) 139 | sendEvent(name:"leftTime", value: "${timeStr}", displayed: false) 140 | sendEvent(name:"time", value: Math.round(data/60), displayed: false) 141 | } 142 | 143 | def now = new Date().format("yyyy-MM-dd HH:mm:ss", location.timeZone) 144 | sendEvent(name: "lastCheckin", value: now, displayed: false) 145 | } 146 | 147 | def setColor(color){ 148 | log.debug "setColor >> ${color.hex}" 149 | processCommand("color", color.hex, colorIDX.toString()) 150 | } 151 | 152 | def setLevel(brightness){ 153 | log.debug "setLevel >> ${brightness}" 154 | processCommand("brightness", brightness, colorIDX.toString()) 155 | } 156 | 157 | def on(){ 158 | log.debug "on" 159 | processCommand("power", "on", powerIDX.toString()) 160 | } 161 | 162 | def off(){ 163 | log.debug "off" 164 | processCommand("power", "off", powerIDX.toString()) 165 | } 166 | 167 | def ledOn(){ 168 | log.debug "ledOn" 169 | processCommand("power", "on", ledIDX.toString()) 170 | } 171 | 172 | def ledOff(){ 173 | log.debug "ledOff" 174 | processCommand("power", "off", ledIDX.toString()) 175 | } 176 | 177 | def setTimer(second){ 178 | log.debug "setTimer >> ${second} second" 179 | processCommand("timer", second, timerIDX.toString()) 180 | } 181 | 182 | def stop(){ 183 | processCommand("timer", 0, timerIDX.toString()) 184 | } 185 | 186 | def processCommand(cmd, data, idx){ 187 | def body = [ 188 | "id": state.id, 189 | "cmd": cmd, 190 | "data": data, 191 | "idx": idx 192 | ] 193 | def options = makeCommand(body) 194 | sendCommand(options, null) 195 | } 196 | 197 | def callback(physicalgraph.device.HubResponse hubResponse){ 198 | def msg 199 | try { 200 | msg = parseLanMessage(hubResponse.description) 201 | def jsonObj = new JsonSlurper().parseText(msg.body) 202 | } catch (e) { 203 | log.error "Exception caught while parsing data: "+e; 204 | } 205 | } 206 | 207 | def refresh(){} 208 | 209 | def updated(){} 210 | 211 | def sendCommand(options, _callback){ 212 | def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: _callback]) 213 | sendHubCommand(myhubAction) 214 | } 215 | 216 | def makeCommand(body){ 217 | def options = [ 218 | "method": "POST", 219 | "path": "/api/control", 220 | "headers": [ 221 | "HOST": state.app_url, 222 | "Content-Type": "application/json" 223 | ], 224 | "body":body 225 | ] 226 | return options 227 | } 228 | 229 | def msToTime(duration) { 230 | def seconds = (duration%60).intValue() 231 | def minutes = ((duration/60).intValue() % 60).intValue() 232 | def hours = ( (duration/(60*60)).intValue() %24).intValue() 233 | 234 | hours = (hours < 10) ? "0" + hours : hours 235 | minutes = (minutes < 10) ? "0" + minutes : minutes 236 | seconds = (seconds < 10) ? "0" + seconds : seconds 237 | 238 | return hours + ":" + minutes + ":" + seconds 239 | } 240 | -------------------------------------------------------------------------------- /smartapps/fison67/ty-connector.src/ty-connector.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * TY Connector (v.0.0.6) 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2019 fison67@nate.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | import groovy.json.JsonOutput 32 | import groovy.transform.Field 33 | 34 | definition( 35 | name: "TY Connector", 36 | namespace: "fison67", 37 | author: "fison67", 38 | description: "A Connector between Tuya and ST", 39 | category: "My Apps", 40 | iconUrl: "https://apprecs.org/gp/images/app-icons/300/85/com.tuya.smart.jpg", 41 | iconX2Url: "https://apprecs.org/gp/images/app-icons/300/85/com.tuya.smart.jpg", 42 | iconX3Url: "https://apprecs.org/gp/images/app-icons/300/85/com.tuya.smart.jpg", 43 | oauth: true 44 | ) 45 | 46 | preferences { 47 | page(name: "mainPage") 48 | page(name: "monitorPage") 49 | page(name: "langPage") 50 | } 51 | 52 | 53 | def mainPage() { 54 | dynamicPage(name: "mainPage", title: "Tuya Connector", nextPage: null, uninstall: true, install: true) { 55 | section("Request New Devices"){ 56 | input "address", "text", title: "Server address", required: true, description:"IP:Port. ex)192.168.0.100:30040" 57 | href url:"http://${settings.address}", style:"embedded", required:false, title:"Local Management", description:"This makes you easy to setup" 58 | } 59 | 60 | section() { 61 | paragraph "View this SmartApp's configuration to use it in other places." 62 | href url:"${apiServerUrl("/api/smartapps/installations/${app.id}/config?access_token=${state.accessToken}")}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\"" 63 | } 64 | } 65 | } 66 | 67 | def installed() { 68 | log.debug "Installed with settings: ${settings}" 69 | 70 | if (!state.accessToken) { 71 | createAccessToken() 72 | } 73 | 74 | state.dniHeaderStr = "ty-connector-" 75 | 76 | initialize() 77 | } 78 | 79 | def updated() { 80 | log.debug "Updated with settings: ${settings}" 81 | 82 | initialize() 83 | setAPIAddress() 84 | } 85 | 86 | /** 87 | * deviceNetworkID : Reference Device. Not Remote Device 88 | */ 89 | def getDeviceToNotifyList(deviceNetworkID){ 90 | def list = [] 91 | state.monitorMap.each{ targetNetworkID, _data -> 92 | if(deviceNetworkID == _data.id){ 93 | def item = [:] 94 | item['id'] = state.dniHeaderStr + targetNetworkID 95 | item['data'] = _data.data 96 | list.push(item) 97 | } 98 | } 99 | return list 100 | } 101 | 102 | def setAPIAddress(){ 103 | def list = getChildDevices() 104 | list.each { child -> 105 | try{ 106 | child.setAddress(settings.address) 107 | }catch(e){ 108 | } 109 | } 110 | } 111 | 112 | def initialize() { 113 | log.debug "initialize" 114 | 115 | def options = [ 116 | "method": "POST", 117 | "path": "/settings/api/smartthings", 118 | "headers": [ 119 | "HOST": settings.address, 120 | "Content-Type": "application/json" 121 | ], 122 | "body":[ 123 | "app_url":"${apiServerUrl}/api/smartapps/installations/", 124 | "app_id":app.id, 125 | "access_token":state.accessToken 126 | ] 127 | ] 128 | 129 | def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: null]) 130 | sendHubCommand(myhubAction) 131 | } 132 | 133 | def dataCallback(physicalgraph.device.HubResponse hubResponse) { 134 | def msg, json, status 135 | try { 136 | msg = parseLanMessage(hubResponse.description) 137 | status = msg.status 138 | json = msg.json 139 | log.debug "${json}" 140 | } catch (e) { 141 | logger('warn', "Exception caught while parsing data: "+e); 142 | } 143 | } 144 | 145 | def getDataList(){ 146 | def options = [ 147 | "method": "GET", 148 | "path": "/requestDevice", 149 | "headers": [ 150 | "HOST": settings.address, 151 | "Content-Type": "application/json" 152 | ] 153 | ] 154 | def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: dataCallback]) 155 | sendHubCommand(myhubAction) 156 | } 157 | 158 | def addDevice(){ 159 | def data = request.JSON 160 | log.debug data 161 | def id = data.id 162 | def name = data.name 163 | def type = data.type 164 | def result = [result: 'success'] 165 | 166 | def dni = state.dniHeaderStr + id.toLowerCase() 167 | log.debug("Try >> ADD Tuya Device id=${id} name=${name}, dni=${dni}") 168 | 169 | def list = getChildDevices(); 170 | def existDevice = false; 171 | list.each { child -> 172 | def _dni = child.deviceNetworkId 173 | if(_dni == dni){ 174 | existDevice = true 175 | } 176 | } 177 | 178 | if(!existDevice){ 179 | def dth = ""; 180 | switch(type){ 181 | case "power-strip": 182 | dth = "Tuya Power Strip" 183 | break 184 | case "plug-default": 185 | dth = "Tuya Plug" 186 | break 187 | case "plug-color": 188 | dth = "Tuya Plug Color" 189 | break 190 | case "switch": 191 | dth = "Tuya Switch" 192 | break 193 | case "color-light": 194 | dth = "Tuya Color Light" 195 | break 196 | case "curtain": 197 | dth = "Tuya Curtain" 198 | break 199 | case "thermostat": 200 | dth = "Tuya Thermostat" 201 | break 202 | } 203 | try{ 204 | def childDevice = addChildDevice("fison67", dth, dni, location.hubs[0].id, [ 205 | "label": name 206 | ]) 207 | childDevice.setInfo(settings.address, id) 208 | childDevice.installChild(data) 209 | }catch(err){ 210 | log.error err 211 | result = [result: 'fail'] 212 | } 213 | log.debug "Success >> ADD Device DNI=${dni} ${name}" 214 | }else{ 215 | result = [result: 'exist'] 216 | } 217 | 218 | def resultString = new groovy.json.JsonOutput().toJson(result) 219 | render contentType: "application/javascript", data: resultString 220 | } 221 | 222 | def updateDevice(){ 223 | def data = request.JSON 224 | log.debug data 225 | def id = data.id 226 | def dni = state.dniHeaderStr + id.toLowerCase() 227 | def chlid = getChildDevice(dni) 228 | if(chlid){ 229 | chlid.setStatus(data.data) 230 | } 231 | def resultString = new groovy.json.JsonOutput().toJson("result":true) 232 | render contentType: "application/javascript", data: resultString 233 | } 234 | 235 | def getDeviceList(){ 236 | def list = getChildDevices(); 237 | def resultList = []; 238 | list.each { child -> 239 | def dni = child.deviceNetworkId 240 | resultList.push( dni.substring(13, dni.length()) ); 241 | } 242 | 243 | def configString = new groovy.json.JsonOutput().toJson("list":resultList) 244 | render contentType: "application/javascript", data: configString 245 | } 246 | 247 | def authError() { 248 | [error: "Permission denied"] 249 | } 250 | 251 | def renderConfig() { 252 | def configJson = new groovy.json.JsonOutput().toJson([ 253 | description: "Tuya Connector API", 254 | platforms: [ 255 | [ 256 | platform: "SmartThings Tuya Connector", 257 | name: "Tuya Connector", 258 | app_url: apiServerUrl("/api/smartapps/installations/"), 259 | app_id: app.id, 260 | access_token: state.accessToken 261 | ] 262 | ], 263 | ]) 264 | 265 | def configString = new groovy.json.JsonOutput().prettyPrint(configJson) 266 | render contentType: "text/plain", data: configString 267 | } 268 | 269 | mappings { 270 | if (!params.access_token || (params.access_token && params.access_token != state.accessToken)) { 271 | path("/config") { action: [GET: "authError"] } 272 | path("/list") { action: [GET: "authError"] } 273 | path("/update") { action: [POST: "authError"] } 274 | path("/add") { action: [POST: "authError"] } 275 | 276 | } else { 277 | path("/config") { action: [GET: "renderConfig"] } 278 | path("/list") { action: [GET: "getDeviceList"] } 279 | path("/update") { action: [POST: "updateDevice"] } 280 | path("/add") { action: [POST: "addDevice"] } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /devicetypes/fison67/tuya-color-light.src/tuya-color-light.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Tuya Light (v.0.0.1) 3 | * 4 | * MIT License 5 | * 6 | * Copyright (c) 2019 fison67@nate.com 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | import groovy.json.JsonSlurper 31 | import java.awt.Color 32 | 33 | metadata { 34 | definition (name: "Tuya Color Light", namespace: "fison67", author: "fison67", vid: "generic-rgb-color-bulb", ocfDeviceType: "oic.d.light") { 35 | capability "Switch" 36 | capability "Light" 37 | capability "Color Control" 38 | capability "ColorTemperature" 39 | capability "Switch Level" 40 | capability "Refresh" 41 | 42 | attribute "lastCheckin", "Date" 43 | 44 | } 45 | 46 | simulator { } 47 | 48 | preferences { 49 | input name: "powerIDX", title:"Power Index" , type: "number", required: false, defaultValue: 1 50 | input name: "modeIDX", title:"Color Mode Index" , type: "number", required: false, defaultValue: 2 51 | input name: "brightnessIDX", title:"Brightness Index" , type: "number", required: false, defaultValue: 3 52 | input name: "colorIDX", title:"Color Index" , type: "number", required: false, defaultValue: 5 53 | } 54 | 55 | tiles { 56 | multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){ 57 | tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { 58 | attributeState "on", label:'${name}', action:"switch.off", icon:"https://postfiles.pstatic.net/MjAxODAzMjdfNjgg/MDAxNTIyMTUzOTg0NzMx.YZwxpTpbz-9oqHVDLhcLyOcdWvn6TE0RPdpB_D7kWzwg.97WcX3XnDGPr5kATUZhhGRYJ1IO1MNV2pbDvg8DXruog.PNG.shin4299/Yeelight_tile_on.png?type=w580", backgroundColor:"#00a0dc", nextState:"turningOff" 59 | attributeState "off", label:'${name}', action:"switch.on", icon:"https://postfiles.pstatic.net/MjAxODAzMjdfMTA0/MDAxNTIyMTUzOTg0NzIz.62-IbE4S7wAOxe3hufTJctU8mlQmrIUQztDaSTnf3kog.sxe2rqceUxFEPqrfYZ_DLkjxM5IPSotCqhErG87DI0Mg.PNG.shin4299/Yeelight_tile_off.png?type=w580", backgroundColor:"#ffffff", nextState:"turningOn" 60 | 61 | attributeState "turningOn", label:'${name}', action:"switch.off", icon:"https://postfiles.pstatic.net/MjAxODAzMjdfMTA0/MDAxNTIyMTUzOTg0NzIz.62-IbE4S7wAOxe3hufTJctU8mlQmrIUQztDaSTnf3kog.sxe2rqceUxFEPqrfYZ_DLkjxM5IPSotCqhErG87DI0Mg.PNG.shin4299/Yeelight_tile_off.png?type=w580", backgroundColor:"#00a0dc", nextState:"turningOff" 62 | attributeState "turningOff", label:'${name}', action:"switch.ofn", icon:"https://postfiles.pstatic.net/MjAxODAzMjdfNjgg/MDAxNTIyMTUzOTg0NzMx.YZwxpTpbz-9oqHVDLhcLyOcdWvn6TE0RPdpB_D7kWzwg.97WcX3XnDGPr5kATUZhhGRYJ1IO1MNV2pbDvg8DXruog.PNG.shin4299/Yeelight_tile_on.png?type=w580", backgroundColor:"#ffffff", nextState:"turningOn" 63 | } 64 | 65 | tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") { 66 | attributeState("default", label:'Updated: ${currentValue}',icon: "st.Health & Wellness.health9") 67 | } 68 | 69 | tileAttribute ("device.level", key: "SLIDER_CONTROL") { 70 | attributeState "level", action:"setLevel" 71 | } 72 | 73 | tileAttribute ("device.color", key: "COLOR_CONTROL") { 74 | attributeState "color", action:"setColor" 75 | } 76 | } 77 | 78 | main(["switch"]) 79 | details(["switch"]) 80 | } 81 | } 82 | 83 | // parse events into attributes 84 | def parse(String description) { 85 | log.debug "Parsing '${description}'" 86 | } 87 | 88 | def setInfo(String app_url, String id) { 89 | log.debug "${app_url}, ${id}" 90 | state.app_url = app_url 91 | state.id = id 92 | } 93 | 94 | def setStatus(data){ 95 | log.debug data 96 | 97 | sendEvent(name: "switch", value: (data[powerIDX.toString()] ? "on" : "off")) 98 | 99 | if(timerIDX > 0){ 100 | def timeStr = msToTime(data[timerIDX.toString()]) 101 | sendEvent(name:"leftTime", value: "${timeStr}", displayed: false) 102 | sendEvent(name:"time", value: Math.round(data/60), displayed: false) 103 | } 104 | if(modeIDX > 0){ 105 | state.colorMode = data[modeIDX.toString()] 106 | } 107 | if(colorIDX > 0){ 108 | def tmp = data[colorIDX.toString()] 109 | def val = "0x" + tmp.substring(tmp.length()-2, tmp.length()) 110 | def brightness = Integer.decode(val) 111 | sendEvent(name:"color", value: "#" + tmp.substring(0, 6) ) 112 | sendEvent(name:"level", value: (brightness * 100 / 255) as int, displayed: true) 113 | 114 | def rgb = hexToRGB(tmp.substring(0, 6)) 115 | float[] hsbValues = new float[3]; 116 | def hueSat = Color.RGBtoHSB(rgb.r, rgb.g, rgb.b, hsbValues) 117 | sendEvent(name:"hue", value: (hueSat[0] * 100) as int ) 118 | sendEvent(name:"saturation", value: (hueSat[1] * 100) as int) 119 | } 120 | if(brightnessIDX > 0){ 121 | if(state.colorMode == "white"){ 122 | def _value = (data[brightnessIDX.toString()] /255*100 ) as int 123 | sendEvent(name:"level", value: _value, displayed: true) 124 | } 125 | } 126 | 127 | def now = new Date().format("yyyy-MM-dd HH:mm:ss", location.timeZone) 128 | sendEvent(name: "lastCheckin", value: now, displayed: false) 129 | } 130 | 131 | def setColor(color){ 132 | log.debug "setColor >> ${color}" 133 | if(state.colorMode != "colour"){ 134 | processCommand("mode", "color", modeIDX.toString()) 135 | } 136 | def hex = color.hex 137 | if(hex == null){ 138 | def rgb = huesatToRGB(color.hue as Integer, color.saturation as Integer) 139 | hex = "#" + Integer.toHexString(rgb[0]) + Integer.toHexString(rgb[1]) + Integer.toHexString(rgb[2]) 140 | color["hex"] = hex 141 | } 142 | state.lastColor = hex 143 | def val = color 144 | val["brightness"] = device.currentValue("level") 145 | processCommand("color", val, colorIDX.toString()) 146 | } 147 | 148 | def setLevel(brightness){ 149 | log.debug "setLevel >> ${brightness} >> mode(" + colorMode + ")" 150 | def value = (brightness * 255/100) as int 151 | if(state.colorMode == "white"){ 152 | processCommand("brightness", value, brightnessIDX.toString()) 153 | }else if(state.colorMode == "colour"){ 154 | def val = ["hex":state.lastColor] 155 | val["brightness"] = value 156 | processCommand("color", val, colorIDX.toString()) 157 | } 158 | } 159 | 160 | def on(){ 161 | log.debug "on" 162 | processCommand("power", "on", powerIDX.toString()) 163 | } 164 | 165 | def off(){ 166 | log.debug "off" 167 | processCommand("power", "off", powerIDX.toString()) 168 | } 169 | 170 | def processCommand(cmd, data, idx){ 171 | def body = [ 172 | "id": state.id, 173 | "cmd": cmd, 174 | "data": data, 175 | "idx": idx 176 | ] 177 | def options = makeCommand(body) 178 | sendCommand(options, null) 179 | } 180 | 181 | def callback(physicalgraph.device.HubResponse hubResponse){ 182 | def msg 183 | try { 184 | msg = parseLanMessage(hubResponse.description) 185 | def jsonObj = new JsonSlurper().parseText(msg.body) 186 | } catch (e) { 187 | log.error "Exception caught while parsing data: "+e; 188 | } 189 | } 190 | 191 | def refresh(){} 192 | 193 | def updated(){} 194 | 195 | def sendCommand(options, _callback){ 196 | def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: _callback]) 197 | sendHubCommand(myhubAction) 198 | } 199 | 200 | def makeCommand(body){ 201 | def options = [ 202 | "method": "POST", 203 | "path": "/api/control", 204 | "headers": [ 205 | "HOST": state.app_url, 206 | "Content-Type": "application/json" 207 | ], 208 | "body":body 209 | ] 210 | return options 211 | } 212 | 213 | def hexToPercent(value){ 214 | def list = [ 215 | "0":0, "1":0,"2":1, "3":1, "4":2, "5":2, "6":2, "7":3, "8":3, "9":4, 216 | "10":4, "11":4,"12":5, "13":5, "14":5, "15":6, "16":6, "17":7, "18":7, "19":7, 217 | "20":8, "21":8,"22":9, "23":9, "24":9, "25":10, "26":10, "27":11, "28":11, "29":11 218 | 219 | ] 220 | } 221 | 222 | def hexToRGB(value){ 223 | return [ 224 | "r": Integer.decode("0x"+value.substring(0,2)), 225 | "g": Integer.decode("0x"+value.substring(2,4)), 226 | "b": Integer.decode("0x"+value.substring(4,6)) 227 | ] 228 | } 229 | 230 | def huesatToRGB(float hue, float sat) { 231 | while(hue >= 100) hue -= 100 232 | int h = (int)(hue / 100 * 6) 233 | float f = hue / 100 * 6 - h 234 | int p = Math.round(255 * (1 - (sat / 100))) 235 | int q = Math.round(255 * (1 - (sat / 100) * f)) 236 | int t = Math.round(255 * (1 - (sat / 100) * (1 - f))) 237 | switch (h) { 238 | case 0: return [255, t, p] 239 | case 1: return [q, 255, p] 240 | case 2: return [p, 255, t] 241 | case 3: return [p, q, 255] 242 | case 4: return [t, p, 255] 243 | case 5: return [255, p, q] 244 | } 245 | } 246 | --------------------------------------------------------------------------------