├── .gitignore ├── Devices ├── Generic HTTP Device │ ├── ArduinoNano.ino │ ├── GenericHTTPDevice.groovy │ ├── NodeMCU.ino │ ├── README.md │ ├── Screenhot_Prototype.jpg │ ├── Screenshot_Android_App.png │ ├── Screenshot_Arduino.png │ ├── Screenshot_PHP_Page.png │ ├── Virtual2ndContactSensor.groovy │ ├── VirtualCustomSwitch.groovy │ ├── WIRING │ │ ├── ArduinoNano-DualRelay5V.fzz │ │ ├── ArduinoNano-DualRelay5V.png │ │ ├── ArduinoNano-ENC28J60-DualRelay5V.fzz │ │ ├── ArduinoNano-ENC28J60-DualRelay5V.png │ │ ├── ArduinoUNO-ENC28J60-DualRelay5V.fzz │ │ ├── ArduinoUNO-ENC28J60-DualRelay5V.png │ │ ├── NodeMCU-DualRelay5V.fzz │ │ ├── NodeMCU-DualRelay5V.png │ │ ├── NodeMCU-ENC28J60-DualRelay5V.fzz │ │ └── NodeMCU-ENC28J60-DualRelay5V.png │ └── index.php ├── LICENSE ├── Panasonic PTZ Camera │ ├── PanasonicPTZCamera.groovy │ ├── PanasonicPTZVideo.groovy │ └── README.md ├── SmartGPIO │ ├── README.md │ ├── Screenshot_Android_App.png │ ├── Screenshot_PHP_Page.png │ └── SmartGPIO.groovy ├── TVDevice │ ├── 3.3V PICTURES & WIRING │ │ ├── 2016-06-11 12.56.41.jpg │ │ ├── 2016-06-11 12.57.01.jpg │ │ ├── IRSender.fzz │ │ ├── IRSender.png │ │ ├── IRSenderSchematic.png │ │ └── Thumbs.db │ ├── 5V PICTURES & WIRING │ │ ├── 2016-06-11 12.30.57.jpg │ │ ├── 2016-06-11 12.31.16.jpg │ │ ├── 2016-06-11 12.31.32.jpg │ │ ├── IRSender5V.fzz │ │ ├── IRSender5V.png │ │ └── IRSender5VSchematic.png │ ├── IRSender.ino │ ├── README.md │ ├── TVDevice.groovy │ └── TVvolume.groovy └── Wink Relay Toggle Switch │ └── Wink Relay Toggle Switch.groovy ├── Icons ├── README.md └── ST-Icons.html ├── LICENSE ├── README.md └── SmartApps ├── Node-RED Power Refresher └── Node-RED Power Refresher.groovy └── Virtual Custom Switch Sync App └── VirtualCustomSwitchSyncApp.groovy /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/ArduinoNano.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Arduino Nano & Ethernet Shield Sample v1.0.20170327 3 | * Source code can be found here: https://github.com/JZ-SmartThings/SmartThings/blob/master/Devices/Generic%20HTTP%20Device 4 | * Copyright 2017 JZ 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | */ 13 | #include 14 | #include 15 | 16 | // DESIGNATE WHICH PINS ARE USED FOR TRIGGERS 17 | // IF USING 3.3V RELAY, TRANSISTOR OR MOSFET THEN TOGGLE Use5Vrelay TO FALSE VIA UI 18 | int relayPin1 = 2; // GPIO5 = D2 19 | int relayPin2 = 3; // GPIO6 = D3 20 | bool Use5Vrelay; // Value defaults by reading eeprom in the setup method 21 | 22 | // DESIGNATE CONTACT SENSOR PINS. 23 | #define SENSORPIN 5 // what pin is the Contact Sensor on? 24 | #define SENSORPIN2 6 // what pin is the 2nd Contact Sensor on? 25 | 26 | // OTHER VARIALBES 27 | String currentIP; 28 | 29 | void(* resetFunction) (void) = 0; 30 | 31 | EthernetServer server = EthernetServer(80); 32 | 33 | void setup() 34 | { 35 | Serial.begin(115200); 36 | 37 | // DEFAULT CONFIG FOR CONTACT SENSOR 38 | //EEPROM.begin(1); 39 | int ContactSensor=EEPROM.read(1); 40 | if (ContactSensor != 0 && ContactSensor != 1) { 41 | EEPROM.write(1,0); 42 | //EEPROM.commit(); 43 | } 44 | if (ContactSensor==1) { 45 | pinMode(SENSORPIN, INPUT_PULLUP); 46 | } else { 47 | pinMode(SENSORPIN, OUTPUT); 48 | digitalWrite(SENSORPIN2, HIGH); 49 | } 50 | 51 | // DEFAULT CONFIG FOR CONTACT SENSOR 2 52 | int ContactSensor2=EEPROM.read(2); 53 | if (ContactSensor2 != 0 && ContactSensor2 != 1) { 54 | EEPROM.write(2,0); 55 | } 56 | if (ContactSensor2==1) { 57 | pinMode(SENSORPIN2, INPUT_PULLUP); 58 | } else { 59 | pinMode(SENSORPIN2, OUTPUT); 60 | digitalWrite(SENSORPIN2, HIGH); 61 | } 62 | 63 | // DEFAULT CONFIG FOR USE5VRELAY 64 | //EEPROM.begin(5); 65 | int eepromUse5Vrelay=EEPROM.read(5); 66 | if (eepromUse5Vrelay != 0 && eepromUse5Vrelay != 1) { 67 | EEPROM.write(5,1); 68 | //EEPROM.commit(); 69 | } 70 | if (eepromUse5Vrelay ? Use5Vrelay=1 : Use5Vrelay=0); 71 | 72 | pinMode(relayPin1, OUTPUT); 73 | pinMode(relayPin2, OUTPUT); 74 | digitalWrite(relayPin1, Use5Vrelay==true ? HIGH : LOW); 75 | digitalWrite(relayPin2, Use5Vrelay==true ? HIGH : LOW); 76 | 77 | uint8_t mac[6] = {0x0A,0x0B,0x0C,0x0D,0x0E,0x0F}; 78 | IPAddress myIP(192,168,0,225); 79 | Ethernet.begin(mac,myIP); 80 | /* // DHCP 81 | if (Ethernet.begin(mac) == 0) { 82 | while (1) { 83 | Serial.println(F("Failed to configure Ethernet using DHCP")); 84 | delay(10000); 85 | } 86 | } 87 | */ 88 | server.begin(); 89 | Serial.print(F("localIP: ")); Serial.println(Ethernet.localIP()); 90 | Serial.print(F("subnetMask: ")); Serial.println(Ethernet.subnetMask()); 91 | Serial.print(F("gatewayIP: ")); Serial.println(Ethernet.gatewayIP()); 92 | Serial.print(F("dnsServerIP: ")); Serial.println(Ethernet.dnsServerIP()); 93 | currentIP=Ethernet.localIP()[0]; currentIP+="."; currentIP+= Ethernet.localIP()[1]; currentIP+= "."; 94 | currentIP+=Ethernet.localIP()[2]; currentIP+= "."; currentIP+=Ethernet.localIP()[3] ; 95 | } 96 | 97 | void loop() 98 | { 99 | // SERIAL MESSAGE 100 | if (millis()%900000==0) { // every 15 minutes 101 | Serial.print("UpTime: "); Serial.println(uptime()); 102 | } 103 | 104 | // REBOOT 105 | //EEPROM.begin(1); 106 | int days=EEPROM.read(0); 107 | int RebootFrequencyDays=0; 108 | RebootFrequencyDays=days; 109 | if (RebootFrequencyDays > 0 && millis() >= (86400000*RebootFrequencyDays)) { //86400000 per day 110 | while(true); 111 | } 112 | 113 | EthernetClient client = server.available(); // try to get client 114 | 115 | String HTTP_req; // stores the HTTP request 116 | String request; 117 | if (client) { // got client? 118 | boolean currentLineIsBlank = true; 119 | while (client.connected()) { 120 | if (client.available()) { // client data available to read 121 | char c = client.read(); // read 1 byte (character) from client 122 | HTTP_req += c; // save the HTTP request 1 char at a time 123 | // last line of client request is blank and ends with \n 124 | // respond to client only after last line received 125 | if (c == '\n') { //&& currentLineIsBlank --- first line only on low memory like UNO/Nano otherwise get it all for AUTH 126 | request = HTTP_req.substring(0,HTTP_req.indexOf('\r')); 127 | //auth = HTTP_req.substring(HTTP_req.indexOf('Authorization: Basic '),HTTP_req.indexOf('\r')); 128 | HTTP_req = ""; // finished with request, empty string 129 | request.replace("GET ", ""); 130 | request.replace(" HTTP/1.1", ""); 131 | 132 | // Match the request 133 | if (request.indexOf("/favicon.ico") > -1) { 134 | return; 135 | } 136 | if (request.indexOf("/RebootNow") != -1) { 137 | resetFunction(); 138 | } 139 | if (request.indexOf("RebootFrequencyDays=") != -1) { 140 | //EEPROM.begin(1); 141 | String RebootFrequencyDays=request; 142 | RebootFrequencyDays.replace("RebootFrequencyDays=",""); RebootFrequencyDays.replace("/",""); RebootFrequencyDays.replace("?",""); 143 | //for (int i = 0 ; i < 512 ; i++) { EEPROM.write(i, 0); } // fully clear EEPROM before overwrite 144 | EEPROM.write(0,atoi(RebootFrequencyDays.c_str())); 145 | //EEPROM.commit(); 146 | } 147 | if (request.indexOf("/ToggleSensor") != -1) { 148 | //EEPROM.begin(1); 149 | if (EEPROM.read(1) == 0) { 150 | EEPROM.write(1,1); 151 | //EEPROM.commit(); 152 | pinMode(SENSORPIN, INPUT_PULLUP); 153 | } else if (EEPROM.read(1) == 1) { 154 | EEPROM.write(1,0); 155 | //EEPROM.commit(); 156 | pinMode(SENSORPIN, OUTPUT); 157 | digitalWrite(SENSORPIN, HIGH); 158 | } 159 | } 160 | if (request.indexOf("/Toggle2ndSensor") != -1) { 161 | if (EEPROM.read(2) == 0) { 162 | EEPROM.write(2,1); 163 | pinMode(SENSORPIN2, INPUT_PULLUP); 164 | } else if (EEPROM.read(2) == 1) { 165 | EEPROM.write(2,0); 166 | } 167 | } 168 | if (request.indexOf("/ToggleUse5Vrelay") != -1) { 169 | //EEPROM.begin(5); 170 | if (EEPROM.read(5) == 0) { 171 | Use5Vrelay=true; 172 | EEPROM.write(5,1); 173 | //EEPROM.commit(); 174 | pinMode(SENSORPIN2, OUTPUT); 175 | digitalWrite(SENSORPIN2, HIGH); 176 | } else { 177 | Use5Vrelay=false; 178 | EEPROM.write(5,0); 179 | //EEPROM.commit(); 180 | } 181 | resetFunction(); 182 | } 183 | //Serial.print("Use5Vrelay == "); Serial.println(Use5Vrelay); 184 | if (request.indexOf("RELAY1=ON") != -1 || request.indexOf("MainTriggerOn=") != -1) { 185 | digitalWrite(relayPin1, Use5Vrelay==true ? LOW : HIGH); 186 | } 187 | if (request.indexOf("RELAY1=OFF") != -1 || request.indexOf("MainTriggerOff=") != -1) { 188 | digitalWrite(relayPin1, Use5Vrelay==true ? HIGH : LOW); 189 | } 190 | if (request.indexOf("RELAY1=MOMENTARY") != -1 || request.indexOf("MainTrigger=") != -1) { 191 | digitalWrite(relayPin1, Use5Vrelay==true ? LOW : HIGH); 192 | delay(300); 193 | digitalWrite(relayPin1, Use5Vrelay==true ? HIGH : LOW); 194 | } 195 | 196 | if (request.indexOf("RELAY2=ON") != -1 || request.indexOf("CustomTriggerOn=") != -1) { 197 | digitalWrite(relayPin2, Use5Vrelay==true ? LOW : HIGH); 198 | } 199 | if (request.indexOf("RELAY2=OFF") != -1 || request.indexOf("CustomTriggerOff=") != -1) { 200 | digitalWrite(relayPin2, Use5Vrelay==true ? HIGH : LOW); 201 | } 202 | if (request.indexOf("RELAY2=MOMENTARY") != -1 || request.indexOf("CustomTrigger=") != -1) { 203 | digitalWrite(relayPin2, Use5Vrelay==true ? LOW : HIGH); 204 | delay(300); 205 | digitalWrite(relayPin2, Use5Vrelay==true ? HIGH : LOW); 206 | } 207 | 208 | // Return the response 209 | client.println(F("HTTP/1.1 200 OK")); 210 | client.println(F("Content-Type: text/html")); 211 | client.println(F("\n")); // do not forget this one 212 | client.println(F("")); 213 | client.println(F("Arduino & ENC28J60 Dual Switch\n\n

ARDUINO & ENC28J60 DUAL SWITCH

")); 216 | client.println(currentIP); 217 | client.println(F("\n

\n")); 218 | 219 | client.println(F("Current Request:
")); 220 | client.println(request); 221 | client.println(F("
")); 222 | 223 | client.println(F("
"));
224 |           // SHOW Use5Vrelay
225 |           client.print(F("Use5Vrelay=")); client.print(Use5Vrelay ? F("true") : F("false") ); client.print(F("\n"));
226 |           // SHOW CONTACT SENSOR
227 |           if (EEPROM.read(1)==1) {
228 |             client.print(F("Contact Sensor Enabled:\n"));
229 |             client.print(F("Contact Sensor=")); client.print(digitalRead(SENSORPIN) ? F("Open") : F("Closed") ); client.print(F("\n"));
230 |           } else {
231 |             client.print(F("Contact Sensor Disabled:\n"));
232 |             client.print(F("Contact Sensor=Closed\n"));
233 |           }
234 |           // SHOW CONTACT SENSOR
235 |           if (EEPROM.read(2)==1) {
236 |             client.print(F("Contact Sensor 2 Enabled:\n"));
237 |             client.print(F("Contact Sensor 2=")); client.print(digitalRead(SENSORPIN2) ? F("Open") : F("Closed") ); client.print(F("\n"));
238 |           } else {
239 |             client.print(F("Contact Sensor 2 Disabled:\n"));
240 |             client.print(F("Contact Sensor 2=Closed\n"));
241 |           }
242 |           client.print(F("UpTime=")); client.println(uptime());
243 |           client.println(freeRam());
244 |           client.println(F("
")); client.println(F("
\n")); 245 | 246 | client.println(F("
\n")); 247 | client.print(F("RELAY1 pin is now: ")); 248 | if(Use5Vrelay==true) { 249 | if(digitalRead(relayPin1) == LOW) { client.print(F("On")); } else { client.print(F("Off")); } 250 | } else { 251 | if(digitalRead(relayPin1) == HIGH) { client.print(F("On")); } else { client.print(F("Off")); } 252 | } 253 | client.println(F("\n
\n")); 254 | client.println(F("\n")); 255 | client.println(F("

\n")); 256 | 257 | client.println(F("
\n")); 258 | client.print(F("RELAY2 pin is now: ")); 259 | if(Use5Vrelay==true) { 260 | if(digitalRead(relayPin2) == LOW) { client.print(F("On")); } else { client.print(F("Off")); } 261 | } else { 262 | if(digitalRead(relayPin2) == HIGH) { client.print(F("On")); } else { client.print(F("Off")); } 263 | } 264 | client.println(F("\n
\n")); 265 | client.println(F("\n")); 266 | client.println(F("

\n")); 267 | 268 | 269 | client.println(F("
")); 270 | // SHOW TOGGLE Use5Vrelay 271 | client.println(F("

\n")); 272 | // SHOW TOGGLE CONTACT SENSORS 273 | client.println(F("   \n")); 274 | client.println(F("

\n")); 275 | 276 | 277 | client.println(F("   
Days between reboots.
0 to disable & 255 days is max.")); 282 | client.println(F("


\n")); 283 | 284 | client.println(F("\n")); 286 | 287 | break; 288 | } 289 | // every line of text received from the client ends with \r\n 290 | if (c == '\n') { 291 | // last character on line of received text 292 | // starting new line with next character read 293 | currentLineIsBlank = true; 294 | } else if (c != '\r') { 295 | // a text character was received from client 296 | currentLineIsBlank = false; 297 | } 298 | } // end if (client.available()) 299 | } // end while (client.connected()) 300 | delay(1); // give the web browser time to receive the data 301 | client.stop(); // close the connection 302 | } // end if (client) 303 | } 304 | 305 | String freeRam () { 306 | #if defined(ARDUINO_ARCH_AVR) 307 | extern int __heap_start, *__brkval; 308 | int v; 309 | return "Free Mem="+String((int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval))+"B of 2048B"; 310 | #elif defined(ESP8266) 311 | return "Free Mem="+String(ESP.getFreeHeap()/1024)+"KB of 80KB"; 312 | #endif 313 | } 314 | 315 | String uptime() { 316 | float d,hr,m,s; 317 | String dstr,hrstr, mstr, sstr; 318 | unsigned long over; 319 | d=int(millis()/(3600000*24)); 320 | dstr=String(d,0); 321 | dstr.replace(" ", ""); 322 | over=millis()%(3600000*24); 323 | hr=int(over/3600000); 324 | hrstr=String(hr,0); 325 | if (hr<10) {hrstr=hrstr="0"+hrstr;} 326 | hrstr.replace(" ", ""); 327 | over=over%3600000; 328 | m=int(over/60000); 329 | mstr=String(m,0); 330 | if (m<10) {mstr=mstr="0"+mstr;} 331 | mstr.replace(" ", ""); 332 | over=over%60000; 333 | s=int(over/1000); 334 | sstr=String(s,0); 335 | if (s<10) {sstr="0"+sstr;} 336 | sstr.replace(" ", ""); 337 | if (dstr=="0") { 338 | return hrstr + ":" + mstr + ":" + sstr; 339 | } else if (dstr=="1") { 340 | return dstr + " Day " + hrstr + ":" + mstr + ":" + sstr; 341 | } else { 342 | return dstr + " Days " + hrstr + ":" + mstr + ":" + sstr; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/GenericHTTPDevice.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic HTTP Device v1.0.20181021 3 | * Source code can be found here: https://github.com/JZ-SmartThings/SmartThings/blob/master/Devices/Generic%20HTTP%20Device/GenericHTTPDevice.groovy 4 | * Copyright 2018 JZ 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | */ 13 | 14 | import groovy.json.JsonSlurper 15 | 16 | metadata { 17 | definition (name: "Generic HTTP Device", author: "JZ", namespace:"JZ") { 18 | capability "Switch" 19 | capability "Temperature Measurement" 20 | capability "Contact Sensor" 21 | capability "Sensor" 22 | capability "Polling" 23 | capability "Refresh" 24 | capability "Health Check" 25 | attribute "mainTriggered", "string" 26 | attribute "mainTriggeredEPOCH", "number" 27 | attribute "refreshTriggered", "string" 28 | attribute "customswitch", "string" 29 | attribute "customTriggered", "string" 30 | attribute "customTriggeredEPOCH", "number" 31 | attribute "cpuUsage", "string" 32 | attribute "spaceUsed", "string" 33 | attribute "upTime", "string" 34 | attribute "cpuTemp", "string" 35 | attribute "freeMem", "string" 36 | attribute "temperature", "string" 37 | attribute "humidity", "string" 38 | attribute "contact2", "string" 39 | attribute "sensor2Triggered", "string" 40 | attribute "sensorTriggered", "string" 41 | command "DeviceTrigger" 42 | command "RefreshTrigger" 43 | command "CustomTrigger" 44 | command "RebootNow" 45 | command "ResetTiles" 46 | command "ClearTiles" 47 | command "updateMainEPOCH" 48 | command "updateCustomEPOCH" 49 | } 50 | 51 | preferences { 52 | input("DeviceIP", "string", title:"Device IP Address", description: "Please enter your device's IP Address", required: true, displayDuringSetup: true) 53 | input("DevicePort", "string", title:"Device Port", description: "Empty assumes port 80.", required: false, displayDuringSetup: true) 54 | input("DevicePath", "string", title:"URL Path (RPi)", description: "Rest of the URL, include the / slash.", displayDuringSetup: true) 55 | input(name: "DevicePostGet", type: "enum", title: "POST or GET. POST for PHP & GET for Arduino.", options: ["POST","GET"], defaultValue: "POST", required: false, displayDuringSetup: true) 56 | input("UseOffVoiceCommandForCustom", "bool", title:"Use the OFF voice command (e.g. by Alexa) to control the Custom command? May cause issues with MQTT sync. Alternative is using my sync SmartApp & Device for Custom command voice control.", description: "", defaultValue: false, required: false, displayDuringSetup: true) 57 | input("DeviceMainMomentary", "bool", title:"MainTrigger is Momentary?", description: "", defaultValue: true, required: false, displayDuringSetup: true) 58 | input("DeviceMainPin", "number", title:'Main Pin # in BCM Format (RPi)', description: 'Empty assumes pin #4.', required: false, displayDuringSetup: false) 59 | input("DeviceCustomMomentary", "bool", title:"CustomTrigger is Momentary?", description: "", defaultValue: true, required: false, displayDuringSetup: true) 60 | input("DeviceCustomPin", "number", title:'Custom Pin # in BCM Format (RPi)', description: 'Empty assumes pin #21.', required: false, displayDuringSetup: false) 61 | input("DeviceSensorInvert", "bool", title:"Invert open/closed states on the primary contact sensor?", description: "", defaultValue: false, required: false, displayDuringSetup: false) 62 | input("DeviceSensor2Invert", "bool", title:"Invert open/closed states on the secondary contact sensor?", description: "", defaultValue: false, required: false, displayDuringSetup: false) 63 | input("UseJSON", "bool", title:"Use JSON instead of HTML?", description: "", defaultValue: false, required: false, displayDuringSetup: true) 64 | input(name: "DeviceTempMeasurement", type: "enum", title: "Temperature measurement in Celcius or Fahrenheit? Default is Fahrenheit.", options: ["Celcius","Fahrenheit"], defaultValue: "Fahrenheit", required: false, displayDuringSetup: true) 65 | section() { 66 | input("HTTPAuth", "bool", title:"Requires User Auth?", description: "Choose if the HTTP requires basic authentication", defaultValue: false, required: true, displayDuringSetup: true) 67 | input("HTTPUser", "string", title:"HTTP User", description: "Enter your basic username", required: false, displayDuringSetup: true) 68 | input("HTTPPassword", "string", title:"HTTP Password", description: "Enter your basic password", required: false, displayDuringSetup: true) 69 | } 70 | } 71 | 72 | simulator { 73 | } 74 | 75 | tiles(scale: 2) { 76 | valueTile("displayName", "device.displayName", width: 6, height: 1, decoration: "flat") { 77 | state("default", label: '${currentValue}', backgroundColor:"#DDDDDD") 78 | } 79 | valueTile("mainTriggered", "device.mainTriggered", width: 5, height: 1, decoration: "flat") { 80 | state("default", label: 'Main triggered:\r\n${currentValue}', backgroundColor:"#ffffff") 81 | } 82 | standardTile("DeviceTrigger", "device.switch", width: 1, height: 1, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 83 | state "off", label:'OFF' , action: "on", icon: "st.Outdoor.outdoor22", backgroundColor:"#53a7c0", nextState: "trying" 84 | state "on", label: 'ON', action: "on", icon: "st.Outdoor.outdoor22", backgroundColor: "#FF6600", nextState: "trying" 85 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.Outdoor.outdoor22", backgroundColor: "#FFAA33" 86 | } 87 | valueTile("customTriggered", "device.customTriggered", width: 5, height: 1, decoration: "flat") { 88 | state("default", label: 'Custom triggered:\r\n${currentValue}', backgroundColor:"#ffffff") 89 | } 90 | standardTile("CustomTrigger", "device.customswitch", width: 1, height: 1, decoration: "flat") { 91 | state "off", label:'CUSTOM', action: "CustomTrigger", icon: "st.Lighting.light13", backgroundColor:"#53a7c0", nextState: "trying" 92 | state "on", label: 'ON', action: "CustomTrigger", icon: "st.Lighting.light11", backgroundColor: "#FF6600", nextState: "trying" 93 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.Lighting.light11", backgroundColor: "#FFAA33" 94 | } 95 | valueTile("sensorTriggered", "device.sensorTriggered", width: 3, height: 1, decoration: "flat") { 96 | state("default", label: 'Sensor 1 State Changed:\r\n${currentValue}', backgroundColor:"#ffffff") 97 | } 98 | standardTile("contact", "device.contact", width: 1, height: 1, decoration: "flat") { 99 | state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" 100 | state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" 101 | } 102 | valueTile("sensor2Triggered", "device.sensor2Triggered", width: 3, height: 1, decoration: "flat") { 103 | state("default", label: 'Sensor 2 State Changed:\r\n${currentValue}', backgroundColor:"#ffffff") 104 | } 105 | standardTile("contact2", "device.contact2", width: 1, height: 1, decoration: "flat") { 106 | state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" 107 | state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" 108 | } 109 | valueTile("refreshTriggered", "device.refreshTriggered", width: 5, height: 1, decoration: "flat") { 110 | state("default", label: 'Refreshed:\r\n${currentValue}', backgroundColor:"#ffffff") 111 | } 112 | standardTile("RefreshTrigger", "device.refreshswitch", width: 1, height: 1, decoration: "flat") { 113 | state "default", label:'REFRESH', action: "refresh.refresh", icon: "st.secondary.refresh-icon", backgroundColor:"#53a7c0", nextState: "refreshing" 114 | state "refreshing", label: 'REFRESHING', action: "ResetTiles", icon: "st.secondary.refresh-icon", backgroundColor: "#FF6600", nextState: "default" 115 | } 116 | 117 | valueTile("cpuUsage", "device.cpuUsage", width: 2, height: 2) { 118 | state("default", label: 'CPU\r\n ${currentValue}%', 119 | backgroundColors:[ 120 | [value: 0, color: "#00cc33"], 121 | [value: 10, color: "#99ff33"], 122 | [value: 30, color: "#ffcc99"], 123 | [value: 55, color: "#ff6600"], 124 | [value: 90, color: "#ff0000"] 125 | ] 126 | ) 127 | } 128 | valueTile("cpuTemp", "device.cpuTemp", width: 2, height: 2) { 129 | state("default", label: 'CPU Temp ${currentValue}', 130 | backgroundColors:[ 131 | [value: 50, color: "#00cc33"], 132 | [value: 60, color: "#99ff33"], 133 | [value: 67, color: "#ff6600"], 134 | [value: 75, color: "#ff0000"] 135 | ] 136 | ) 137 | } 138 | valueTile("spaceUsed", "device.spaceUsed", width: 2, height: 2) { 139 | state("default", label: 'Space Used\r\n ${currentValue}%', 140 | backgroundColors:[ 141 | [value: 50, color: "#00cc33"], 142 | [value: 75, color: "#ffcc66"], 143 | [value: 85, color: "#ff6600"], 144 | [value: 95, color: "#ff0000"] 145 | ] 146 | ) 147 | } 148 | valueTile("upTime", "device.upTime", width: 2, height: 2, decoration: "flat") { 149 | state("default", label: 'UpTime\r\n ${currentValue}', backgroundColor:"#ffffff") 150 | } 151 | valueTile("freeMem", "device.freeMem", width: 2, height: 2, decoration: "flat") { 152 | state("default", label: 'Free Mem\r\n ${currentValue}', backgroundColor:"#ffffff") 153 | } 154 | standardTile("clearTiles", "device.clear", width: 2, height: 2, decoration: "flat") { 155 | state "default", label:'Clear Tiles', action:"ClearTiles", icon:"st.Bath.bath9" 156 | } 157 | valueTile("temperature", "device.temperature", width: 2, height: 2) { 158 | state("default", label:'Temp\n${currentValue}', 159 | backgroundColors:[ 160 | [value: 32, color: "#153591"], 161 | [value: 44, color: "#1e9cbb"], 162 | [value: 59, color: "#90d2a7"], 163 | [value: 74, color: "#44b621"], 164 | [value: 84, color: "#f1d801"], 165 | [value: 92, color: "#d04e00"], 166 | [value: 98, color: "#bc2323"] 167 | ] 168 | ) 169 | } 170 | valueTile("humidity", "device.humidity", width: 2, height: 2) { 171 | state("default", label: 'Humidity\n${currentValue}', 172 | backgroundColors:[ 173 | [value: 50, color: "#00cc33"], 174 | [value: 60, color: "#99ff33"], 175 | [value: 67, color: "#ff6600"], 176 | [value: 75, color: "#ff0000"] 177 | ] 178 | ) 179 | } 180 | standardTile("RebootNow", "device.rebootnow", width: 1, height: 1, decoration: "flat") { 181 | state "default", label:'REBOOT' , action: "RebootNow", icon: "st.Seasonal Winter.seasonal-winter-014", backgroundColor:"#ff0000", nextState: "rebooting" 182 | state "rebooting", label: 'REBOOTING', action: "ResetTiles", icon: "st.Office.office13", backgroundColor: "#FF6600", nextState: "default" 183 | } 184 | main "DeviceTrigger" 185 | details(["displayName","mainTriggered", "DeviceTrigger", "customTriggered", "CustomTrigger", "sensorTriggered", "sensor2Triggered", "refreshTriggered", "RefreshTrigger", "cpuUsage", "cpuTemp", "upTime", "spaceUsed", "freeMem", "clearTiles", "temperature", "humidity" , "RebootNow"]) 186 | } 187 | } 188 | 189 | def refresh() { 190 | def FullCommand = 'Refresh=' 191 | if (DeviceMainPin) {FullCommand=FullCommand+"&MainPin="+DeviceMainPin} //else {FullCommand=FullCommand+"&MainPin=4"} 192 | if (DeviceCustomPin) {FullCommand=FullCommand+"&CustomPin="+DeviceCustomPin} //else {FullCommand=FullCommand+"&CustomPin=21"} 193 | if (UseJSON==true) { FullCommand=FullCommand+"&UseJSON=" } 194 | if (DeviceMainMomentary==true) { settings.UseOffVoiceCommandForCustom = true } 195 | runCmd(FullCommand) 196 | } 197 | def poll() { 198 | refresh() 199 | } 200 | def ping() { 201 | log.debug "ping()" 202 | refresh() 203 | } 204 | def on() { 205 | def FullCommand = '' 206 | if (DeviceMainMomentary==true) { 207 | settings.UseOffVoiceCommandForCustom = true 208 | FullCommand='MainTrigger=' 209 | } else { 210 | if (device.currentState("switch")!=null && device.currentState("switch").getValue()=="off") { FullCommand='MainTriggerOn=' } else { FullCommand='MainTriggerOff=' } 211 | } 212 | if (DeviceMainPin) {FullCommand=FullCommand+"&MainPin="+DeviceMainPin} //else {FullCommand=FullCommand+"&MainPin=4"} 213 | if (DeviceCustomPin) {FullCommand=FullCommand+"&CustomPin="+DeviceCustomPin} //else {FullCommand=FullCommand+"&CustomPin=21"} 214 | if (UseJSON==true) {FullCommand=FullCommand+"&UseJSON="} 215 | runCmd(FullCommand) 216 | } 217 | def off() { 218 | if (UseOffVoiceCommandForCustom==true) { 219 | settings.UseOffVoiceCommandForCustom = true 220 | CustomTrigger() 221 | } else { 222 | log.debug "Running ON() Function for MAIN Command Handling." 223 | on() 224 | } 225 | } 226 | 227 | def CustomTrigger() { 228 | //log.debug device.currentState("customswitch").getValue() + " === customswitch state" 229 | def FullCommand = '' 230 | if (DeviceCustomMomentary==true) { 231 | settings.UseOffVoiceCommandForCustom = true 232 | FullCommand='CustomTrigger=' 233 | } else { 234 | log.debug "main swtich currentState===" + device.currentState("switch") 235 | if (device.currentState("switch")!=null && device.currentState("customswitch").getValue()=="off") { FullCommand='CustomTriggerOn=' } else { FullCommand='CustomTriggerOff=' } 236 | } 237 | if (DeviceMainPin) {FullCommand=FullCommand+"&MainPin="+DeviceMainPin} //else {FullCommand=FullCommand+"&MainPin=4"} 238 | if (DeviceCustomPin) {FullCommand=FullCommand+"&CustomPin="+DeviceCustomPin} //else {FullCommand=FullCommand+"&CustomPin=21"} 239 | if (UseJSON==true) {FullCommand=FullCommand+"&UseJSON="} 240 | runCmd(FullCommand) 241 | } 242 | def RebootNow() { 243 | log.debug "Reboot Triggered!!!" 244 | runCmd('RebootNow=') 245 | } 246 | def ClearTiles() { 247 | sendEvent(name: "mainTriggered", value: "", unit: "") 248 | sendEvent(name: "customTriggered", value: "", unit: "") 249 | sendEvent(name: "refreshTriggered", value: "", unit: "") 250 | sendEvent(name: "sensorTriggered", value: "", unit: "") 251 | sendEvent(name: "sensor2Triggered", value: "", unit: "") 252 | sendEvent(name: "cpuUsage", value: "", unit: "") 253 | sendEvent(name: "cpuTemp", value: "", unit: "") 254 | sendEvent(name: "spaceUsed", value: "", unit: "") 255 | sendEvent(name: "upTime", value: "", unit: "") 256 | sendEvent(name: "freeMem", value: "", unit: "") 257 | sendEvent(name: "temperature", value: "", unit: "") 258 | sendEvent(name: "humidity", value: "", unit: "") 259 | } 260 | def ResetTiles() { 261 | //RETURN BUTTONS TO CORRECT STATE 262 | if (DeviceMainMomentary==true) { 263 | sendEvent(name: "switch", value: "off", isStateChange: true) 264 | } 265 | if (DeviceCustomMomentary==true) { 266 | sendEvent(name: "customswitch", value: "off", isStateChange: true) 267 | } 268 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 269 | sendEvent(name: "rebootnow", value: "default", isStateChange: true) 270 | log.debug "Resetting tiles." 271 | } 272 | 273 | def runCmd(String varCommand) { 274 | def host = DeviceIP 275 | def hosthex = convertIPtoHex(host).toUpperCase() 276 | def LocalDevicePort = '' 277 | if (DevicePort==null) { LocalDevicePort = "80" } else { LocalDevicePort = DevicePort } 278 | def porthex = convertPortToHex(LocalDevicePort).toUpperCase() 279 | device.deviceNetworkId = "$hosthex:$porthex" 280 | def userpassascii = "${HTTPUser}:${HTTPPassword}" 281 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString() 282 | 283 | log.debug "The device id configured is: $device.deviceNetworkId" 284 | 285 | def headers = [:] 286 | headers.put("HOST", "$host:$LocalDevicePort") 287 | headers.put("Content-Type", "application/x-www-form-urlencoded") 288 | if (HTTPAuth) { 289 | headers.put("Authorization", userpass) 290 | } 291 | log.debug "The Header is $headers" 292 | 293 | def path = '' 294 | def body = '' 295 | log.debug "Uses which method: $DevicePostGet" 296 | def method = "POST" 297 | try { 298 | if (DevicePostGet.toUpperCase() == "GET") { 299 | method = "GET" 300 | path = varCommand 301 | if (path.substring(0,1) != "/") { path = "/" + path } 302 | log.debug "GET path is: $path" 303 | } else { 304 | path = DevicePath 305 | body = varCommand 306 | log.debug "POST body is: $body" 307 | } 308 | log.debug "The method is $method" 309 | } 310 | catch (Exception e) { 311 | settings.DevicePostGet = "POST" 312 | log.debug e 313 | log.debug "You must not have set the preference for the DevicePOSTGET option" 314 | } 315 | 316 | try { 317 | def hubAction = new physicalgraph.device.HubAction( 318 | method: method, 319 | path: path, 320 | body: body, 321 | headers: headers 322 | ) 323 | hubAction.options = [outputMsgToS3:false] 324 | log.debug hubAction 325 | hubAction 326 | } 327 | catch (Exception e) { 328 | log.debug "Hit Exception $e on $hubAction" 329 | } 330 | } 331 | 332 | def updateMainEPOCH(String caller) { 333 | log.debug "EPOCH Main before update: " + device.currentValue("mainTriggeredEPOCH") 334 | sendEvent(name: "mainTriggeredEPOCH", value: now()+6000, isStateChange: true) 335 | log.debug "Updated Main EPOCH ${caller} to: " + now()+6000 336 | } 337 | def updateCustomEPOCH(String caller) { 338 | log.debug "EPOCH Custom before update: " + device.currentValue("customTriggeredEPOCH") 339 | sendEvent(name: "customTriggeredEPOCH", value: now()+6000, isStateChange: true) 340 | log.debug "Updated Custom EPOCH ${caller} to: " + now()+6000 341 | } 342 | 343 | def parse(String description) { 344 | // log.debug "Parsing '${description}'" 345 | def whichTile = '' 346 | def map = [:] 347 | def retResult = [] 348 | def descMap = parseDescriptionAsMap(description) 349 | def jsonlist = [:] 350 | def bodyReturned = ' ' 351 | def headersReturned = ' ' 352 | if (descMap["body"]) { bodyReturned = new String(descMap["body"].decodeBase64()) } 353 | if (descMap["headers"]) { headersReturned = new String(descMap["headers"].decodeBase64()) } 354 | //log.debug "BODY---" + bodyReturned 355 | //log.debug "HEADERS---" + headersReturned 356 | 357 | if (descMap["body"]) { 358 | if (headersReturned.contains("application/json")) { 359 | def body = new String(descMap["body"].decodeBase64()) 360 | def slurper = new JsonSlurper() 361 | jsonlist = slurper.parseText(body) 362 | //log.debug "JSONLIST---" + jsonlist."Refresh" 363 | //log.debug "JSONFULL---" + body 364 | jsonlist.put ("Date", new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone)) 365 | if (jsonlist."CurrentRequest" != null) { 366 | if (jsonlist."CurrentRequest".contains('Refresh')) { jsonlist.put ("Refresh", "Success") } 367 | if (jsonlist."CurrentRequest".contains('MainTrigger=')) { jsonlist.put ("MainTrigger", "Success") } 368 | if (jsonlist."CurrentRequest".contains('MainTriggerOn=')) { jsonlist.put ("MainTriggerOn", "Success") } 369 | if (jsonlist."CurrentRequest".contains('MainTriggerOff=')) { jsonlist.put ("MainTriggerOff", "Success") } 370 | if (jsonlist."CurrentRequest".contains('CustomTrigger=')) { jsonlist.put ("CustomTrigger", "Success") } 371 | if (jsonlist."CurrentRequest".contains('CustomTriggerOn=')) { jsonlist.put ("CustomTriggerOn", "Success") } 372 | if (jsonlist."CurrentRequest".contains('CustomTriggerOff=')) { jsonlist.put ("CustomTriggerOff", "Success") } 373 | } 374 | } else { 375 | jsonlist.put ("Date", new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone)) 376 | def data=bodyReturned.eachLine { line -> 377 | if (line.contains('CPU=')) { jsonlist.put ("CPU", line.replace("CPU=","")) } 378 | if (line.contains('Space Used=')) { jsonlist.put ("Space Used", line.replace("Space Used=","")) } 379 | if (line.contains('UpTime=')) { jsonlist.put ("UpTime", line.replace("UpTime=","")) } 380 | if (line.contains('CPU Temp=')) { jsonlist.put ("CPU Temp",line.replace("CPU Temp=","")) } 381 | if (line.contains('Free Mem=')) { jsonlist.put ("Free Mem",line.replace("Free Mem=","")) } 382 | if (line.contains('Temperature=')) { jsonlist.put ("Temperature",line.replace("Temperature=","").replaceAll("[^\\p{ASCII}]", "°")) } 383 | if (line.contains('Humidity=')) { jsonlist.put ("Humidity",line.replace("Humidity=","")) } 384 | if (line.contains('MainTrigger=Success')) { jsonlist.put ("MainTrigger".replace("=",""), "Success") } 385 | if (line.contains('MainTrigger=Failed : Authentication Required!')) { jsonlist.put ("MainTrigger".replace("=",""), "Authentication Required!") } 386 | if (line.contains('MainTriggerOn=Success')) { jsonlist.put ("MainTriggerOn", "Success") } 387 | if (line.contains('MainTriggerOn=Failed : Authentication Required!')) { jsonlist.put ("MainTriggerOn", "Authentication Required!") } 388 | if (line.contains('MainTriggerOff=Success')) { jsonlist.put ("MainTriggerOff", "Success") } 389 | if (line.contains('MainTriggerOff=Failed : Authentication Required!')) { jsonlist.put ("MainTriggerOff", "Authentication Required!") } 390 | if (line.contains('MainPinStatus=1')) { jsonlist.put ("MainPinStatus".replace("=",""), 1) } 391 | if (line.contains('MainPinStatus=0')) { jsonlist.put ("MainPinStatus".replace("=",""), 0) } 392 | if (line.contains('CustomTrigger=Success')) { jsonlist.put ("CustomTrigger", "Success") } 393 | if (line.contains('CustomTrigger=Failed : Authentication Required!')) { jsonlist.put ("CustomTrigger", "Authentication Required!") } 394 | if (line.contains('CustomTriggerOn=Success')) { jsonlist.put ("CustomTriggerOn", "Success") } 395 | if (line.contains('CustomTriggerOn=Failed : Authentication Required!')) { jsonlist.put ("CustomTriggerOn", "Authentication Required!") } 396 | if (line.contains('CustomTriggerOff=Success')) { jsonlist.put ("CustomTriggerOff", "Success") } 397 | if (line.contains('CustomTriggerOff=Failed : Authentication Required!')) { jsonlist.put ("CustomTriggerOff", "Authentication Required!") } 398 | if (line.contains('CustomPinStatus=1')) { jsonlist.put ("CustomPinStatus".replace("=",""), 1) } 399 | if (line.contains('CustomPinStatus=0')) { jsonlist.put ("CustomPinStatus".replace("=",""), 0) } 400 | if (DeviceSensorInvert == false) { 401 | if (line.contains('Contact Sensor=Open')) { jsonlist.put ("SensorPinStatus".replace("=",""), "Open") } 402 | if (line.contains('Contact Sensor=Closed')) { jsonlist.put ("SensorPinStatus".replace("=",""), "Closed") } 403 | if (line.contains('SensorPinStatus=Open')) { jsonlist.put ("SensorPinStatus".replace("=",""), "Open") } 404 | if (line.contains('SensorPinStatus=Closed')) { jsonlist.put ("SensorPinStatus".replace("=",""), "Closed") } 405 | } else { 406 | if (line.contains('Contact Sensor=Open')) { jsonlist.put ("SensorPinStatus".replace("=",""), "Closed") } 407 | if (line.contains('Contact Sensor=Closed')) { jsonlist.put ("SensorPinStatus".replace("=",""), "Open") } 408 | if (line.contains('SensorPinStatus=Open')) { jsonlist.put ("SensorPinStatus".replace("=",""), "Closed") } 409 | if (line.contains('SensorPinStatus=Closed')) { jsonlist.put ("SensorPinStatus".replace("=",""), "Open") } 410 | } 411 | if (DeviceSensor2Invert == false) { 412 | if (line.contains('Contact Sensor 2=Open')) { jsonlist.put ("Sensor2PinStatus".replace("=",""), "Open") } 413 | if (line.contains('Contact Sensor 2=Closed')) { jsonlist.put ("Sensor2PinStatus".replace("=",""), "Closed") } 414 | if (line.contains('Sensor2PinStatus=Open')) { jsonlist.put ("Sensor2PinStatus".replace("=",""), "Open") } 415 | if (line.contains('Sensor2PinStatus=Closed')) { jsonlist.put ("Sensor2PinStatus".replace("=",""), "Closed") } 416 | } else { 417 | if (line.contains('Contact Sensor 2=Open')) { jsonlist.put ("Sensor2PinStatus".replace("=",""), "Closed") } 418 | if (line.contains('Contact Sensor 2=Closed')) { jsonlist.put ("Sensor2PinStatus".replace("=",""), "Open") } 419 | if (line.contains('Sensor2PinStatus=Open')) { jsonlist.put ("Sensor2PinStatus".replace("=",""), "Closed") } 420 | if (line.contains('Sensor2PinStatus=Closed')) { jsonlist.put ("Sensor2PinStatus".replace("=",""), "Open") } 421 | } 422 | if (line.contains('Refresh=')) { jsonlist.put ("Refresh", "Success") } 423 | if (line.contains('Refresh=Success')) { jsonlist.put ("Refresh", "Success") } 424 | if (line.contains('Refresh=Failed : Authentication Required!')) { jsonlist.put ("Refresh", "Authentication Required!") } 425 | if (line.contains('RebootNow=Success')) { jsonlist.put ("RebootNow", "Success") } 426 | if (line.contains('RebootNow=Failed : Authentication Required!')) { jsonlist.put ("RebootNow", "Authentication Required!") } 427 | //ARDUINO CHECKS 428 | if (line.contains('MainTrigger=')) { jsonlist.put ("MainTrigger".replace("=",""), "Success") } 429 | if (line.contains('MainTriggerOn=')) { jsonlist.put ("MainTriggerOn", "Success") } 430 | if (line.contains('MainTriggerOff=')) { jsonlist.put ("MainTriggerOff", "Success") } 431 | if (line.contains('RELAY1 pin is now: On')) { jsonlist.put ("MainPinStatus".replace("=",""), 1) } 432 | if (line.contains('RELAY1 pin is now: Off')) { jsonlist.put ("MainPinStatus".replace("=",""), 0) } 433 | if (line.contains('CustomTrigger=')) { jsonlist.put ("CustomTrigger".replace("=",""), "Success") } 434 | if (line.contains('CustomTriggerOn=')) { jsonlist.put ("CustomTriggerOn", "Success") } 435 | if (line.contains('CustomTriggerOff=')) { jsonlist.put ("CustomTriggerOff", "Success") } 436 | if (line.contains('RELAY2 pin is now: On')) { jsonlist.put ("CustomPinStatus".replace("=",""), 1) } 437 | if (line.contains('RELAY2 pin is now: Off')) { jsonlist.put ("CustomPinStatus".replace("=",""), 0) } 438 | } 439 | } 440 | } 441 | if (descMap["body"]) { 442 | if (jsonlist."Refresh"=="Authentication Required!") { 443 | sendEvent(name: "refreshTriggered", value: "Use Authentication Credentials", unit: "") 444 | whichTile = 'refresh' 445 | } 446 | if (jsonlist."Refresh"=="Success") { 447 | sendEvent(name: "refreshTriggered", value: jsonlist."Date", unit: "") 448 | whichTile = 'refresh' 449 | } 450 | if (jsonlist."CustomTrigger"=="Authentication Required!") { 451 | sendEvent(name: "customTriggered", value: "Use Authentication Credentials", unit: "") 452 | } 453 | if (jsonlist."CustomTrigger"=="Success") { 454 | sendEvent(name: "customswitch", value: "on", isStateChange: true) 455 | sendEvent(name: "customTriggered", value: "MOMENTARY @ " + jsonlist."Date", unit: "") 456 | updateCustomEPOCH("from parse > CustomTrigger") 457 | whichTile = 'customoff' 458 | } 459 | if (jsonlist."CustomTriggerOn"=="Success" && jsonlist."CustomPinStatus"==1) { 460 | sendEvent(name: "customTriggered", value: "ON @ " + jsonlist."Date", unit: "") 461 | updateCustomEPOCH("from parse > CustomTriggerOn") 462 | whichTile = 'customon' 463 | } 464 | if (jsonlist."CustomTriggerOn"=="Authentication Required!") { 465 | sendEvent(name: "customTriggered", value: "Use Authentication Credentials", unit: "") 466 | } 467 | if (jsonlist."CustomTriggerOff"=="Success" && jsonlist."CustomPinStatus"==0) { 468 | sendEvent(name: "customTriggered", value: "OFF @ " + jsonlist."Date", unit: "") 469 | updateCustomEPOCH("from parse > CustomTriggerOff") 470 | whichTile = 'customoff' 471 | } 472 | if (jsonlist."CustomTriggerOff"=="Authentication Required!") { 473 | sendEvent(name: "customTriggered", value: "Use Authentication Credentials", unit: "") 474 | } 475 | if (jsonlist."CustomPinStatus"==1) { 476 | sendEvent(name: "customswitch", value: "on", isStateChange: true) 477 | updateCustomEPOCH("from parse > CustomPinStatus 1") 478 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 479 | whichTile = 'customon' 480 | } else if (jsonlist."CustomPinStatus"==0) { 481 | sendEvent(name: "customswitch", value: "off", isStateChange: true) 482 | updateCustomEPOCH("from parse > CustomPinStatus 0") 483 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 484 | whichTile = 'customoff' 485 | } 486 | if (jsonlist."MainTrigger"=="Authentication Required!") { 487 | sendEvent(name: "mainTriggered", value: "Use Authentication Credentials", unit: "") 488 | } 489 | if (jsonlist."MainTrigger"=="Success") { 490 | sendEvent(name: "switch", value: "on", isStateChange: true) 491 | sendEvent(name: "mainTriggered", value: "MOMENTARY @ " + jsonlist."Date", unit: "") 492 | updateMainEPOCH("from parse > MainTrigger") 493 | whichTile = 'mainoff' 494 | } 495 | if (jsonlist."MainTriggerOn"=="Success" && jsonlist."MainPinStatus"==1) { 496 | sendEvent(name: "mainTriggered", value: "ON @ " + jsonlist."Date", unit: "") 497 | updateMainEPOCH("from parse > MainTriggerOn") 498 | whichTile = 'mainon' 499 | } 500 | if (jsonlist."MainTriggerOn"=="Authentication Required!") { 501 | sendEvent(name: "mainTriggered", value: "Use Authentication Credentials", unit: "") 502 | } 503 | if (jsonlist."MainTriggerOff"=="Success" && jsonlist."MainPinStatus"==0) { 504 | sendEvent(name: "mainTriggered", value: "OFF @ " + jsonlist."Date", unit: "") 505 | updateMainEPOCH("from parse > MainTriggerOff") 506 | whichTile = 'mainoff' 507 | } 508 | if (jsonlist."MainTriggerOff"=="Authentication Required!") { 509 | sendEvent(name: "mainTriggered", value: "Use Authentication Credentials", unit: "") 510 | whichTile = 'mainoff' 511 | } 512 | if (jsonlist."MainPinStatus"==1) { 513 | sendEvent(name: "switch", value: "on", isStateChange: true) 514 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 515 | updateMainEPOCH("from parse > CustomPinStatus 1") 516 | whichTile = 'mainon' 517 | } else if (jsonlist."MainPinStatus"==0) { 518 | sendEvent(name: "switch", value: "off", isStateChange: true) 519 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 520 | updateMainEPOCH("from parse > CustomPinStatus 0") 521 | whichTile = 'mainoff' 522 | } 523 | if (device.currentState("contact")==null) {sendEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed")} 524 | if (jsonlist."SensorPinStatus"=="Open") { 525 | if (device.currentState("contact").getValue()=="closed") { sendEvent(name: "sensorTriggered", value: "OPEN @ " + jsonlist."Date", unit: "") } 526 | sendEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open") 527 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 528 | } else if (jsonlist."SensorPinStatus"=="Closed") { 529 | if (device.currentState("contact").getValue()=="open") { sendEvent(name: "sensorTriggered", value: "CLOSED @ " + jsonlist."Date", unit: "") } 530 | sendEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed") 531 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 532 | } else { 533 | sendEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed") 534 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 535 | } 536 | if (device.currentState("contact2")==null) {sendEvent(name: "contact2", value: "closed", descriptionText: "$device.displayName is closed")} 537 | if (jsonlist."Sensor2PinStatus"=="Open") { 538 | if (device.currentState("contact2").getValue()=="closed") { sendEvent(name: "sensor2Triggered", value: "OPEN @ " + jsonlist."Date", unit: "")} 539 | sendEvent(name: "contact2", value: "open", descriptionText: "$device.displayName is open") 540 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 541 | } else if (jsonlist."Sensor2PinStatus"=="Closed") { 542 | if (device.currentState("contact2").getValue()=="open") { sendEvent(name: "sensor2Triggered", value: "CLOSED @ " + jsonlist."Date", unit: "")} 543 | sendEvent(name: "contact2", value: "closed", descriptionText: "$device.displayName is closed") 544 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 545 | } else { 546 | sendEvent(name: "contact2", value: "closed", descriptionText: "$device.displayName is closed") 547 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 548 | } 549 | if (jsonlist."CPU") { 550 | sendEvent(name: "cpuUsage", value: jsonlist."CPU".replace("=","\n").replace("%",""), unit: "") 551 | } 552 | if (jsonlist."Space Used") { 553 | sendEvent(name: "spaceUsed", value: jsonlist."Space Used".replace("=","\n").replace("%",""), unit: "") 554 | } 555 | if (jsonlist."UpTime") { 556 | sendEvent(name: "upTime", value: jsonlist."UpTime".replace("=","\n"), unit: "") 557 | } 558 | if (jsonlist."CPU Temp") { 559 | sendEvent(name: "cpuTemp", value: jsonlist."CPU Temp".replace("=","\n").replace("\'","°").replace("C ","C="), unit: "") 560 | } 561 | 562 | if (jsonlist."Free Mem") { 563 | sendEvent(name: "freeMem", value: jsonlist."Free Mem".replace("=","\n"), unit: "") 564 | } 565 | if (jsonlist."Temperature") { 566 | //sendEvent(name: "temperature", value: jsonlist."Temperature".replace("=","\n").replace("\'","°").replace("C ","C="), unit: "") 567 | if (DeviceTempMeasurement == "Celcius") { 568 | sendEvent(name: "temperature", value: jsonlist."Temperature".split(" ")[0].replace("°C"," ").replace("\'C"," "), unit: "") 569 | } else { 570 | sendEvent(name: "temperature", value: jsonlist."Temperature".split(" ")[1].replace("°F"," ").replace("\'F"," "), unit: "") 571 | } 572 | //String s = jsonlist."Temperature" 573 | //for(int i = 0; i < s.length(); i++) { 574 | // int c = s.charAt(i); 575 | // log.trace "'${c}'\n" 576 | //} 577 | } else { 578 | sendEvent(name: "temperature", value: 0, unit: "") 579 | } 580 | if (jsonlist."Humidity") { 581 | sendEvent(name: "humidity", value: jsonlist."Humidity".replace("=","\n"), unit: "") 582 | } else { 583 | sendEvent(name: "humidity", value: 0, unit: "") 584 | } 585 | if (jsonlist."RebootNow") { 586 | whichTile = 'RebootNow' 587 | } 588 | } 589 | 590 | log.debug jsonlist 591 | 592 | //RESET THE DEVICE ID TO GENERIC/RANDOM NUMBER. THIS ALLOWS MULTIPLE DEVICES TO USE THE SAME ID/IP 593 | device.deviceNetworkId = "ID_WILL_BE_CHANGED_AT_RUNTIME_" + (Math.abs(new Random().nextInt()) % 99999 + 1) 594 | 595 | //CHANGE NAME TILE 596 | sendEvent(name: "displayName", value: DeviceIP, unit: "") 597 | 598 | //RETURN BUTTONS TO CORRECT STATE & SET EPOCH AGAIN 599 | log.debug 'whichTile: ' + whichTile 600 | switch (whichTile) { 601 | case 'refresh': 602 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 603 | return createEvent(name: "refreshswitch", value: "default", isStateChange: true) 604 | //log.debug "refreshswitch returned ${result?.descriptionText}" 605 | case 'customoff': 606 | sendEvent(name: "customswitch", value: "off", isStateChange: true) 607 | return createEvent(name: "customswitch", value: "off", isStateChange: true) 608 | updateCustomEPOCH("from parse > customoff") 609 | case 'customon': 610 | sendEvent(name: "customswitch", value: "on", isStateChange: true) 611 | return createEvent(name: "customswitch", value: "on", isStateChange: true) 612 | updateCustomEPOCH("from parse > customon") 613 | case 'mainoff': 614 | return createEvent(name: "switch", value: "off", isStateChange: true) 615 | case 'mainon': 616 | return createEvent(name: "switch", value: "on", isStateChange: true) 617 | case 'RebootNow': 618 | sendEvent(name: "rebootnow", value: "default", isStateChange: true) 619 | return createEvent(name: "rebootnow", value: "default", isStateChange: true) 620 | default: 621 | sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 622 | return createEvent(name: "refreshswitch", value: "default", isStateChange: true) 623 | //log.debug "refreshswitch returned ${result?.descriptionText}" 624 | } 625 | } 626 | 627 | def parseDescriptionAsMap(description) { 628 | description.split(",").inject([:]) { map, param -> 629 | def nameAndValue = param.split(":") 630 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 631 | } 632 | } 633 | private String convertIPtoHex(ipAddress) { 634 | String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join() 635 | //log.debug "IP address entered is $ipAddress and the converted hex code is $hex" 636 | return hex 637 | } 638 | private String convertPortToHex(port) { 639 | String hexport = port.toString().format( '%04x', port.toInteger() ) 640 | //log.debug hexport 641 | return hexport 642 | } 643 | private Integer convertHexToInt(hex) { 644 | Integer.parseInt(hex,16) 645 | } 646 | private String convertHexToIP(hex) { 647 | //log.debug("Convert hex to ip: $hex") 648 | [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") 649 | } 650 | private getHostAddress() { 651 | def parts = device.deviceNetworkId.split(":") 652 | //log.debug device.deviceNetworkId 653 | def ip = convertHexToIP(parts[0]) 654 | def port = convertHexToInt(parts[1]) 655 | return ip + ":" + port 656 | } 657 | -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/README.md: -------------------------------------------------------------------------------- 1 | # Generic HTTP Device 2 | Link to the project: https://community.smartthings.com/t/43335 3 | 4 | This project consists of a Raspberry Pi running Raspbian OS. It runs HTTPD with index.php as the source. The PHP runs the gpio command in order to enable the pins on the Pi. The code sample in the PHP file causes a relay to momentarily turn on then off. I'm using this on a gate so the short/momentary capability was key. However, it's very customizable and now offers on/off states for both switches. For advanced/full instructions on installing and configuring Raspbian OS, see the project's forum link at the top. 5 | 6 | The code and project expanded to the use of Atmel/AVR devices like Arduino UNO/Nano/Mega and the WIFI capable SOC like ESP8266/NodeMCU/WeMos D1 Mini. It can report temperature & humidity back to SmartThings using the DHT modules. It can also report magnetic contact sensor state to SmartThings. Basic HTTP authentication is available as an option. The code can use the popular Ethernet module ENC28J60 using a custom library: 7 | https://github.com/UIPEthernet/UIPEthernet 8 | 9 | ---SMARTTHINGS: 10 | The GenericHTTPDevice.groovy file is the Device Handler for SmartThings. The VirtualCustomSwitch.groovy file can be used in conjunction with the SmartApp which will keep the custom button in sync with a virtual one: 11 | https://github.com/JZ-SmartThings/SmartThings/tree/master/SmartApps/Virtual%20Custom%20Switch%20Sync%20App 12 | 13 | ---RASPBERY PI or LINUX: 14 | For Raspberry Pi or Linux, use index.php and likely place it in the /var/www/html folder of your RPi. It runs the external gpio command in Linux. 15 | At the top of index.php, change the first variable to "true" instead of "false" and this will make the PHP page protected with basic authentication. After making that change, make sure to change the SmartThings preferences for the device. 16 | 17 | ---ARDUINO or NODEMCU 18 | The *.ino files are the Arduino IDE code samples. Verify the few options at the top of the script before flashing your device. 19 | 20 | This project was tested successfully via an external IP, Pollster and with an Amazon Echo/Alexa. Echo can run TWO functions in my app. The ON command triggers the main function and OFF triggers the custom function but can be changed to only control the Main switch. 21 | 22 | * v1.0.20181021 - Fixed Groovy DTH to show the PHP Contact Sensor states properly. 23 | * v1.0.20180916 - Fixed Groovy DTH to correctly handle PHP null JSON values. 24 | * v1.0.20180502 - Lots of bug fixes. MQTT logic optimized eliminating intermittent issues with large HTML client responses while doing MQTT execution in-line. Added ability to limit frequency of requests. This helps with MQTT infinite loops with on & off constantly triggered. Reliability should be much better with this version. 25 | * v1.0.20171030 - Modified DHT retry logic in Arduino code. Modified main Groovy code & Virtual Sync SmartApp to stop assuming that momentary main means that Alexa should use the OFF command for custom/secondary trigger. This caused issues with SmartThings MQTT Bridge as the OFF command, sticking to explicit settings. 26 | * v1.0.20171008 - Added MQTT & hosting JSON status page. New support Eco Plugs/WiOn switches (by KAB) & Sonoff devices. Easy ability to integrate with Home Assistant. Cleaner UI after hiding settings. 27 | * v1.0.20170826 - Updated PHP, Generic HTTP Device & Virtual Sync App code. PHP was for better ability to interface with Node RED. Groovy code was updated for better synchronization between secondary switch & virtual switch. 28 | * v1.0.20170408 - Both virtual device handlers (2nd switch & 2nd sensor) now have refresh ability & a last-refreshed info tile. 29 | * v1.0.20170327 - Added refresh ability to the SmartApp to eliminate reliance on other refresh utilities/apps. 30 | * v1.0.20170327 - Added 2nd contact sensor. Created a virtual device handler for synchronizing the 2nd contact sensor's state for automation. 31 | * v1.0.20170326 - Fixed MAC address when using RJ45/UIPEthernet. Use5Vrelay flag is now controlled via UI not variable & stored in EEPROM. Inversion of Contact Sensor status, allows for flipping the definition of what closed & open means (NO/NC logic). 32 | * v1.0.20170227 - Created a SmartApp & a Virtual Custom Switch device. The SmartApp will keep the virtual switch synced up with the Custom Switch of the generic HTTP device. This will help to automate the secondary/custom button. 33 | * v1.0.20170221 - Changed all code samples including ST Device Handler. Defaulting the contact sensor to closed state for accurate ST alerts. Contact Sensor enabling flag now resides in the EEPROM & in the PHP code it's defined at the top. Fixed UIPEthernet IP not showing up on Arduino UNO/Nano page. 34 | * v1.0.20170218 - Changed only the Arduino/NodeMCU code samples to use the GND signal instead of VCC for better accuracy. Read up on INPUT_PULLUP for my reasoning. 35 | * v1.0.20170214 - Added contact sensor ability. Using SmartThings "capability" so rules can be respected when sensor state changes. Modified both Arduino sketches and PHP samples. 36 | * v1.0.20170126 - NodeMCU sketch DHT sensor retry logic is a bit smarter. Small bug fixes. 37 | * v1.0.20170121 - NodeMCU sketch is now able to do OTA updates via a web-page or directly via the Arduino IDE. 38 | * v1.0.20170110 - Arduino & NodeMCU code updated. Stateful on/off functionality fixed. DHT sensor will now retry 5 times so should return results with more success than before. DHT part number displayed. 39 | * v1.0.20170108 - ESP8266/NodeMCU can now be wired with an ENC28J60 using modified UIPEthernet library. Ability to use transistor/mosfet rather than relays. Reboot is now optionally modified via the web-page & stored in the first byte of the EEPROM thus the setting will survive a reboot or a flash. ST code now will work with an invalid header (impacts ESP8266 & ENC28J60 unreliability). Wiring diagrams updated. 40 | * v1.0.20161231 - Arduino sketch enhancements & fixes: Added free memory/heap logic. UpTime reports day/days correctly now. Replaced reboot logic with a simple while loop to cause a soft watchdog reset. Switched to use5Vrelay=true to match wiring diagrams. Added abort when favicon.ico request is sent. 41 | * v1.0.20161223 - Added UpTime for the Arduino IDE samples. Added device name & IP at the top of SmartThings screen. Added wiring for Arduino Nano & a generic ENC28J60 ethernet board. 42 | * v1.0.20160806 - Security issue with the ESP8266 library, read this post: https://community.smartthings.com/t/esp8266-nodemcu-arduino-based-tv-remote/50161/22 43 | * v1.0.20160731 - Added Arduino Nano v3 plus ENC28J60 Ethenet Shield sample code. Updated wiring diagrams. 44 | * v1.0.20160719 - NodeMCU enhancements: forcing reboot every 8 hours to guarantee device being up. Also added simple variable to control whether using 5v or 3.3v relays & whether logic sends a HIGH or a LOW depending on that variable. Added temperature & humidity support for DHT11, DHT22 & DHT21. 45 | * v1.0.20160604 - Added Arduino / ESP8266-12E / NodeMCU Support and a code sample to GitHub. 46 | * v1.0.20160430 - Main and Custom switches can now be momentary or have on/off states. GPIO pin numbers for Main/Custom is now configurable. Ability to force Echo/Alexa to be restricted in controlling the Main switch when running ON/OFF commands when configured to be stateful ON/OFF (not momentary). Refresh works and updates Main/Custom tile per the GPIO pin status. Version contains lots of enhancements, bug fixes and future proofing. 47 | * v1.0.20160428 - Now able to control in preferences whether CustomTrigger is momentary or has states like on/off. This is the CUSTOM tile, which can easily be made as the MAIN/primary tile, search top of ST code for MAIN designation & change to CustomTrigger. 48 | * v1.0.20160423 - Small changes: using second line of df -h for space used. Scaling GPIO image better for RPi 3 with more lines on gpio readall command. 49 | * v1.0.20160410 - Changing device ID to random number at end of execution so multiple devices can point to one IP. Amazon Echo can run both ON/main and OFF/custom functions. Added ability to output GPIO status in PHP. Releasing another ST device that will pull the image and control the installation of required packages hence the reset of device ID. 50 | * v1.0.20160407 - Added Poll & Refresh to execute the TEST function. Now able to add the switch to Pollster and should see the time update in Test Triggered. Validated external IP with custom port and Amazon Echo/Alexa. 51 | * v1.0.20160406 - Added the CustomTrigger button. Made buttons smaller. CPU Temp now accurately converts C to F. Added color to some tiles. Defaulting in port & body if left empty in prefs. 52 | * v1.0.20160405 - Modified Space Used to use awk instead of cut and delimiters. GitHub reported bug. 53 | * v1.0.20160402 - Added JSON support. Modified Reboot tile with states. Added Clear Tiles button. 54 | * v1.0.20160328 - Modified tile background while in "running" states and added free memory tile. 55 | * v1.0.20160327 - Toggling tile states in general and with respect to authentication. Fixed GPIO in the PHP script to be correct and toggle on then off. Used to have off then on, which is incorrect. 56 | * v1.0.20160326 - Added temperature. Added basic authentication. 57 | * v1.0.20160323 - Initial version 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/Screenhot_Prototype.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/Screenhot_Prototype.jpg -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/Screenshot_Android_App.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/Screenshot_Android_App.png -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/Screenshot_Arduino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/Screenshot_Arduino.png -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/Screenshot_PHP_Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/Screenshot_PHP_Page.png -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/Virtual2ndContactSensor.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Virtual 2nd Contact Sensor v1.0.20170408 3 | * Copyright 2017 JZ 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. You may obtain a copy of the License at: 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 10 | * for the specific language governing permissions and limitations under the License. 11 | */ 12 | metadata { 13 | definition (name: "Virtual 2nd Contact Sensor", namespace: "JZ", author: "JZ") { 14 | capability "Contact Sensor" 15 | capability "Sensor" 16 | capability "Refresh" 17 | 18 | command "open" 19 | command "close" 20 | } 21 | simulator { 22 | status "open": "contact:open" 23 | status "closed": "contact:closed" 24 | } 25 | tiles(scale: 2) { 26 | standardTile("contact", "device.contact", width: 6, height: 2) { 27 | state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#53a7c0") 28 | state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#FF6600") 29 | } 30 | valueTile("sensor2Triggered", "device.sensor2Triggered", width: 6, height: 2, decoration: "flat") { 31 | state("default", label: 'Sensor 2 State Changed:\r\n${currentValue}', backgroundColor:"#ffffff") 32 | } 33 | valueTile("refreshTriggered", "device.refreshTriggered", width: 4, height: 2, decoration: "flat") { 34 | state("default", label: 'Refreshed:\r\n${currentValue}', backgroundColor:"#ffffff") 35 | } 36 | standardTile("refresh", "device.refresh", width: 2, height: 2, decoration: "flat") { 37 | state "default", label:'REFRESH', action: "refresh", icon: "st.secondary.refresh-icon", backgroundColor:"#53a7c0", nextState: "refreshing" 38 | state "refreshing", label: 'REFRESHING', action: "refresh", icon: "st.secondary.refresh-icon", backgroundColor: "#FF6600", nextState: "default" 39 | } 40 | main "contact" 41 | details (["contact","sensor2Triggered","refreshTriggered","refresh"]) 42 | } 43 | } 44 | 45 | def refresh() { 46 | log.debug "refresh()" 47 | sendEvent(name: "refresh", value: new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone)) 48 | } 49 | 50 | def parseORIG(String description) { 51 | def pair = description.split(":") 52 | createEvent(name: pair[0].trim(), value: pair[1].trim()) 53 | } 54 | 55 | def parse(description) { 56 | def eventMap 57 | if (description.type == null) eventMap = [name:"$description.name", value:"$description.value"] 58 | else eventMap = [name:"$description.name", value:"$description.value", type:"$description.type"] 59 | createEvent(eventMap) 60 | } 61 | 62 | def open() { 63 | log.trace "open()" 64 | sendEvent(name: "contact", value: "open") 65 | } 66 | 67 | def close() { 68 | log.trace "close()" 69 | sendEvent(name: "contact", value: "closed") 70 | } -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/VirtualCustomSwitch.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Virtual Custom Switch v1.0.20170408 3 | * Copyright 2017 JZ 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. You may obtain a copy of the License at: 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 10 | * for the specific language governing permissions and limitations under the License. 11 | */ 12 | metadata { 13 | definition (name: "Virtual Custom Switch", namespace: "JZ", author: "JZ") { 14 | capability "Switch" 15 | capability "Refresh" 16 | attribute "refresh", "string" 17 | } 18 | 19 | tiles(scale: 2) { 20 | standardTile("switch", "device.switch", width: 6, height: 2, canChangeIcon: true) { 21 | state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" 22 | state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" 23 | } 24 | valueTile("customTriggered", "device.customTriggered", width: 6, height: 2, decoration: "flat") { 25 | state("default", label: 'Custom triggered:\r\n${currentValue}', backgroundColor:"#ffffff") 26 | } 27 | valueTile("refreshTriggered", "device.refreshTriggered", width: 4, height: 2, decoration: "flat") { 28 | state("default", label: 'Refreshed:\r\n${currentValue}', backgroundColor:"#ffffff") 29 | } 30 | standardTile("refresh", "device.refresh", width: 2, height: 2, decoration: "flat") { 31 | state "default", label:'REFRESH', action: "refresh", icon: "st.secondary.refresh-icon", backgroundColor:"#53a7c0", nextState: "refreshing" 32 | state "refreshing", label: 'REFRESHING', action: "refresh", icon: "st.secondary.refresh-icon", backgroundColor: "#FF6600", nextState: "default" 33 | } 34 | main "switch" 35 | details(["switch","on","off","customTriggered","refreshTriggered","refresh"]) 36 | } 37 | } 38 | 39 | def refresh() { 40 | log.debug "refresh()" 41 | sendEvent(name: "refresh", value: new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone)) 42 | } 43 | 44 | def parse(description) { 45 | def eventMap 46 | if (description.type == null) eventMap = [name:"$description.name", value:"$description.value"] 47 | else eventMap = [name:"$description.name", value:"$description.value", type:"$description.type"] 48 | createEvent(eventMap) 49 | } 50 | 51 | def on() { 52 | log.debug "$version on()" 53 | sendEvent(name: "switch", value: "on") 54 | } 55 | 56 | def off() { 57 | log.debug "$version off()" 58 | sendEvent(name: "switch", value: "off") 59 | } 60 | 61 | private getVersion() { 62 | "PUBLISHED" 63 | } -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/WIRING/ArduinoNano-DualRelay5V.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/WIRING/ArduinoNano-DualRelay5V.fzz -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/WIRING/ArduinoNano-DualRelay5V.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/WIRING/ArduinoNano-DualRelay5V.png -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/WIRING/ArduinoNano-ENC28J60-DualRelay5V.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/WIRING/ArduinoNano-ENC28J60-DualRelay5V.fzz -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/WIRING/ArduinoNano-ENC28J60-DualRelay5V.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/WIRING/ArduinoNano-ENC28J60-DualRelay5V.png -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/WIRING/ArduinoUNO-ENC28J60-DualRelay5V.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/WIRING/ArduinoUNO-ENC28J60-DualRelay5V.fzz -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/WIRING/ArduinoUNO-ENC28J60-DualRelay5V.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/WIRING/ArduinoUNO-ENC28J60-DualRelay5V.png -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/WIRING/NodeMCU-DualRelay5V.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/WIRING/NodeMCU-DualRelay5V.fzz -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/WIRING/NodeMCU-DualRelay5V.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/WIRING/NodeMCU-DualRelay5V.png -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/WIRING/NodeMCU-ENC28J60-DualRelay5V.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/WIRING/NodeMCU-ENC28J60-DualRelay5V.fzz -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/WIRING/NodeMCU-ENC28J60-DualRelay5V.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/Generic HTTP Device/WIRING/NodeMCU-ENC28J60-DualRelay5V.png -------------------------------------------------------------------------------- /Devices/Generic HTTP Device/index.php: -------------------------------------------------------------------------------- 1 | "gate1"); 11 | $valid_users = array_keys($valid_passwords); 12 | $user = $_SERVER['PHP_AUTH_USER']; 13 | $pass = $_SERVER['PHP_AUTH_PW']; 14 | $validated = (in_array($user, $valid_users)) && ($pass == $valid_passwords[$user]); 15 | if (!$validated) { 16 | header('WWW-Authenticate: Basic realm="Generic HTTP Device"'); 17 | header('HTTP/1.0 401 Unauthorized'); 18 | if (isset($_POST['Refresh'])) { 19 | echo "Refresh=Failed : "; 20 | } 21 | if (isset($_POST['MainTrigger']) || isset($_POST['MainTriggerOn']) || isset($_POST['MainTriggerOff'])) { 22 | echo "MainTrigger=Failed : "; 23 | } 24 | if (isset($_POST['CustomTrigger']) || isset($_POST['CustomTriggerOn']) || isset($_POST['CustomTriggerOff'])) { 25 | echo "CustomTrigger=Failed : "; 26 | } 27 | if (isset($_POST['RebootNow'])) { 28 | echo "RebootNow=Failed : "; 29 | } 30 | if (isset($_POST['GPIO'])) { 31 | echo "GPIO=Failed : "; 32 | } 33 | if (isset($_POST['GDInstall'])) { 34 | echo "GDInstall=Failed : "; 35 | } 36 | die ("Authentication Required!"); 37 | } 38 | } 39 | // If code arrives here, this would be a valid user. 40 | 41 | //BUILD ARRAY VALUES 42 | date_default_timezone_set('America/Los_Angeles'); 43 | //CHECK IF GD IS INSTALLED 44 | $gd_installed=false; 45 | foreach(get_loaded_extensions() as $name){ if ($name=='gd') {$gd_installed=true;}} 46 | $rpi = array( 47 | "Date" => date("Y-m-d h:i:sA"), 48 | "Space Used" => shell_exec('df -h | awk \'NR==2\' | awk \'{print $(NF-1)}\' | tr -d \'\n\''), 49 | "UpTime" => trim(substr(shell_exec('uptime'),strpos(shell_exec('uptime'), 'up')+2, strpos(shell_exec('uptime'), ',')-strpos(shell_exec('uptime'), 'up')-2)), 50 | "CPU" => shell_exec('grep \'cpu \' /proc/stat | awk \'{usage=($2+$4)*100/($2+$4+$5)} END {print usage "%"}\' | sed \'s/\(\.[0-9]\).*$/\1%/g\' | tr -d \'\n\''), 51 | "CPU Temp" => CPUTemp(), 52 | "Free Mem" => shell_exec('free -t -h | tr -s " " | grep "Total:" | awk -F " " \'{print $4 " of " $2}\' | tr -d \'\n\''), 53 | "php-gd" => $gd_installed 54 | ); 55 | 56 | function CPUTemp() { 57 | $celcius = shell_exec('sudo vcgencmd measure_temp | sed "s/temp=//g" | tr -d \'\n\''); 58 | $fahrenheit = round(substr(str_replace("C","",$celcius), 0, -1) * 1.8 + 32, 0) . "'F"; 59 | return $celcius .' '. $fahrenheit; 60 | } 61 | 62 | if ($contact_sensor) { 63 | shell_exec("sudo gpio -g mode $sensor_pin in"); 64 | $sensor_pin_status = shell_exec("sudo raspi-gpio get $sensor_pin | grep 'func=INPUT' | grep 'level=1'"); 65 | if (strlen($sensor_pin_status) > 5) { 66 | $rpi = $rpi + array("SensorPinStatus" => 0); 67 | } else { 68 | $rpi = $rpi + array("SensorPinStatus" => 1); 69 | } 70 | } else { // Default to Closed 71 | $rpi = $rpi + array("SensorPinStatus" => 0); 72 | } 73 | if ($contact_sensor_2) { 74 | shell_exec("sudo gpio -g mode $sensor_pin_2 in"); 75 | $sensor_pin_status_2 = shell_exec("sudo raspi-gpio get $sensor_pin_2 | grep 'func=INPUT' | grep 'level=1'"); 76 | if (strlen($sensor_pin_status_2) > 5) { 77 | $rpi = $rpi + array("Sensor2PinStatus" => 0); 78 | } else { 79 | $rpi = $rpi + array("Sensor2PinStatus" => 1); 80 | } 81 | } else { // Default to Closed 82 | $rpi = $rpi + array("Sensor2PinStatus" => 0); 83 | } 84 | 85 | $main_pin=4; 86 | if (isset($_POST['MainPin'])) { 87 | if (strlen($_POST['MainPin'])>0) { $main_pin=(int)substr(str_replace(";","",$_POST['MainPin']),0,2); } 88 | } 89 | $rpi = $rpi + array("MainPin" => $main_pin); 90 | if (isset($_POST['MainTrigger'])) { 91 | exec("sudo gpio -g mode $main_pin out ; gpio -g write $main_pin 0 ; sleep 0.1 ; gpio -g write $main_pin 1"); 92 | $rpi = $rpi + array("MainTrigger" => "Success"); 93 | } 94 | if (isset($_POST['MainTriggerOn'])) { 95 | shell_exec("sudo gpio -g mode $main_pin out ; gpio -g write $main_pin 0"); 96 | $rpi = $rpi + array("MainTriggerOn" => "Success"); 97 | } 98 | if (isset($_POST['MainTriggerOff'])) { 99 | shell_exec("sudo gpio -g mode $main_pin out ; gpio -g write $main_pin 1"); 100 | $rpi = $rpi + array("MainTriggerOff" => "Success"); 101 | } 102 | $main_pin_status = shell_exec("sudo raspi-gpio get $main_pin | grep 'func=OUTPUT' | grep 'level=1'"); 103 | if (strlen($main_pin_status) > 5) { 104 | $rpi = $rpi + array("MainPinStatus" => 0); 105 | } else { 106 | $rpi = $rpi + array("MainPinStatus" => 1); 107 | } 108 | 109 | $custom_pin=21; 110 | if (isset($_POST['CustomPin'])) { 111 | if (strlen($_POST['CustomPin'])>0) { $custom_pin=(int)substr(str_replace(";","",$_POST['CustomPin']),0,2); } 112 | } 113 | $rpi = $rpi + array("CustomPin" => $custom_pin); 114 | if (isset($_POST['CustomTrigger'])) { 115 | shell_exec("sudo gpio -g mode $custom_pin out ; gpio -g write $custom_pin 0 ; sleep 0.1 ; gpio -g write $custom_pin 1"); 116 | $rpi = $rpi + array("CustomTrigger" => "Success"); 117 | } 118 | if (isset($_POST['CustomTriggerOn'])) { 119 | shell_exec("sudo gpio -g mode $custom_pin out ; gpio -g write $custom_pin 0"); 120 | $rpi = $rpi + array("CustomTriggerOn" => "Success"); 121 | } 122 | if (isset($_POST['CustomTriggerOff'])) { 123 | shell_exec("sudo gpio -g mode $custom_pin out ; gpio -g write $custom_pin 1"); 124 | $rpi = $rpi + array("CustomTriggerOff" => "Success"); 125 | } 126 | $custom_pin_status = shell_exec("sudo raspi-gpio get $custom_pin | grep 'func=OUTPUT' | grep 'level=1'"); 127 | if (strlen($custom_pin_status) > 5) { 128 | $rpi = $rpi + array("CustomPinStatus" => 0); 129 | } else { 130 | $rpi = $rpi + array("CustomPinStatus" => 1); 131 | } 132 | 133 | if (isset($_POST['Refresh'])) { 134 | $rpi = $rpi + array("Refresh" => "Success"); 135 | } 136 | if (isset($_POST['RebootNow'])) { 137 | shell_exec("sudo shutdown -r now"); 138 | $rpi = $rpi + array("RebootNow" => "Success"); 139 | } 140 | if (isset($_POST['GDInstall']) && $rpi['php-gd'] != "Installed") { 141 | $gdinstalling = str_replace("\n","",shell_exec('sudo ps -ef | grep php-gd | grep -v grep | wc -l')); 142 | if ($gdinstalling=="0") { 143 | shell_exec('sudo apt-get update; sudo apt-get -y install php-gd --fix-missing ; sudo service apache2 restart'); 144 | } 145 | } 146 | if (isset($_POST['GPIO']) && $rpi['php-gd'] == true) { 147 | header("Content-type: image/jpeg"); 148 | $gpiolines = shell_exec("gpio readall | wc -l"); 149 | $imheight = 200 + ($gpiolines - 19)*12; 150 | $im = @imagecreate(400, $imheight) or die("Cannot initialize new GD image stream."); 151 | $background_color = imagecolorallocate($im, 255, 255, 255); 152 | $text_color = imagecolorallocate($im, 0, 0, 0); 153 | $green = imagecolorallocate($im, 51, 255, 153); 154 | $blue = imagecolorallocate($im, 102, 204, 255); 155 | $red = imagecolorallocate($im, 255, 51, 51); 156 | imagefilledrectangle($im, 8, 5, 196, $imheight-5, $green); 157 | imagefilledrectangle($im, 203, 5, 391, $imheight-5, $blue); 158 | imagefilledrectangle($im, 172, 0, 227, 11, $red); 159 | $array = explode("\n", shell_exec("gpio readall")); 160 | imagestring($im, 1.3, 0, 0, " ", $text_color); 161 | $counter=0; 162 | foreach ($array as $v) { 163 | imagestring($im, 1.3, 0, $counter, $v, $text_color); 164 | $counter = $counter + 12; 165 | } 166 | //imagejpeg($im, NULL, 80-(3*($gpiolines-19))); 167 | imagejpeg($im, NULL, 90); 168 | imagedestroy($im); 169 | exit; 170 | } 171 | if (isset($_POST['UseJSON']) || isset($_GET['UseJSON'])) { 172 | header('Content-type: application/json'); 173 | echo json_encode($rpi, JSON_PRETTY_PRINT); 174 | die (); 175 | } 176 | ?> 177 | 178 | 179 | 180 | 181 | 182 | Raspberry Pi Relay 183 | 207 | 208 | 209 | 210 |
211 |
212 | 
244 | 
245 | 246 |
247 | 248 |
249 |
250 |   251 |   252 | 253 |
254 |
   Main Pin # in BCM
255 |
256 |
257 |
258 |   259 |   260 | 261 |
262 |
   Custom Pin # in BCM
263 |
264 |
265 | 266 |
267 | 268 | 269 |
270 | 271 | 272 |
273 | 274 |
   UseJSON
275 |
276 |
277 |
278 | Project on SmartThings Community
279 | Project on GitHub
280 |
281 | 282 |
283 | 284 | 285 | -------------------------------------------------------------------------------- /Devices/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Devices/Panasonic PTZ Camera/PanasonicPTZCamera.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Panasonic Camera 3 | * 4 | * Source code can be found here: https://github.com/JZ-SmartThings/SmartThings/blob/master/Devices/Panasonic%20PTZ%20Camera/PanasonicPTZCamera.groovy 5 | * 6 | * Copyright 2017 JZ 7 | * 8 | * Tested with Panasonic BL-C30A, BB-HCM511A, BB-HCM531A 9 | * Thanks to: patrickstuart & blebson 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 12 | * in compliance with the License. You may obtain a copy of the License at: 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 17 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 18 | * for the specific language governing permissions and limitations under the License. 19 | * 20 | */ 21 | 22 | metadata { 23 | definition (name: "Panasonic Camera", author: "JZ", namespace: "JZ") { 24 | capability "Image Capture" 25 | capability "Sensor" 26 | capability "Actuator" 27 | 28 | attribute "hubactionMode", "string" 29 | 30 | command "left" 31 | command "right" 32 | command "up" 33 | command "down" 34 | command "home" 35 | command "preset1" 36 | command "preset2" 37 | command "preset3" 38 | } 39 | 40 | preferences { 41 | input("CameraIP", "string", title:"Camera IP Address", description: "Please enter your camera's IP Address", required: true, displayDuringSetup: true) 42 | input("CameraPort", "string", title:"Camera Port", description: "Re-type camera port, usually 80", defaultValue: 80 , required: true, displayDuringSetup: true) 43 | input("CameraAuth", "bool", title:"Does Camera require User Auth?", description: "Choose if the camera requires basic authentication", defaultValue: true, displayDuringSetup: true) 44 | input("CameraPostGet", "string", title:"Does Camera use a Post or Get, normally Get?", description: "Re-type GET", defaultValue: "GET", displayDuringSetup: true) 45 | input("CameraUser", "string", title:"Camera User", description: "Please enter your camera's username", required: false, displayDuringSetup: true) 46 | input("CameraPassword", "string", title:"Camera Password", description: "Please enter your camera's password", required: false, displayDuringSetup: true) 47 | } 48 | 49 | simulator { 50 | } 51 | 52 | tiles { 53 | standardTile("take", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { 54 | state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking" 55 | state "taking", label:'Taking', action: "", icon: "st.camera.take-photo", backgroundColor: "#53a7c0" 56 | state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking" 57 | } 58 | standardTile("home", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 59 | state "home", label: "Home", action: "home", icon: "st.Home.home2" 60 | } 61 | standardTile("preset1", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 62 | state "preset1", label: "Preset 1", action: "preset1", icon: "st.camera.dlink-hdpan" 63 | } 64 | carouselTile("cameraDetails", "device.image", width: 3, height: 2) { } 65 | 66 | standardTile("blank", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 67 | state "blank", label: "", action: "", icon: "", backgroundColor: "#FFFFFF" 68 | } 69 | standardTile("up", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 70 | state "up", label: "", action: "up", icon: "st.samsung.da.oven_ic_up" 71 | } 72 | standardTile("preset2", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 73 | state "preset2", label: "Preset 2", action: "preset2", icon: "st.camera.dlink-hdpan" 74 | } 75 | standardTile("preset3", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 76 | state "preset3", label: "Preset 3", action: "preset3", icon: "st.camera.dlink-hdpan" 77 | } 78 | standardTile("left", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 79 | state "left", label: "", action: "left", icon: "st.samsung.da.RAC_4line_01_ic_left" 80 | } 81 | standardTile("down", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 82 | state "down", label: "", action: "down", icon: "st.samsung.da.oven_ic_down" 83 | } 84 | standardTile("right", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 85 | state "right", label: "", action: "right", icon: "st.samsung.da.RAC_4line_03_ic_right" 86 | } 87 | main "take" 88 | details([ "take", "home", "preset1", "cameraDetails", "preset2", "up", "preset3", "left", "down", "right"]) 89 | } 90 | } 91 | 92 | def take() { 93 | log.debug "Taking picture" 94 | cameraCmd("/SnapshotJPEG?Resolution=640x480") 95 | } 96 | def home() { 97 | log.debug "Moving to Home position" 98 | cameraCmd("/nphControlCamera?Direction=HomePosition") 99 | } 100 | def preset1() { 101 | log.debug "Moving to Preset 1 position" 102 | cameraCmd("nphControlCamera?Direction=Preset&PresetOperation=Move&Data=1") 103 | } 104 | def preset2() { 105 | log.debug "Moving to Preset 2 position" 106 | cameraCmd("nphControlCamera?Direction=Preset&PresetOperation=Move&Data=2") 107 | } 108 | def preset3() { 109 | log.debug "Moving to Preset 3 position" 110 | cameraCmd("nphControlCamera?Direction=Preset&PresetOperation=Move&Data=3") 111 | } 112 | def up() { 113 | log.debug "Tilt Up" 114 | cameraCmd("/nphControlCamera?Direction=TiltUp") 115 | } 116 | def left() { 117 | log.debug "Pan Left" 118 | cameraCmd("/nphControlCamera?Direction=PanLeft") 119 | } 120 | def right() { 121 | log.debug "Pan Right" 122 | cameraCmd("/nphControlCamera?Direction=PanRight") 123 | } 124 | def down() { 125 | log.debug "Tilt Down" 126 | cameraCmd("/nphControlCamera?Direction=TiltDown") 127 | } 128 | 129 | 130 | def cameraCmd(String varCommand) { 131 | def userpassascii = "${CameraUser}:${CameraPassword}" 132 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString() 133 | def host = CameraIP 134 | def hosthex = convertIPtoHex(host).toUpperCase() 135 | def porthex = convertPortToHex(CameraPort).toUpperCase() 136 | device.deviceNetworkId = "$hosthex:$porthex" 137 | 138 | log.debug "The device id configured is: $device.deviceNetworkId" 139 | 140 | def path = varCommand 141 | log.debug "path is: $path" 142 | log.debug "Requires Auth: $CameraAuth" 143 | log.debug "Uses which method: $CameraPostGet" 144 | 145 | def headers = [:] 146 | headers.put("HOST", "$host:$CameraPort") 147 | if (CameraAuth) { 148 | headers.put("Authorization", userpass) 149 | } 150 | 151 | log.debug "The Header is $headers" 152 | 153 | def method = "GET" 154 | try { 155 | if (CameraPostGet.toUpperCase() == "POST") { 156 | method = "POST" 157 | } 158 | } 159 | catch (Exception e) { 160 | settings.CameraPostGet = "GET" 161 | log.debug e 162 | log.debug "You must not of set the preference for the CameraPOSTGET option" 163 | } 164 | log.debug "The method is $method" 165 | try { 166 | def hubAction = new physicalgraph.device.HubAction( 167 | method: method, 168 | path: path, 169 | headers: headers 170 | ) 171 | hubAction.options = [outputMsgToS3:true] 172 | log.debug hubAction 173 | hubAction 174 | } 175 | catch (Exception e) { 176 | log.debug "Hit Exception $e on $hubAction" 177 | } 178 | } 179 | 180 | def parse(String description) { 181 | log.debug "Parsing '${description}'" 182 | def map = [:] 183 | def retResult = [] 184 | def descMap = parseDescriptionAsMap(description) 185 | def imageKey = descMap["tempImageKey"] ? descMap["tempImageKey"] : descMap["key"] 186 | if (imageKey) { 187 | try { 188 | storeTemporaryImage(imageKey, getPictureName()) 189 | } 190 | catch (Exception e) { 191 | log.error e 192 | } 193 | } 194 | else if (descMap["headers"] && descMap["body"]){ 195 | def body = new String(descMap["body"].decodeBase64()) 196 | log.debug "Body: ${body}" 197 | } 198 | else { 199 | log.debug "PARSE FAILED FOR: '${description}'" 200 | } 201 | } 202 | 203 | def parseDescriptionAsMap(description) { 204 | description.split(",").inject([:]) { map, param -> 205 | def nameAndValue = param.split(":") 206 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 207 | } 208 | } 209 | 210 | private getPictureName() { 211 | def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '') 212 | log.debug pictureUuid 213 | def picName = device.deviceNetworkId.replaceAll(':', '') + "_$pictureUuid" + ".jpg" 214 | return picName 215 | } 216 | 217 | private String convertIPtoHex(ipAddress) { 218 | String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join() 219 | log.debug "IP address entered is $ipAddress and the converted hex code is $hex" 220 | return hex 221 | } 222 | 223 | private String convertPortToHex(port) { 224 | String hexport = port.toString().format( '%04x', port.toInteger() ) 225 | log.debug hexport 226 | return hexport 227 | } 228 | 229 | private Integer convertHexToInt(hex) { 230 | Integer.parseInt(hex,16) 231 | } 232 | 233 | 234 | private String convertHexToIP(hex) { 235 | log.debug("Convert hex to ip: $hex") 236 | [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") 237 | } 238 | 239 | private getHostAddress() { 240 | def parts = device.deviceNetworkId.split(":") 241 | log.debug device.deviceNetworkId 242 | def ip = convertHexToIP(parts[0]) 243 | def port = convertHexToInt(parts[1]) 244 | return ip + ":" + port 245 | } 246 | -------------------------------------------------------------------------------- /Devices/Panasonic PTZ Camera/PanasonicPTZVideo.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Panasonic Camera 3 | * 4 | * Source code can be found here: https://github.com/JZ-SmartThings/SmartThings/blob/master/Devices/Panasonic%20PTZ%20Camera/PanasonicPTZCamera.groovy 5 | * 6 | * Copyright 2016 JZ 7 | * 8 | * Tested with Panasonic BL-C30A 9 | * Thanks to: patrickstuart & blebson 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 12 | * in compliance with the License. You may obtain a copy of the License at: 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 17 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 18 | * for the specific language governing permissions and limitations under the License. 19 | * 20 | */ 21 | 22 | metadata { 23 | definition (name: "Panasonic Camera With Video", author: "JZ", namespace: "JZ") { 24 | capability "Image Capture" 25 | capability "Sensor" 26 | capability "Actuator" 27 | 28 | attribute "hubactionMode", "string" 29 | 30 | command "left" 31 | command "right" 32 | command "up" 33 | command "down" 34 | command "home" 35 | command "preset1" 36 | command "preset2" 37 | command "preset3" 38 | command "start" 39 | command "stop" 40 | command "vidOn" 41 | command "vidOff" 42 | } 43 | 44 | preferences { 45 | input("CameraIP", "string", title:"Camera IP Address", description: "Please enter your camera's IP Address", required: true, displayDuringSetup: true) 46 | input("CameraPort", "string", title:"Camera Port", description: "Re-type camera port, usually 80", defaultValue: 80 , required: true, displayDuringSetup: true) 47 | input("CameraAuth", "bool", title:"Does Camera require User Auth?", description: "Choose if the camera requires basic authentication", defaultValue: true, displayDuringSetup: true) 48 | input("CameraPostGet", "string", title:"Does Camera use a Post or Get, normally Get?", description: "Re-type GET", defaultValue: "GET", displayDuringSetup: true) 49 | input("CameraUser", "string", title:"Camera User", description: "Please enter your camera's username", required: false, displayDuringSetup: true) 50 | input("CameraPassword", "string", title:"Camera Password", description: "Please enter your camera's password", required: false, displayDuringSetup: true) 51 | } 52 | 53 | mappings { path("/getInHomeURL") { action: [GET: "getInHomeURL"]} } 54 | 55 | simulator { 56 | } 57 | 58 | tiles (scale: 2){ 59 | multiAttributeTile(name: "videoPlayer", type: "videoPlayer", width: 6, height: 4) { 60 | tileAttribute("device.switch2", key: "CAMERA_STATUS") { 61 | attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", action: "vidOff", backgroundColor: "#79b821", defaultState: true) 62 | attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", action: "vidOn", backgroundColor: "#ffffff") 63 | attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0") 64 | attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", action: "refresh.refresh", backgroundColor: "#F22000") 65 | } 66 | 67 | tileAttribute("device.camera", key: "PRIMARY_CONTROL") { 68 | attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", backgroundColor: "#79b821", defaultState: true) 69 | attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", backgroundColor: "#ffffff") 70 | attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0") 71 | attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", backgroundColor: "#F22000") 72 | } 73 | 74 | tileAttribute("device.startLive", key: "START_LIVE") { 75 | attributeState("live", action: "start", defaultState: true) 76 | } 77 | 78 | tileAttribute("device.stream", key: "STREAM_URL") { 79 | attributeState("activeURL", defaultState: true) 80 | } 81 | tileAttribute("device.betaLogo", key: "BETA_LOGO") { 82 | attributeState("betaLogo", label: "", value: "", defaultState: true) 83 | } 84 | } 85 | standardTile("take", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { 86 | state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking" 87 | state "taking", label:'Taking', action: "", icon: "st.camera.take-photo", backgroundColor: "#53a7c0" 88 | state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking" 89 | } 90 | standardTile("home", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 91 | state "home", label: "Home", action: "home", icon: "st.Home.home2" 92 | } 93 | standardTile("preset1", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 94 | state "preset1", label: "Preset 1", action: "preset1", icon: "st.camera.dlink-hdpan" 95 | } 96 | carouselTile("cameraDetails", "device.image", width: 3, height: 2) { } 97 | 98 | standardTile("blank", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 99 | state "blank", label: "", action: "", icon: "", backgroundColor: "#FFFFFF" 100 | } 101 | standardTile("up", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 102 | state "up", label: "", action: "up", icon: "st.samsung.da.oven_ic_up" 103 | } 104 | standardTile("preset2", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 105 | state "preset2", label: "Preset 2", action: "preset2", icon: "st.camera.dlink-hdpan" 106 | } 107 | standardTile("preset3", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 108 | state "preset3", label: "Preset 3", action: "preset3", icon: "st.camera.dlink-hdpan" 109 | } 110 | standardTile("left", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 111 | state "left", label: "", action: "left", icon: "st.samsung.da.RAC_4line_01_ic_left" 112 | } 113 | standardTile("down", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 114 | state "down", label: "", action: "down", icon: "st.samsung.da.oven_ic_down" 115 | } 116 | standardTile("right", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 117 | state "right", label: "", action: "right", icon: "st.samsung.da.RAC_4line_03_ic_right" 118 | } 119 | main "take" 120 | details([ "videoPlayer", "take", "home", "preset1", "cameraDetails", "preset2", "up", "preset3", "left", "down", "right"]) 121 | } 122 | } 123 | 124 | 125 | 126 | def start() { 127 | log.trace "start()" 128 | def dataLiveVideo = [ 129 | OutHomeURL : "http://${CameraUser}:${CameraPassword}@${CameraIP}:${CameraPort}/nphMotionJpeg?Resolution=320x240&Quality=Standard", 130 | InHomeURL : "http://${CameraUser}:${CameraPassword}@${CameraIP}:${CameraPort}/nphMotionJpeg?Resolution=320x240&Quality=Standard", 131 | ThumbnailURL: "http://cdn.device-icons.smartthings.com/camera/dlink-indoor@2x.png", 132 | cookie : [key: "key", value: "value"] 133 | ] 134 | 135 | def event = [ 136 | name : "stream", 137 | value : groovy.json.JsonOutput.toJson(dataLiveVideo).toString(), 138 | data : groovy.json.JsonOutput.toJson(dataLiveVideo), 139 | descriptionText: "Starting the livestream", 140 | eventType : "VIDEO", 141 | displayed : false, 142 | isStateChange : true 143 | ] 144 | sendEvent(event) 145 | } 146 | 147 | def stop() { 148 | log.trace "stop()" 149 | } 150 | def vidOn() { 151 | log.trace "on()" 152 | // no-op 153 | } 154 | def vidOff() { 155 | log.trace "off()" 156 | // no-op 157 | } 158 | def getInHomeURL() { 159 | [InHomeURL: "http://${CameraUser}:${CameraPassword}@${CameraIP}:${CameraPort}/nphMotionJpeg?Resolution=320x240&Quality=Standard"] 160 | } 161 | 162 | def take() { 163 | log.debug "Taking picture" 164 | cameraCmd("/SnapshotJPEG?Resolution=640x480") 165 | } 166 | def home() { 167 | log.debug "Moving to Home position" 168 | cameraCmd("/nphControlCamera?Direction=HomePosition") 169 | } 170 | def preset1() { 171 | log.debug "Moving to Preset 1 position" 172 | cameraCmd("nphControlCamera?Direction=Preset&PresetOperation=Move&Data=1") 173 | } 174 | def preset2() { 175 | log.debug "Moving to Preset 2 position" 176 | cameraCmd("nphControlCamera?Direction=Preset&PresetOperation=Move&Data=2") 177 | } 178 | def preset3() { 179 | log.debug "Moving to Preset 3 position" 180 | cameraCmd("nphControlCamera?Direction=Preset&PresetOperation=Move&Data=3") 181 | } 182 | def up() { 183 | log.debug "Tilt Up" 184 | cameraCmd("/nphControlCamera?Direction=TiltUp") 185 | } 186 | def left() { 187 | log.debug "Pan Left" 188 | cameraCmd("/nphControlCamera?Direction=PanLeft") 189 | } 190 | def right() { 191 | log.debug "Pan Right" 192 | cameraCmd("/nphControlCamera?Direction=PanRight") 193 | } 194 | def down() { 195 | log.debug "Tilt Down" 196 | cameraCmd("/nphControlCamera?Direction=TiltDown") 197 | } 198 | 199 | 200 | def cameraCmd(String varCommand) { 201 | def userpassascii = "${CameraUser}:${CameraPassword}" 202 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString() 203 | def host = CameraIP 204 | def hosthex = convertIPtoHex(host).toUpperCase() 205 | def porthex = convertPortToHex(CameraPort).toUpperCase() 206 | device.deviceNetworkId = "$hosthex:$porthex" 207 | 208 | log.debug "The device id configured is: $device.deviceNetworkId" 209 | 210 | def path = varCommand 211 | log.debug "path is: $path" 212 | log.debug "Requires Auth: $CameraAuth" 213 | log.debug "Uses which method: $CameraPostGet" 214 | 215 | def headers = [:] 216 | headers.put("HOST", "$host:$CameraPort") 217 | if (CameraAuth) { 218 | headers.put("Authorization", userpass) 219 | } 220 | 221 | log.debug "The Header is $headers" 222 | 223 | def method = "GET" 224 | try { 225 | if (CameraPostGet.toUpperCase() == "POST") { 226 | method = "POST" 227 | } 228 | } 229 | catch (Exception e) { 230 | settings.CameraPostGet = "GET" 231 | log.debug e 232 | log.debug "You must not of set the preference for the CameraPOSTGET option" 233 | } 234 | log.debug "The method is $method" 235 | try { 236 | def hubAction = new physicalgraph.device.HubAction( 237 | method: method, 238 | path: path, 239 | headers: headers 240 | ) 241 | hubAction.options = [outputMsgToS3:true] 242 | log.debug hubAction 243 | hubAction 244 | } 245 | catch (Exception e) { 246 | log.debug "Hit Exception $e on $hubAction" 247 | } 248 | } 249 | 250 | def parse(String description) { 251 | log.debug "Parsing '${description}'" 252 | def map = [:] 253 | def retResult = [] 254 | def descMap = parseDescriptionAsMap(description) 255 | def imageKey = descMap["tempImageKey"] ? descMap["tempImageKey"] : descMap["key"] 256 | if (imageKey) { 257 | try { 258 | storeTemporaryImage(imageKey, getPictureName()) 259 | } 260 | catch (Exception e) { 261 | log.error e 262 | } 263 | } 264 | else if (descMap["headers"] && descMap["body"]){ 265 | def body = new String(descMap["body"].decodeBase64()) 266 | log.debug "Body: ${body}" 267 | } 268 | else { 269 | log.debug "PARSE FAILED FOR: '${description}'" 270 | } 271 | } 272 | 273 | def parseDescriptionAsMap(description) { 274 | description.split(",").inject([:]) { map, param -> 275 | def nameAndValue = param.split(":") 276 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 277 | } 278 | } 279 | 280 | private getPictureName() { 281 | def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '') 282 | log.debug pictureUuid 283 | def picName = device.deviceNetworkId.replaceAll(':', '') + "_$pictureUuid" + ".jpg" 284 | return picName 285 | } 286 | 287 | private String convertIPtoHex(ipAddress) { 288 | String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join() 289 | log.debug "IP address entered is $ipAddress and the converted hex code is $hex" 290 | return hex 291 | } 292 | 293 | private String convertPortToHex(port) { 294 | String hexport = port.toString().format( '%04x', port.toInteger() ) 295 | log.debug hexport 296 | return hexport 297 | } 298 | 299 | private Integer convertHexToInt(hex) { 300 | Integer.parseInt(hex,16) 301 | } 302 | 303 | 304 | private String convertHexToIP(hex) { 305 | log.debug("Convert hex to ip: $hex") 306 | [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") 307 | } 308 | 309 | private getHostAddress() { 310 | def parts = device.deviceNetworkId.split(":") 311 | log.debug device.deviceNetworkId 312 | def ip = convertHexToIP(parts[0]) 313 | def port = convertHexToInt(parts[1]) 314 | return ip + ":" + port 315 | } 316 | -------------------------------------------------------------------------------- /Devices/Panasonic PTZ Camera/README.md: -------------------------------------------------------------------------------- 1 | # Panasonic PTZ Camera 2 | 3 | Based on a few projects in the ST Community, I wrote a camera device handler that can control a Panasonic PTZ Camera. So you can move it in the four directions and also call 3 presets defined on the camera. This was successfully tested with these models: BL-C30A, BB-HCM511A, BB-HCM531A 4 | 5 | Thanks to: patrickstuart & blebson 6 | 7 | Link to the project: https://community.smartthings.com/t/panasonic-ptz-ip-camera/43336 -------------------------------------------------------------------------------- /Devices/SmartGPIO/README.md: -------------------------------------------------------------------------------- 1 | # SmartGPIO 2 | 3 | This project consists of a Raspberry Pi running Raspbian OS. It runs HTTPD with index.php as the source. The PHP has many functions but this device driver is specifically to see the GPIO status of all pins in a picture format. 4 | 5 | Link to the project: https://community.smartthings.com/t/44803 6 | 7 | Grab index.php file from here: https://github.com/JZ-SmartThings/SmartThings/tree/master/Devices/Generic%20HTTP%20Device 8 | 9 | The Groovy file is the Device Handler for SmartThings. 10 | index.php is meant to reside in /var/www/html folder of the Raspbery Pi and runs the a command in Linux and returns a JPEG of that output to SmartThings. 11 | At the top of index.php, change the first variable to "true" instead of "false" and this will make the PHP page protected with basic authentication. After making that change, make sure to change the SmartThings preferences for the device. 12 | 13 | Use the TEST button first to make sure that you install a required graphic component for PHP called php5-gd. Once my app senses that this component exists, it will populate the image tile. Otherwise the TAKE button will not work and not return an image as expected. 14 | 15 | Here's a useful command for checking the Raspberry Pi pin status & value. It's pretty close to what WebIOPi offers but in text mode and refreshes every half second: watch -n0.5 gpio readall 16 | 17 |
v1.0.20160410 - Initial version 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /Devices/SmartGPIO/Screenshot_Android_App.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/SmartGPIO/Screenshot_Android_App.png -------------------------------------------------------------------------------- /Devices/SmartGPIO/Screenshot_PHP_Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/SmartGPIO/Screenshot_PHP_Page.png -------------------------------------------------------------------------------- /Devices/SmartGPIO/SmartGPIO.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * SmartGPIO v1.0.20170130 3 | * 4 | * Source code can be found here: https://github.com/JZ-SmartThings/SmartThings/blob/master/Devices/SmartGPIO/SmartGPIO.groovy 5 | * 6 | * Copyright 2016 JZ 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 9 | * in compliance with the License. You may obtain a copy of the License at: 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 14 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 15 | * for the specific language governing permissions and limitations under the License. 16 | * 17 | */ 18 | 19 | import groovy.json.JsonSlurper 20 | 21 | metadata { 22 | definition (name: "SmartGPIO", author: "JZ", namespace:"JZ") { 23 | capability "Switch" 24 | capability "Polling" 25 | capability "Refresh" 26 | capability "Image Capture" 27 | attribute "lastTriggered", "string" 28 | attribute "testTriggered", "string" 29 | command "DeviceTrigger" 30 | command "TestTrigger" 31 | command "install" 32 | } 33 | 34 | preferences { 35 | input("DeviceIP", "string", title:"Device IP Address", description: "Please enter your device's IP Address", required: true, displayDuringSetup: true) 36 | input("DevicePort", "string", title:"Device Port", description: "Empty assumes port 80.", required: false, displayDuringSetup: true) 37 | input("DevicePath", "string", title:"URL Path", description: "Rest of the URL, include forward slash.", displayDuringSetup: true) 38 | input(name: "DevicePostGet", type: "enum", title: "POST or GET", options: ["POST","GET"], defaultValue: "POST", required: false, displayDuringSetup: true) 39 | input("UseJSON", "bool", title:"Use JSON instead of HTML?", description: "Use JSON instead of HTML?", defaultValue: false, required: false, displayDuringSetup: true) 40 | section() { 41 | input("HTTPAuth", "bool", title:"Requires User Auth?", description: "Choose if the HTTP requires basic authentication", defaultValue: false, required: true, displayDuringSetup: true) 42 | input("HTTPUser", "string", title:"HTTP User", description: "Enter your basic username", required: false, displayDuringSetup: true) 43 | input("HTTPPassword", "string", title:"HTTP Password", description: "Enter your basic password", required: false, displayDuringSetup: true) 44 | } 45 | } 46 | 47 | simulator { 48 | } 49 | 50 | tiles(scale: 2) { 51 | valueTile("testTriggered", "device.testTriggered", width: 5, height: 1, decoration: "flat") { 52 | state("default", label: 'Test triggered: ${currentValue}', backgroundColor:"#ffffff") 53 | } 54 | standardTile("TestTrigger", "device.testswitch", width: 1, height: 1, decoration: "flat") { 55 | state "default", label:'TEST', action: "poll", icon: "st.Office.office13", backgroundColor:"#53a7c0", nextState: "testrunning" 56 | state "testrunning", label: 'TESTING', action: "ResetTiles", icon: "st.Office.office13", backgroundColor: "#FF6600" 57 | state "getgd", label: 'INSTALL', action: "install", icon: "st.Office.office13", backgroundColor: "#FF0000", nextState: "wait" 58 | state "wait", label: 'WAIT', action: "ResetTiles", icon: "st.Office.office13", backgroundColor: "#FF0000" 59 | } 60 | valueTile("lastTriggered", "device.lastTriggered", width: 5, height: 1, decoration: "flat") { 61 | state("default", label: 'Taken on:\n${currentValue}', backgroundColor:"#ffffff") 62 | } 63 | standardTile("take", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { 64 | state "take", label: "Take", action: "on", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking" 65 | state "taking", label:'Taking', action: "", icon: "st.camera.take-photo", backgroundColor: "#53a7c0" 66 | state "image", label: "Take", action: "Image Capture.on", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking" 67 | } 68 | carouselTile("cameraDetails", "device.image", width: 6, height: 3) { } 69 | main "take" 70 | details(["testTriggered", "TestTrigger", "lastTriggered", "take", "cameraDetails"]) 71 | } 72 | } 73 | def refresh() { 74 | poll() 75 | } 76 | def poll() { 77 | off() 78 | } 79 | def on() { 80 | def LocalDeviceBodyText = '' 81 | if (DeviceBodyText==null) { LocalDeviceBodyText = "GPIO=" } else { LocalDeviceBodyText = DeviceBodyText } 82 | 83 | if (UseJSON==true) { 84 | log.debug "GPIO=UseJSON= Triggered!!!" 85 | runCmd("GPIO=&UseJSON=") 86 | } else { 87 | log.debug "GPIO= Triggered!!!" 88 | runCmd("GPIO=") 89 | } 90 | } 91 | def off() { 92 | if (UseJSON==true) { 93 | log.debug "Test Triggered!!!" 94 | runCmd('Test=&UseJSON=', false) 95 | } else { 96 | log.debug "Test JSON Triggered!!!" 97 | runCmd('Test=', false) 98 | } 99 | } 100 | def install() { 101 | log.debug "INSTALL FUNCTION" 102 | sendEvent(name: "testTriggered", value: "TRY IN 3 MINUTES", unit: "", isStateChange: true) 103 | runCmd('GDInstall=', false) 104 | } 105 | def ResetTiles() { 106 | //RETURN BUTTONS TO CORRECT STATE 107 | sendEvent(name: "testswitch", value: "default", isStateChange: true) 108 | log.debug "Resetting tiles." 109 | } 110 | def runCmd(String varCommand, def useS3=true) { 111 | def host = DeviceIP 112 | def hosthex = convertIPtoHex(host).toUpperCase() 113 | def LocalDevicePort = '' 114 | if (DevicePort==null) { LocalDevicePort = "80" } else { LocalDevicePort = DevicePort } 115 | def porthex = convertPortToHex(LocalDevicePort).toUpperCase() 116 | device.deviceNetworkId = "$hosthex:$porthex" 117 | def userpassascii = "${HTTPUser}:${HTTPPassword}" 118 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString() 119 | 120 | log.debug "The device id configured is: $device.deviceNetworkId" 121 | 122 | def path = DevicePath 123 | log.debug "path is: $path" 124 | log.debug "Uses which method: $DevicePostGet" 125 | def body = varCommand 126 | log.debug "body is: $body" 127 | 128 | def headers = [:] 129 | headers.put("HOST", "$host:$LocalDevicePort") 130 | headers.put("Content-Type", "application/x-www-form-urlencoded") 131 | if (HTTPAuth) { 132 | headers.put("Authorization", userpass) 133 | } 134 | log.debug "The Header is $headers" 135 | def method = "POST" 136 | try { 137 | if (DevicePostGet.toUpperCase() == "GET") { 138 | method = "GET" 139 | } 140 | } 141 | catch (Exception e) { 142 | settings.DevicePostGet = "POST" 143 | log.debug e 144 | log.debug "You must not have set the preference for the DevicePOSTGET option" 145 | } 146 | log.debug "The method is $method" 147 | try { 148 | def hubAction = new physicalgraph.device.HubAction( 149 | method: method, 150 | path: path, 151 | body: body, 152 | headers: headers 153 | ) 154 | hubAction.options = [outputMsgToS3:useS3] 155 | log.debug "useS3===" + useS3 156 | //log.debug hubAction 157 | if (useS3==false) { 158 | sendEvent(name: "testTriggered", value: "", unit: "", isStateChange: true) 159 | if (body == "GDInstall=") { 160 | log.debug "STARTING GD INSTALL" 161 | sendEvent(name: "testTriggered", value: "TRY IN 5 MINUTES", unit: "", isStateChange: true) 162 | } 163 | } 164 | hubAction 165 | } 166 | catch (Exception e) { 167 | log.debug "Hit Exception $e on $hubAction" 168 | } 169 | } 170 | 171 | def parse(String description) { 172 | log.debug "Parsing '${description}'" 173 | def whichTile = '' 174 | def map = [:] 175 | def retResult = [] 176 | def descMap = parseDescriptionAsMap(description) 177 | def jsonlist = [:] 178 | def bodyReturned = ' ' 179 | def headersReturned = ' ' 180 | if (descMap["body"] && descMap["headers"]) { 181 | bodyReturned = new String(descMap["body"].decodeBase64()) 182 | headersReturned = new String(descMap["headers"].decodeBase64()) 183 | } 184 | log.debug "BODY---" + bodyReturned 185 | //log.debug "HEADERS---" + headersReturned 186 | 187 | def LocalDeviceBodyText = '' 188 | if (DeviceBodyText==null) { LocalDeviceBodyText = "GPIO=" } else { LocalDeviceBodyText = DeviceBodyText } 189 | 190 | def imageKey = descMap["tempImageKey"] ? descMap["tempImageKey"] : descMap["key"] 191 | if (imageKey) { 192 | try { 193 | storeTemporaryImage(imageKey, getPictureName()) 194 | def imageDate = new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone) 195 | sendEvent(name: "lastTriggered", value: imageDate, unit: "", isStateChange: true) 196 | } 197 | catch (Exception e) { 198 | log.error e 199 | } 200 | } else if (descMap["body"]) { 201 | if (headersReturned.contains("application/json")) { 202 | def body = new String(descMap["body"].decodeBase64()) 203 | def slurper = new JsonSlurper() 204 | jsonlist = slurper.parseText(body) 205 | jsonlist.put ("Date", new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone)) 206 | log.debug "JSON parsing..." 207 | } else if (headersReturned.contains("text/html")) { 208 | jsonlist.put ("Date", new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone)) 209 | def data=bodyReturned.eachLine { line -> 210 | //if (line.contains(LocalDeviceBodyText + 'Success')) { jsonlist.put (LocalDeviceBodyText.replace("=",""), "Success") } 211 | //if (line.contains(LocalDeviceBodyText + 'Failed : Authentication Required!')) { jsonlist.put (LocalDeviceBodyText.replace("=",""), "Authentication Required!") } 212 | if (line.contains('Test=Success')) { jsonlist.put ("Test", "Success") } 213 | if (line.contains('Test=Failed : Authentication Required!')) { jsonlist.put ("Test", "Authentication Required!") } 214 | } 215 | log.debug "HTML parsing..." 216 | } 217 | } 218 | 219 | //RESET THE DEVICE ID TO GENERIC/RANDOM NUMBER. THIS ALLOWS MULTIPLE DEVICES TO USE THE SAME ID/IP 220 | device.deviceNetworkId = "ID_WILL_BE_CHANGED_AT_RUNTIME_" + (Math.abs(new Random().nextInt()) % 99999 + 1) 221 | 222 | if (descMap["body"] && (headersReturned.contains("application/json") || headersReturned.contains("text/html"))) { 223 | if (jsonlist."php5-gd"==false) { 224 | sendEvent(name: "testTriggered", value: jsonlist."Date" + "\nphp5-gd Not Installed", unit: "", isStateChange: true) 225 | sendEvent(name: "testswitch", value: "getgd", unit: "", isStateChange: true) 226 | } else if (jsonlist."php5-gd"==true) { 227 | sendEvent(name: "testTriggered", value: jsonlist."Date" + "\nphp5-gd Installed", unit: "", isStateChange: true) 228 | sendEvent(name: "testswitch", value: "default", isStateChange: true) 229 | def result = createEvent(name: "testswitch", value: "default", isStateChange: true) 230 | return result 231 | } 232 | if (jsonlist."Test"=="Authentication Required!") { 233 | sendEvent(name: "testTriggered", value: "\nUse Authentication Credentials", unit: "") 234 | def result = createEvent(name: "testswitch", value: "default", isStateChange: true) 235 | return result 236 | } 237 | if (jsonlist."GPIO"=="Authentication Required!") { 238 | //sendEvent(name: "lastTriggered", value: "\nUse Authentication Credentials", unit: "") 239 | } 240 | } 241 | 242 | log.debug jsonlist 243 | } 244 | 245 | private getPictureName() { 246 | def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '') 247 | log.debug pictureUuid 248 | def picName = device.deviceNetworkId.replaceAll(':', '') + "_$pictureUuid" + ".jpg" 249 | return picName 250 | } 251 | 252 | def parseDescriptionAsMap(description) { 253 | description.split(",").inject([:]) { map, param -> 254 | def nameAndValue = param.split(":") 255 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 256 | } 257 | } 258 | private String convertIPtoHex(ipAddress) { 259 | String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join() 260 | //log.debug "IP address entered is $ipAddress and the converted hex code is $hex" 261 | return hex 262 | } 263 | private String convertPortToHex(port) { 264 | String hexport = port.toString().format( '%04x', port.toInteger() ) 265 | //log.debug hexport 266 | return hexport 267 | } 268 | private Integer convertHexToInt(hex) { 269 | Integer.parseInt(hex,16) 270 | } 271 | private String convertHexToIP(hex) { 272 | //log.debug("Convert hex to ip: $hex") 273 | [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") 274 | } 275 | private getHostAddress() { 276 | def parts = device.deviceNetworkId.split(":") 277 | //log.debug device.deviceNetworkId 278 | def ip = convertHexToIP(parts[0]) 279 | def port = convertHexToInt(parts[1]) 280 | return ip + ":" + port 281 | } 282 | -------------------------------------------------------------------------------- /Devices/TVDevice/3.3V PICTURES & WIRING/2016-06-11 12.56.41.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/3.3V PICTURES & WIRING/2016-06-11 12.56.41.jpg -------------------------------------------------------------------------------- /Devices/TVDevice/3.3V PICTURES & WIRING/2016-06-11 12.57.01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/3.3V PICTURES & WIRING/2016-06-11 12.57.01.jpg -------------------------------------------------------------------------------- /Devices/TVDevice/3.3V PICTURES & WIRING/IRSender.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/3.3V PICTURES & WIRING/IRSender.fzz -------------------------------------------------------------------------------- /Devices/TVDevice/3.3V PICTURES & WIRING/IRSender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/3.3V PICTURES & WIRING/IRSender.png -------------------------------------------------------------------------------- /Devices/TVDevice/3.3V PICTURES & WIRING/IRSenderSchematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/3.3V PICTURES & WIRING/IRSenderSchematic.png -------------------------------------------------------------------------------- /Devices/TVDevice/3.3V PICTURES & WIRING/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/3.3V PICTURES & WIRING/Thumbs.db -------------------------------------------------------------------------------- /Devices/TVDevice/5V PICTURES & WIRING/2016-06-11 12.30.57.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/5V PICTURES & WIRING/2016-06-11 12.30.57.jpg -------------------------------------------------------------------------------- /Devices/TVDevice/5V PICTURES & WIRING/2016-06-11 12.31.16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/5V PICTURES & WIRING/2016-06-11 12.31.16.jpg -------------------------------------------------------------------------------- /Devices/TVDevice/5V PICTURES & WIRING/2016-06-11 12.31.32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/5V PICTURES & WIRING/2016-06-11 12.31.32.jpg -------------------------------------------------------------------------------- /Devices/TVDevice/5V PICTURES & WIRING/IRSender5V.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/5V PICTURES & WIRING/IRSender5V.fzz -------------------------------------------------------------------------------- /Devices/TVDevice/5V PICTURES & WIRING/IRSender5V.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/5V PICTURES & WIRING/IRSender5V.png -------------------------------------------------------------------------------- /Devices/TVDevice/5V PICTURES & WIRING/IRSender5VSchematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Devices/TVDevice/5V PICTURES & WIRING/IRSender5VSchematic.png -------------------------------------------------------------------------------- /Devices/TVDevice/IRSender.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * TV Device Sample v1.0.20170603 3 | * Source code can be found here: https://github.com/JZ-SmartThings/SmartThings/blob/master/Devices/TVDevice 4 | * Copyright 2016 JZ 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | */ 13 | 14 | // SELECT IR PIN 15 | const int IRpin = D2; 16 | 17 | //true SENDS GROUND, false SENDS VCC SIGNAL 18 | const bool sendGround = false; 19 | 20 | #include 21 | IRsend irsend(D2, sendGround); 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | const char* ssid = "WIFI_SSID"; 31 | const char* password = "WIFI_PASSWORD"; 32 | 33 | MDNSResponder mdns; 34 | ESP8266WebServer server(80); 35 | 36 | // OTA AUTODETECT 37 | WiFiServer TelnetServer(8266); 38 | 39 | // OTHER VARIABLES 40 | String currentIP; 41 | 42 | void setup(void){ 43 | //TURN OFF BUILTIN LEDS 44 | pinMode(LED_BUILTIN, OUTPUT); //GPIO16 also D0 also LED_BUILTIN 45 | pinMode(D4, OUTPUT); //GPIO2 also D4 46 | digitalWrite(LED_BUILTIN, HIGH); 47 | digitalWrite(D4, HIGH); 48 | 49 | irsend.begin(); 50 | //OTA 51 | TelnetServer.begin(); 52 | 53 | Serial.begin(115200); 54 | WiFi.mode(WIFI_STA); 55 | WiFi.begin(ssid, password); 56 | Serial.println(""); 57 | 58 | // Wait for connection 59 | while (WiFi.status() != WL_CONNECTED) { 60 | delay(500); 61 | Serial.print("."); 62 | } 63 | Serial.println(""); 64 | Serial.print("Connected to "); 65 | Serial.println(ssid); 66 | Serial.print("IP address: "); 67 | Serial.println(WiFi.localIP()); 68 | currentIP = WiFi.localIP().toString(); 69 | 70 | if (mdns.begin("esp8266", WiFi.localIP())) { 71 | Serial.println("MDNS responder started"); 72 | } 73 | 74 | // OTA 75 | // Port defaults to 8266 76 | //ArduinoOTA.setPort(8266); 77 | // Hostname defaults to esp8266-[ChipID] 78 | //ArduinoOTA.setHostname("myesp8266"); 79 | // No authentication by default 80 | //ArduinoOTA.setPassword((const char *)"xxxxx"); 81 | 82 | ArduinoOTA.onStart([]() { 83 | Serial.println("OTA Start"); 84 | }); 85 | ArduinoOTA.onEnd([]() { 86 | Serial.println("OTA End"); 87 | Serial.println("Rebooting..."); 88 | }); 89 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 90 | Serial.printf("Progress: %u%%\r\n", (progress / (total / 100))); 91 | }); 92 | ArduinoOTA.onError([](ota_error_t error) { 93 | Serial.printf("Error[%u]: ", error); 94 | if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); 95 | else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); 96 | else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); 97 | else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); 98 | else if (error == OTA_END_ERROR) Serial.println("End Failed"); 99 | }); 100 | ArduinoOTA.begin(); 101 | 102 | server.on("/", handleRoot); 103 | server.on("/ir", handleIr); 104 | 105 | server.on("/inline", [](){ 106 | server.send(200, "text/plain", "this works as well"); 107 | }); 108 | 109 | server.onNotFound(handleNotFound); 110 | 111 | server.begin(); 112 | Serial.println("HTTP server started"); 113 | } 114 | 115 | void loop(void){ 116 | ArduinoOTA.handle(); 117 | server.handleClient(); 118 | } 119 | 120 | 121 | void handleRoot() { 122 | String htmlContent = "ESP8266 IR Remote

ESP8266 IR Remote

"; 123 | htmlContent += currentIP; 124 | htmlContent += "


\n"; 125 | for (uint8_t i=0; iTV ON

TV OFF

"; 132 | htmlContent += "

TV CHANNEL UP

TV CHANNEL DOWN

PREVIOUS

"; 133 | htmlContent += "

TV VOLUME UP

TV VOLUME DOWN

TV MUTE

TV INPUT

"; 134 | htmlContent += "

HDMI 1

HDMI 2

"; 135 | htmlContent += "

HDMI 3

HDMI 4

"; 136 | server.send(200, "text/html", htmlContent); 137 | } 138 | 139 | void handleIr(){ 140 | for (uint8_t i=0; iv1.0.20170603 - Compliant with new IRremoteESP8266 library. 10 |
v1.0.20170529 - New volume up/down buttons on the main TVDevice. Now all buttons will reset as expected after pressing. 11 |
v1.0.20160806 - Security issue with the ESP8266 library, read this post: https://community.smartthings.com/t/esp8266-nodemcu-arduino-based-tv-remote/50161/22 12 |
v1.0.20160721 - Updated pics/wiring to use D2 13 |
v1.0.20160611 - Initial version 14 |
15 | -------------------------------------------------------------------------------- /Devices/TVDevice/TVDevice.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * TVDevice v1.0.20180502 3 | * 4 | * Source code can be found here: https://github.com/JZ-SmartThings/SmartThings/blob/master/Devices/TVDevice/TVDevice.groovy 5 | * 6 | * Copyright 2018 JZ 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 9 | * in compliance with the License. You may obtain a copy of the License at: 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 14 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 15 | * for the specific language governing permissions and limitations under the License. 16 | * 17 | */ 18 | 19 | import groovy.json.JsonSlurper 20 | 21 | metadata { 22 | definition (name: "TVDevice", author: "JZ", namespace:"JZ") { 23 | capability "Switch" 24 | capability "Switch Level" 25 | attribute "displayName", "string" 26 | command "tvinput" 27 | command "chanup" 28 | command "chandown" 29 | command "tvprev" 30 | command "volup" 31 | command "voldown" 32 | command "tvmute" 33 | command "ResetTiles" 34 | } 35 | 36 | preferences { 37 | input("DeviceIP", "string", title:"Device IP Address", description: "Please enter your device's IP Address", required: true, displayDuringSetup: true) 38 | input("DevicePort", "string", title:"Device Port", description: "Empty assumes port 80.", required: false, displayDuringSetup: true) 39 | input("DevicePathOn", "string", title:"URL Path for ON", description: "Rest of the URL, include forward slash.", displayDuringSetup: true) 40 | input("DevicePathOff", "string", title:"URL Path for ON", description: "Rest of the URL, include forward slash.", displayDuringSetup: true) 41 | input(name: "DevicePostGet", type: "enum", title: "POST or GET", options: ["POST","GET"], defaultValue: "POST", required: false, displayDuringSetup: true) 42 | section() { 43 | input("HTTPAuth", "bool", title:"Requires User Auth?", description: "Choose if the HTTP requires basic authentication", defaultValue: false, required: true, displayDuringSetup: true) 44 | input("HTTPUser", "string", title:"HTTP User", description: "Enter your basic username", required: false, displayDuringSetup: true) 45 | input("HTTPPassword", "string", title:"HTTP Password", description: "Enter your basic password", required: false, displayDuringSetup: true) 46 | } 47 | } 48 | 49 | simulator { 50 | } 51 | 52 | tiles(scale: 2) { 53 | valueTile("displayName", "device.displayName", width: 6, height: 1, decoration: "flat") { 54 | state("default", label: '${currentValue}', backgroundColor:"#DDDDDD") 55 | } 56 | //valueTile("displayName", "device.displayName", width: 6, height: 1, decoration: "flat") { 57 | // state("default", label: '{currentValue}', backgroundColor:"#ffffff") 58 | //} 59 | standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 60 | state "off", label:'OFF' , action: "on", icon: "st.Electronics.electronics18", backgroundColor:"#53a7c0", nextState: "trying" 61 | state "on", label: 'ON', action: "off", icon: "st.Electronics.electronics18", backgroundColor: "#FF6600", nextState: "trying" 62 | state "trying", label: 'TRYING', icon: "st.Electronics.electronics18", backgroundColor: "#FFAA33" 63 | } 64 | standardTile("switchon", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 65 | state "default", label: 'ON', action: "on", icon: "st.Electronics.electronics18", backgroundColor: "#FF6600", nextState: "trying" 66 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.Electronics.electronics18", backgroundColor: "#FFAA33" 67 | } 68 | standardTile("switchoff", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 69 | state "default", label:'OFF' , action: "off", icon: "st.Electronics.electronics18", backgroundColor:"#53a7c0", nextState: "trying" 70 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.Electronics.electronics18", backgroundColor: "#FFAA33" 71 | } 72 | standardTile("tvinput", "device.tvinput", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 73 | state "default", label: 'TV INPUT', action: "tvinput", icon: "st.Electronics.electronics6", backgroundColor: "#79b821", nextState: "trying" 74 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.Electronics.electronics6", backgroundColor: "#FFAA33" 75 | } 76 | standardTile("chanup", "device.chanup", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 77 | state "default", label: 'CHAN UP', action: "chanup", icon: "st.custom.buttons.add-icon", backgroundColor: "#FF6600", nextState: "trying" 78 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.custom.buttons.add-icon", backgroundColor: "#FFAA33" 79 | } 80 | standardTile("chandown", "device.chandown", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 81 | state "default", label:'CHAN DOWN' , action: "chandown", icon: "st.custom.buttons.subtract-icon", backgroundColor:"#53a7c0", nextState: "trying" 82 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.custom.buttons.subtract-icon", backgroundColor: "#FFAA33" 83 | } 84 | standardTile("tvprev", "device.tvprev", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 85 | state "default", label: 'PREVIOUS', action: "tvprev", icon: "st.motion.motion.active", backgroundColor: "#79b821", nextState: "trying" 86 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.motion.motion.active", backgroundColor: "#FFAA33" 87 | } 88 | standardTile("volup", "device.volup", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 89 | state "default", label: 'VOL UP', action: "volup", icon: "st.custom.buttons.add-icon", backgroundColor: "#FF6600", nextState: "trying" 90 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.custom.buttons.add-icon", backgroundColor: "#FFAA33" 91 | } 92 | standardTile("voldown", "device.voldown", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 93 | state "default", label:'VOL DOWN' , action: "voldown", icon: "st.custom.buttons.subtract-icon", backgroundColor:"#53a7c0", nextState: "trying" 94 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.custom.buttons.subtract-icon", backgroundColor: "#FFAA33" 95 | } 96 | standardTile("tvmute", "device.tvmute", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 97 | state "default", label: 'MUTE', action: "tvmute", icon: "st.custom.sonos.muted", backgroundColor: "#9966CC", nextState: "trying" 98 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.custom.sonos.muted", backgroundColor: "#FFAA33" 99 | } 100 | controlTile("levelSliderControl", "device.level", "slider", width: 6, height: 1, inactiveLabel: false, range:"(0..100)") { 101 | state "level", label:'HDMI Input', action:"switch level.setLevel" 102 | } 103 | 104 | main "switch" 105 | details(["displayName","levelSliderControl", "switchon", "switchoff", "tvinput", "chanup", "tvprev", "volup", "chandown", "tvmute", "voldown" ]) 106 | } 107 | } 108 | 109 | def ResetTiles() { 110 | sendEvent(name: "switchon", value: "default", isStateChange: true) 111 | sendEvent(name: "switchoff", value: "default", isStateChange: true) 112 | sendEvent(name: "tvinput", value: "default", isStateChange: true) 113 | sendEvent(name: "chanup", value: "default", isStateChange: true) 114 | sendEvent(name: "chandown", value: "default", isStateChange: true) 115 | sendEvent(name: "tvprev", value: "default", isStateChange: true) 116 | sendEvent(name: "volup", value: "default", isStateChange: true) 117 | sendEvent(name: "voldown", value: "default", isStateChange: true) 118 | sendEvent(name: "tvmute", value: "default", isStateChange: true) 119 | log.debug "Resetting tiles." 120 | } 121 | 122 | 123 | def setLevel(value) { 124 | def level=value as int 125 | def cmd="" as String 126 | log.debug "setLevel >> value: $value" 127 | if (value<10) { 128 | level=0 129 | cmd="/ir?tv=mute" 130 | } else if (value>=10 && value<=14) { 131 | level=10 132 | cmd="/ir?tv=input" 133 | } else if (value>=15 && value<=24) { 134 | level=20 135 | cmd="/ir?hdmi=" + 1 136 | } else if (value>=25 && value<=34) { 137 | level=30 138 | cmd="/ir?hdmi=" + 2 139 | } else if (value>=35 && value<=44) { 140 | level=40 141 | cmd="/ir?hdmi=" + 3 142 | } else if (value>=45 && value<=54) { 143 | level=50 144 | cmd="/ir?hdmi=" + 4 145 | } else if (value>=55 && value<=64) { 146 | level=60 147 | cmd="/ir?hdmi=" + 5 148 | } else if (value>=65 && value<=74) { 149 | level=70 150 | cmd="/ir?hdmi=" + 6 151 | } else if (value>=75 && value<=84) { 152 | level=80 153 | cmd="/ir?hdmi=" + 7 154 | } else if (value>=85 && value<=94) { 155 | level=90 156 | cmd="/ir?hdmi=" + 8 157 | } else if (value>95) { 158 | level=100 159 | } 160 | sendEvent(name: "level", value: level, isStateChange: true) 161 | runCmd(cmd) 162 | } 163 | 164 | def on() { 165 | log.debug "---ON COMMAND---" 166 | runCmd("/ir?tv=on") 167 | } 168 | def off() { 169 | runCmd("/ir?tv=off") 170 | } 171 | def tvinput() { 172 | runCmd("/ir?tv=input") 173 | } 174 | def chanup() { 175 | runCmd("/ir?tv=chanup") 176 | } 177 | def chandown() { 178 | runCmd("/ir?tv=chandown") 179 | } 180 | def tvprev() { 181 | runCmd("/ir?tv=prev") 182 | } 183 | def volup() { 184 | runCmd("/ir?tv=volup") 185 | } 186 | def voldown() { 187 | runCmd("/ir?tv=voldown") 188 | } 189 | def tvmute() { 190 | runCmd("/ir?tv=mute") 191 | } 192 | 193 | def runCmd(String varCommand) { 194 | def host = DeviceIP 195 | def hosthex = convertIPtoHex(host).toUpperCase() 196 | def LocalDevicePort = '' 197 | if (DevicePort==null) { LocalDevicePort = "80" } else { LocalDevicePort = DevicePort } 198 | def porthex = convertPortToHex(LocalDevicePort).toUpperCase() 199 | device.deviceNetworkId = "$hosthex:$porthex" 200 | def userpassascii = "${HTTPUser}:${HTTPPassword}" 201 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString() 202 | 203 | log.debug "The device id configured is: $device.deviceNetworkId" 204 | 205 | def path = varCommand 206 | log.debug "path is: $path" 207 | log.debug "Uses which method: $DevicePostGet" 208 | def body = varCommand 209 | log.debug "body is: $body" 210 | 211 | def headers = [:] 212 | headers.put("HOST", "$host:$LocalDevicePort") 213 | headers.put("Content-Type", "application/x-www-form-urlencoded") 214 | if (HTTPAuth) { 215 | headers.put("Authorization", userpass) 216 | } 217 | log.debug "The Header is $headers" 218 | def method = "POST" 219 | try { 220 | if (DevicePostGet.toUpperCase() == "GET") { 221 | method = "GET" 222 | } 223 | } 224 | catch (Exception e) { 225 | settings.DevicePostGet = "POST" 226 | log.debug e 227 | log.debug "You must not have set the preference for the DevicePOSTGET option" 228 | } 229 | log.debug "The method is $method" 230 | try { 231 | def hubAction = new physicalgraph.device.HubAction( 232 | method: method, 233 | path: path, 234 | body: body, 235 | headers: headers 236 | ) 237 | hubAction.options = [outputMsgToS3:false] 238 | log.debug hubAction 239 | hubAction 240 | } 241 | catch (Exception e) { 242 | log.debug "Hit Exception $e on $hubAction" 243 | } 244 | } 245 | 246 | def parse(String description) { 247 | // log.debug "Parsing '${description}'" 248 | def whichTile = '' 249 | def map = [:] 250 | def retResult = [] 251 | def descMap = parseDescriptionAsMap(description) 252 | def jsonlist = [:] 253 | def bodyReturned = ' ' 254 | def headersReturned = ' ' 255 | if (descMap["body"] && descMap["headers"]) { 256 | bodyReturned = new String(descMap["body"].decodeBase64()) 257 | headersReturned = new String(descMap["headers"].decodeBase64()) 258 | } 259 | // log.debug "BODY---" + bodyReturned 260 | // log.debug "HEADERS---" + headersReturned 261 | 262 | if (descMap["body"]) { 263 | if (headersReturned.contains("application/json")) { 264 | def body = new String(descMap["body"].decodeBase64()) 265 | def slurper = new JsonSlurper() 266 | jsonlist = slurper.parseText(body) 267 | //log.debug "JSONLIST---" + jsonlist."CPU" 268 | jsonlist.put ("Date", new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone)) 269 | } else if (headersReturned.contains("text/html")) { 270 | jsonlist.put ("Date", new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone)) 271 | def data=bodyReturned.eachLine { line -> 272 | 273 | if (line.length()<15 && !line.contains("<") && !line.contains(">") && !line.contains("/")) { 274 | log.trace "---" + line 275 | if (line.contains('tv=')) { jsonlist.put ("tv", line.replace("tv=","")) } 276 | if (line.contains('hdmi=')) { jsonlist.put ("hdmi", line.replace("hdmi=","")) } 277 | } 278 | } 279 | } 280 | } 281 | if (descMap["body"] && (headersReturned.contains("application/json") || headersReturned.contains("text/html"))) { 282 | //putImageInS3(descMap) 283 | if (jsonlist."tv"=="on") { 284 | sendEvent(name: "switchon", value: "default", isStateChange: true) 285 | whichTile = 'mainon' 286 | } 287 | if (jsonlist."tv"=="off") { 288 | sendEvent(name: "switchoff", value: "default", isStateChange: true) 289 | whichTile = 'mainoff' 290 | } 291 | if (jsonlist."tv"=="input") { 292 | sendEvent(name: "tvinput", value: "default", isStateChange: true) 293 | whichTile = 'tvinput' 294 | } 295 | if (jsonlist."tv"=="chanup") { 296 | sendEvent(name: "chanup", value: "default", isStateChange: true) 297 | whichTile = 'chanup' 298 | } 299 | if (jsonlist."tv"=="chandown") { 300 | sendEvent(name: "chandown", value: "default", isStateChange: true) 301 | whichTile = 'chandown' 302 | } 303 | if (jsonlist."tv"=="prev") { 304 | sendEvent(name: "tvprev", value: "default", isStateChange: true) 305 | whichTile = 'tvprev' 306 | } 307 | if (jsonlist."tv"=="volup") { 308 | sendEvent(name: "volup", value: "default", isStateChange: true) 309 | whichTile = 'volup' 310 | } 311 | if (jsonlist."tv"=="voldown") { 312 | sendEvent(name: "voldown", value: "default", isStateChange: true) 313 | whichTile = 'voldown' 314 | } 315 | if (jsonlist."tv"=="mute") { 316 | sendEvent(name: "tvmute", value: "default", isStateChange: true) 317 | whichTile = 'tvmute' 318 | } 319 | } 320 | 321 | log.debug jsonlist 322 | 323 | //RESET THE DEVICE ID TO GENERIC/RANDOM NUMBER. THIS ALLOWS MULTIPLE DEVICES TO USE THE SAME ID/IP 324 | device.deviceNetworkId = "ID_WILL_BE_CHANGED_AT_RUNTIME_" + (Math.abs(new Random().nextInt()) % 99999 + 1) 325 | 326 | //CHANGE NAME TILE 327 | sendEvent(name: "displayName", value: DeviceIP, unit: "") 328 | 329 | //RETURN BUTTONS TO CORRECT STATE 330 | log.debug 'whichTile: ' + whichTile 331 | switch (whichTile) { 332 | case 'mainoff': 333 | //sendEvent(name: "mainswitch", value: "off", isStateChange: true) 334 | //def result = createEvent(name: "mainswitch", value: "off", isStateChange: true) 335 | sendEvent(name: "switch", value: "off", isStateChange: true) 336 | def result = createEvent(name: "switchon", value: "default", isStateChange: true) 337 | return result 338 | case 'mainon': 339 | //sendEvent(name: "mainswitch", value: "on", isStateChange: true) 340 | //def result = createEvent(name: "mainswitch", value: "on", isStateChange: true) 341 | sendEvent(name: "switch", value: "on", isStateChange: true) 342 | def result = createEvent(name: "switchon", value: "default", isStateChange: true) 343 | return result 344 | case 'tvinput': 345 | //sendEvent(name: "tvinput", value: "default", isStateChange: true) 346 | def result = createEvent(name: "tvinput", value: "default", isStateChange: true) 347 | return result 348 | case 'chanup': 349 | def result = createEvent(name: "chanup", value: "default", isStateChange: true) 350 | return result 351 | case 'chandown': 352 | def result = createEvent(name: "chandown", value: "default", isStateChange: true) 353 | return result 354 | case 'tvprev': 355 | //sendEvent(name: "tvprev", value: "default", isStateChange: true) 356 | def result = createEvent(name: "tvprev", value: "default", isStateChange: true) 357 | return result 358 | case 'volup': 359 | def result = createEvent(name: "volup", value: "default", isStateChange: true) 360 | return result 361 | case 'voldown': 362 | def result = createEvent(name: "voldown", value: "default", isStateChange: true) 363 | return result 364 | case 'tvmute': 365 | def result = createEvent(name: "tvmute", value: "default", isStateChange: true) 366 | return result 367 | case 'RebootNow': 368 | //sendEvent(name: "rebootnow", value: "default", isStateChange: true) 369 | def result = createEvent(name: "rebootnow", value: "default", isStateChange: true) 370 | return result 371 | // default: 372 | // sendEvent(name: "refreshswitch", value: "default", isStateChange: true) 373 | // def result = createEvent(name: "refreshswitch", value: "default", isStateChange: true) 374 | // // log.debug "refreshswitch returned ${result?.descriptionText}" 375 | // return result 376 | } 377 | 378 | // sendEvent(name: "switch", value: "on", unit: "") 379 | // sendEvent(name: "level", value: value, unit: "") 380 | } 381 | 382 | def parseDescriptionAsMap(description) { 383 | description.split(",").inject([:]) { map, param -> 384 | def nameAndValue = param.split(":") 385 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 386 | } 387 | } 388 | private String convertIPtoHex(ipAddress) { 389 | String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join() 390 | //log.debug "IP address entered is $ipAddress and the converted hex code is $hex" 391 | return hex 392 | } 393 | private String convertPortToHex(port) { 394 | String hexport = port.toString().format( '%04x', port.toInteger() ) 395 | //log.debug hexport 396 | return hexport 397 | } 398 | private Integer convertHexToInt(hex) { 399 | Integer.parseInt(hex,16) 400 | } 401 | private String convertHexToIP(hex) { 402 | //log.debug("Convert hex to ip: $hex") 403 | [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") 404 | } 405 | private getHostAddress() { 406 | def parts = device.deviceNetworkId.split(":") 407 | //log.debug device.deviceNetworkId 408 | def ip = convertHexToIP(parts[0]) 409 | def port = convertHexToInt(parts[1]) 410 | return ip + ":" + port 411 | } 412 | -------------------------------------------------------------------------------- /Devices/TVDevice/TVvolume.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * TVvolume v1.0.20160619 3 | * 4 | * Source code can be found here: https://github.com/JZ-SmartThings/SmartThings/blob/master/Devices/TVDevice/TVDevice.groovy 5 | * 6 | * Copyright 2016 JZ 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 9 | * in compliance with the License. You may obtain a copy of the License at: 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 14 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 15 | * for the specific language governing permissions and limitations under the License. 16 | * 17 | */ 18 | 19 | import groovy.json.JsonSlurper 20 | 21 | metadata { 22 | definition (name: "TVvolume", author: "JZ", namespace:"JZ") { 23 | capability "Switch" 24 | attribute "displayName", "string" 25 | command "tvmute" 26 | command "ResetTiles" 27 | } 28 | 29 | preferences { 30 | input("DeviceIP", "string", title:"Device IP Address", description: "Please enter your device's IP Address", required: true, displayDuringSetup: true) 31 | input("DevicePort", "string", title:"Device Port", description: "Empty assumes port 80.", required: false, displayDuringSetup: true) 32 | input("DevicePathOn", "string", title:"URL Path for ON", description: "Rest of the URL, include forward slash.", displayDuringSetup: true) 33 | input("DevicePathOff", "string", title:"URL Path for OFF", description: "Rest of the URL, include forward slash.", displayDuringSetup: true) 34 | input(name: "DevicePostGet", type: "enum", title: "POST or GET", options: ["POST","GET"], defaultValue: "POST", required: false, displayDuringSetup: true) 35 | section() { 36 | input("HTTPAuth", "bool", title:"Requires User Auth?", description: "Choose if the HTTP requires basic authentication", defaultValue: false, required: true, displayDuringSetup: true) 37 | input("HTTPUser", "string", title:"HTTP User", description: "Enter your basic username", required: false, displayDuringSetup: true) 38 | input("HTTPPassword", "string", title:"HTTP Password", description: "Enter your basic password", required: false, displayDuringSetup: true) 39 | } 40 | } 41 | 42 | simulator { 43 | } 44 | 45 | tiles(scale: 2) { 46 | valueTile("displayName", "device.displayName", width: 4, height: 4, decoration: "flat") { 47 | state("default", label: 'TV Volume: \n${currentValue}', backgroundColor:"#ffffff") 48 | } 49 | standardTile("switchon", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 50 | state "default", label: 'ON', action: "on", icon: "st.custom.buttons.add-icon", backgroundColor: "#FF6600", nextState: "trying" 51 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.custom.buttons.add-icon", backgroundColor: "#FFAA33" 52 | } 53 | standardTile("switchoff", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 54 | state "default", label:'OFF' , action: "off", icon: "st.custom.buttons.subtract-icon", backgroundColor:"#53a7c0", nextState: "trying" 55 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.custom.buttons.subtract-icon", backgroundColor: "#FFAA33" 56 | } 57 | standardTile("tvmute", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true, decoration: "flat") { 58 | state "default", label: 'MUTE', action: "tvmute", icon: "st.custom.sonos.muted", backgroundColor: "#9966CC", nextState: "trying" 59 | state "trying", label: 'TRYING', action: "ResetTiles", icon: "st.custom.sonos.muted", backgroundColor: "#FFAA33" 60 | } 61 | main "tvmute" 62 | details(["displayName","switchon", "switchoff", "tvmute"]) 63 | } 64 | } 65 | 66 | def ResetTiles() { 67 | sendEvent(name: "switchon", value: "default", isStateChange: true) 68 | sendEvent(name: "switchoff", value: "default", isStateChange: true) 69 | sendEvent(name: "tvmute", value: "default", isStateChange: true) 70 | log.debug "Resetting tiles." 71 | } 72 | 73 | def on() { 74 | log.debug "---ON COMMAND---" 75 | runCmd("/ir?tv=volup") 76 | } 77 | def off() { 78 | log.debug "---OFF COMMAND---" 79 | runCmd("/ir?tv=voldown") 80 | } 81 | def tvmute() { 82 | runCmd("/ir?tv=mute") 83 | } 84 | 85 | def runCmd(String varCommand) { 86 | def host = DeviceIP 87 | def hosthex = convertIPtoHex(host).toUpperCase() 88 | def LocalDevicePort = '' 89 | if (DevicePort==null) { LocalDevicePort = "80" } else { LocalDevicePort = DevicePort } 90 | def porthex = convertPortToHex(LocalDevicePort).toUpperCase() 91 | device.deviceNetworkId = "$hosthex:$porthex" 92 | def userpassascii = "${HTTPUser}:${HTTPPassword}" 93 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString() 94 | 95 | log.debug "The device id configured is: $device.deviceNetworkId" 96 | 97 | def path = varCommand 98 | log.debug "path is: $path" 99 | log.debug "Uses which method: $DevicePostGet" 100 | def body = varCommand 101 | log.debug "body is: $body" 102 | 103 | def headers = [:] 104 | headers.put("HOST", "$host:$LocalDevicePort") 105 | headers.put("Content-Type", "application/x-www-form-urlencoded") 106 | if (HTTPAuth) { 107 | headers.put("Authorization", userpass) 108 | } 109 | log.debug "The Header is $headers" 110 | def method = "POST" 111 | try { 112 | if (DevicePostGet.toUpperCase() == "GET") { 113 | method = "GET" 114 | } 115 | } 116 | catch (Exception e) { 117 | settings.DevicePostGet = "POST" 118 | log.debug e 119 | log.debug "You must not have set the preference for the DevicePOSTGET option" 120 | } 121 | log.debug "The method is $method" 122 | try { 123 | def hubAction = new physicalgraph.device.HubAction( 124 | method: method, 125 | path: path, 126 | body: body, 127 | headers: headers 128 | ) 129 | hubAction.options = [outputMsgToS3:false] 130 | log.debug hubAction 131 | hubAction 132 | } 133 | catch (Exception e) { 134 | log.debug "Hit Exception $e on $hubAction" 135 | } 136 | } 137 | 138 | def parse(String description) { 139 | // log.debug "Parsing '${description}'" 140 | def whichTile = '' 141 | def map = [:] 142 | def retResult = [] 143 | def descMap = parseDescriptionAsMap(description) 144 | def jsonlist = [:] 145 | def bodyReturned = ' ' 146 | def headersReturned = ' ' 147 | if (descMap["body"] && descMap["headers"]) { 148 | bodyReturned = new String(descMap["body"].decodeBase64()) 149 | headersReturned = new String(descMap["headers"].decodeBase64()) 150 | } 151 | // log.debug "BODY---" + bodyReturned 152 | // log.debug "HEADERS---" + headersReturned 153 | 154 | if (descMap["body"]) { 155 | if (headersReturned.contains("application/json")) { 156 | def body = new String(descMap["body"].decodeBase64()) 157 | def slurper = new JsonSlurper() 158 | jsonlist = slurper.parseText(body) 159 | //log.debug "JSONLIST---" + jsonlist."CPU" 160 | jsonlist.put ("Date", new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone)) 161 | } else if (headersReturned.contains("text/html")) { 162 | jsonlist.put ("Date", new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone)) 163 | def data=bodyReturned.eachLine { line -> 164 | 165 | if (line.length()<15 && !line.contains("<") && !line.contains(">") && !line.contains("/")) { 166 | log.trace "---" + line 167 | if (line.contains('tv=')) { jsonlist.put ("tv", line.replace("tv=","")) } 168 | } 169 | } 170 | } 171 | } 172 | if (descMap["body"] && (headersReturned.contains("application/json") || headersReturned.contains("text/html"))) { 173 | //putImageInS3(descMap) 174 | if (jsonlist."tv"=="volup") { 175 | sendEvent(name: "switchon", value: "default", isStateChange: true) 176 | whichTile = 'mainon' 177 | } 178 | if (jsonlist."tv"=="voldown") { 179 | sendEvent(name: "switchoff", value: "default", isStateChange: true) 180 | whichTile = 'mainoff' 181 | } 182 | if (jsonlist."tv"=="mute") { 183 | sendEvent(name: "tvmute", value: "default", isStateChange: true) 184 | whichTile = 'tvmute' 185 | } 186 | } 187 | 188 | log.debug jsonlist 189 | 190 | //RESET THE DEVICE ID TO GENERIC/RANDOM NUMBER. THIS ALLOWS MULTIPLE DEVICES TO USE THE SAME ID/IP 191 | device.deviceNetworkId = "ID_WILL_BE_CHANGED_AT_RUNTIME_" + (Math.abs(new Random().nextInt()) % 99999 + 1) 192 | 193 | //CHANGE NAME TILE 194 | sendEvent(name: "displayName", value: DeviceIP, unit: "") 195 | 196 | //RETURN BUTTONS TO CORRECT STATE 197 | log.debug 'whichTile: ' + whichTile 198 | switch (whichTile) { 199 | case 'mainoff': 200 | sendEvent(name: "switch", value: "off", isStateChange: true) 201 | def result = createEvent(name: "switchon", value: "default", isStateChange: true) 202 | return result 203 | case 'mainon': 204 | sendEvent(name: "switch", value: "on", isStateChange: true) 205 | def result = createEvent(name: "switchon", value: "default", isStateChange: true) 206 | return result 207 | case 'tvmute': 208 | sendEvent(name: "switch", value: "default", isStateChange: true) 209 | def result = createEvent(name: "tvmute", value: "default", isStateChange: true) 210 | return result 211 | } 212 | } 213 | 214 | def parseDescriptionAsMap(description) { 215 | description.split(",").inject([:]) { map, param -> 216 | def nameAndValue = param.split(":") 217 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 218 | } 219 | } 220 | private String convertIPtoHex(ipAddress) { 221 | String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join() 222 | //log.debug "IP address entered is $ipAddress and the converted hex code is $hex" 223 | return hex 224 | } 225 | private String convertPortToHex(port) { 226 | String hexport = port.toString().format( '%04x', port.toInteger() ) 227 | //log.debug hexport 228 | return hexport 229 | } 230 | private Integer convertHexToInt(hex) { 231 | Integer.parseInt(hex,16) 232 | } 233 | private String convertHexToIP(hex) { 234 | //log.debug("Convert hex to ip: $hex") 235 | [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") 236 | } 237 | private getHostAddress() { 238 | def parts = device.deviceNetworkId.split(":") 239 | //log.debug device.deviceNetworkId 240 | def ip = convertHexToIP(parts[0]) 241 | def port = convertHexToInt(parts[1]) 242 | return ip + ":" + port 243 | } 244 | -------------------------------------------------------------------------------- /Devices/Wink Relay Toggle Switch/Wink Relay Toggle Switch.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Wink Relay Toggle Switch v1.0.20171222 3 | * Copyright 2017 JZ 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. You may obtain a copy of the License at: 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 10 | * for the specific language governing permissions and limitations under the License. 11 | */ 12 | metadata { 13 | definition (name: "Wink Relay Toggle Switch", namespace: "JZ", author: "JZ") { 14 | capability "Switch" 15 | capability "Refresh" 16 | attribute "syncSwitch", "string" 17 | attribute "lastToggled", "string" 18 | attribute "lastToggledEPOC", "number" 19 | } 20 | 21 | tiles(scale: 2) { 22 | standardTile("switch", "device.switch", width: 6, height: 4, canChangeIcon: true) { 23 | state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" 24 | state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" 25 | } 26 | valueTile("lastToggled", "device.lastToggled", width: 4, height: 2, decoration: "flat") { 27 | state("default", label: '${currentValue}', backgroundColor:"#ffffff") 28 | } 29 | standardTile("refresh", "device.refresh", width: 2, height: 2, decoration: "flat") { 30 | state "default", label:'REFRESH', action: "refresh", icon: "st.secondary.refresh-icon", backgroundColor:"#53a7c0" 31 | } 32 | main "switch" 33 | details(["switch","lastToggled","refresh"]) 34 | } 35 | 36 | preferences { 37 | input "dupeInterval", "number", title: "Seconds to ignore duplicate calls. Prevents double firing when late external calls like IFTTT/MQTT run. Important for momentary/toggle switches when externally integrated.", description: "Range: 0-75. Default: 10 sec.", range: "0..75", defaultValue: "10", displayDuringSetup: false 38 | } 39 | } 40 | 41 | def refresh() { 42 | log.debug "refresh()" 43 | def syncSwitch = device.currentState("syncSwitch").getValue() 44 | log.debug "$syncSwitch" 45 | } 46 | 47 | def parse(description) { 48 | def eventMap 49 | if (description.type == null) eventMap = [name:"$description.name", value:"$description.value"] 50 | else eventMap = [name:"$description.name", value:"$description.value", type:"$description.type"] 51 | createEvent(eventMap) 52 | } 53 | 54 | def on() { 55 | log.debug "on()" 56 | toggleSwitch() 57 | } 58 | 59 | def off() { 60 | log.debug "off()" 61 | toggleSwitch() 62 | } 63 | 64 | def toggleSwitch() { 65 | if (device.currentValue("lastToggledEPOC")==null || device.currentValue("lastToggledEPOC")==0) { 66 | sendEvent(name: "lastToggledEPOC", value: 1, isStateChange: true) 67 | } 68 | 69 | if (now()-device.currentValue("lastToggledEPOC") > 0) { 70 | def currstate = "off" 71 | if (device.currentState("syncSwitch")!=null) { 72 | currstate=device.currentState("syncSwitch").getValue() 73 | } 74 | 75 | def currdate = new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone) 76 | if (currstate == "on") { 77 | sendEvent(name: "switch", value: "off", isStateChange: true, display: false) 78 | sendEvent(name: "syncSwitch", value: "off", isStateChange: true, display: false) 79 | currdate = "OFF @ " + currdate 80 | sendEvent(name: "lastToggled", value: currdate) 81 | log.debug "Toggled: off" 82 | updateEPOC("Updated EPOC from ON method") 83 | } 84 | else if (currstate == "off") { 85 | sendEvent(name: "switch", value: "on", isStateChange: true, display: false) 86 | sendEvent(name: "syncSwitch", value: "on", isStateChange: true, display: false) 87 | currdate = "ON @ " + currdate 88 | sendEvent(name: "lastToggled", value: currdate) 89 | log.debug "Toggled: on" 90 | updateEPOC("Updated EPOC from OFF method") 91 | } 92 | } else { 93 | log.debug "Not triggered due to EPOC difference." 94 | } 95 | } 96 | 97 | def updateEPOC(String caller) { 98 | def dupeThreshold = 10000 99 | if (dupeInterval != null) { 100 | dupeThreshold = dupeInterval*1000 101 | } 102 | 103 | log.debug "EPOC before update: " + device.currentValue("lastToggledEPOC") 104 | sendEvent(name: "lastToggledEPOC", value: now()+dupeThreshold, isStateChange: true) 105 | log.debug "Updated EPOC ${caller} to: " + now()+dupeThreshold 106 | } 107 | -------------------------------------------------------------------------------- /Icons/README.md: -------------------------------------------------------------------------------- 1 | # SmartThings Icon List 2 | 3 | Please use this URL to see this page: https://cdn.rawgit.com/JZ-SmartThings/SmartThings/master/Icons/ST-Icons.html -------------------------------------------------------------------------------- /Icons/ST-Icons.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JZ-SmartThings/SmartThings/4bec0ff1d7803f245abd64c75df409ba378baf35/Icons/ST-Icons.html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartThings -------------------------------------------------------------------------------- /SmartApps/Node-RED Power Refresher/Node-RED Power Refresher.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Node-RED Power Refresher v1.0.20190128 3 | * This works well with TP-Link HS110 Power Monitoring Switch. Shortest interval should not be less than 5 seconds or SmartThings hub may become unresponsive &CRASH!!! 4 | * Copyright 2019 JZ 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | 13 | *** Node-Red import: 14 | [{"id":"81cd71eb.f90cf","type":"http in","z":"d0355279.302e48","name":"","url":"/powerrefresh","method":"get","upload":false,"swaggerDoc":"","x":110,"y":80,"wires":[["171f5f8f.646db","ac51dfc7.e21ee","c3a3b1cc.e18f58"]]},{"id":"81e0d24e.ee8b4","type":"function","z":"d0355279.302e48","name":"Remove Dupes","func":"var last_power = Number(flow.get('last_power')) || 0;\nflow.set('last_power',Number(msg.payload.power));\n\nif (last_power == Number(msg.payload.power)) {\n return null;\n} else {\n msg.payload=null;\n msg.payload=Number(flow.get('last_power')) || 0;\n return msg;\n}","outputs":1,"noerr":0,"x":400,"y":240,"wires":[["7dcd4771.e3572","66a3f2b.87eb98c"]]},{"id":"171f5f8f.646db","type":"debug","z":"d0355279.302e48","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.power","x":130,"y":120,"wires":[]},{"id":"7dcd4771.e3572","type":"debug","z":"d0355279.302e48","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":390,"y":160,"wires":[]},{"id":"ac51dfc7.e21ee","type":"http response","z":"d0355279.302e48","name":"","statusCode":"200","headers":{},"x":380,"y":40,"wires":[]},{"id":"66a3f2b.87eb98c","type":"mqtt out","z":"d0355279.302e48","name":"MQTT Power","topic":"smartthings/HVAC Pump Sensor/power","qos":"","retain":"","broker":"cb9f73e8.8d453","x":810,"y":120,"wires":[]},{"id":"938cf8b.741e088","type":"delay","z":"d0355279.302e48","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"15","rateUnits":"minute","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":580,"y":80,"wires":[["66a3f2b.87eb98c","90c6db12.ec0278"]]},{"id":"90c6db12.ec0278","type":"debug","z":"d0355279.302e48","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":810,"y":80,"wires":[]},{"id":"c3a3b1cc.e18f58","type":"function","z":"d0355279.302e48","name":"Fix Payload","func":"tmp_payload=Number(msg.payload.power) || 0;\nmsg.payload=null;\nmsg.payload=tmp_payload\nreturn msg;\n","outputs":1,"noerr":0,"x":390,"y":80,"wires":[["938cf8b.741e088","9932436a.33a088"]]},{"id":"9932436a.33a088","type":"rbe","z":"d0355279.302e48","name":"","func":"rbe","gap":"","start":"","inout":"out","property":"payload","x":550,"y":120,"wires":[["66a3f2b.87eb98c"]]},{"id":"cb9f73e8.8d453","type":"mqtt-broker","z":"","broker":"localhost","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","willTopic":"","willQos":"0","willPayload":""}] 15 | 16 | */ 17 | 18 | definition( 19 | name: "Node-RED Power Refresher", 20 | namespace: "JZ", 21 | author: "JZ", 22 | description: "Node-RED Power Refresher calls the refresh method of a single device and posts to Node-RED.", 23 | category: "", 24 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 25 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 26 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 27 | 28 | preferences { 29 | section("Node-RED Power Refresher Configuration:") { 30 | input ("refreshdevice", "capability.refresh", title: "Device to Refresh", multiple: false, required: true) 31 | } 32 | section("Refresh Interval in Seconds. 0 or null turns refreshing off:") { 33 | input ("refreshfreq", "number", title: "Refresh Frequency in seconds?", multiple: false, required: true, default: 0) 34 | } 35 | section("Host:Port of Node-RED:") { 36 | input ("hostport", "string", title: "Host and port of Node-RED?", multiple: false, required: true, default: "192.168.0.123:1880") 37 | } 38 | section("The rest of the page URL endpoint after forward slash:") { 39 | input ("urlending", "string", title: "URL endpoint page?", multiple: false, required: false, default: "powerrefresh") 40 | } 41 | section("Form attribute name:") { 42 | input ("formattribute", "string", title: "Form attribute?", multiple: false, required: false, default: "power") 43 | } 44 | } 45 | 46 | def installed() { 47 | log.debug "Installed with settings: ${settings}" 48 | initialize() 49 | } 50 | 51 | def updated() { 52 | log.debug "Updated with settings: ${settings}" 53 | unsubscribe() 54 | unschedule() 55 | initialize() 56 | } 57 | 58 | def initialize() { 59 | if (refreshdevice) { 60 | subscribe(app, runApp) 61 | } 62 | if (refreshfreq > 0) { 63 | schedule(now() + refreshfreq*1000, refreshFunc) 64 | } 65 | 66 | } 67 | 68 | def callRefresh(evt) { 69 | refreshdevice.refresh() 70 | } 71 | 72 | def runApp(evt) { 73 | log.debug "Manual refresh of " + settings["refreshdevice"] + " triggered. Currently set to refresh every " + refreshfreq + " seconds." 74 | refreshdevice.refresh() 75 | } 76 | 77 | def refreshFunc() { 78 | refreshdevice.refresh() 79 | //log.debug "Auto refresh of " + settings["refreshdevice"] + " triggered. Currently set to refresh every " + refreshfreq + " seconds." 80 | schedule(now() + refreshfreq*1000, refreshFunc) 81 | 82 | if (state.lastvalue != String.format('%6.0f', refreshdevice*.currentValue('power')[0]).trim()) { 83 | state.lastvalue = String.format('%6.0f', refreshdevice*.currentValue('power')[0]).trim() 84 | def theAction = new physicalgraph.device.HubAction("""GET /${settings['urlending']}?${settings['formattribute']}=${(String.format('%6.0f', refreshdevice*.currentValue('power')[0]).trim())} HTTP/1.1\r\n Accept: */*\r\nHOST: ${settings['hostport']}\r\n\r\n""", physicalgraph.device.Protocol.LAN, settings['hostport'], [callback: calledBackHandler]) 85 | sendHubCommand(theAction) 86 | //def theAction = new physicalgraph.device.HubAction("""GET /zoozpower?power=${(String.format('%6.0f', refreshdevice*.currentValue('power')[0]).trim())} HTTP/1.1\r\n Accept: */*\r\nHOST: 192.168.0.251:1880\r\n\r\n""", physicalgraph.device.Protocol.LAN, "192.168.0.251:1880", [callback: calledBackHandler]) 87 | } 88 | } 89 | void calledBackHandler(physicalgraph.device.HubResponse hubResponse) 90 | { 91 | log.debug "Reponse ${hubResponse.body}" 92 | } -------------------------------------------------------------------------------- /SmartApps/Virtual Custom Switch Sync App/VirtualCustomSwitchSyncApp.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Virtual Custom Switch Sync App v1.0.20190210 3 | * Copyright 2019 JZ 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. You may obtain a copy of the License at: 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 10 | * for the specific language governing permissions and limitations under the License. 11 | */ 12 | definition( 13 | name: "Virtual Custom Switch Sync App", 14 | namespace: "JZ", 15 | author: "JZ", 16 | description: "Synchronize a simulated/virtual switch with the Custom Switch & 2nd Sensor of the Generic HTTP Device Handler. This helps with automation of the second button & sensor.", 17 | category: "", 18 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 19 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 20 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 21 | 22 | preferences { 23 | section("Choose your Generic HTTP Device:") { 24 | input ("httpswitch", "capability.switch", title: "HTTP Device?", multiple: false, required: true) 25 | } 26 | section("Choose your Simulated, currently unlinked Switch:") { 27 | input ("virtualswitch", "capability.switch", title: "Virtual Switch?", multiple: false, required: false) 28 | } 29 | section("Choose your Simulated, currently unlinked Contact Sensor:") { 30 | input ("virtualsensor", "capability.sensor", title: "Virtual Contact Sensor?", multiple: false, required: false) 31 | } 32 | section("Refresh/Poll Interval in Minutes. 0 or null turns refreshing off (try not to refresh too often):") { 33 | input ("refreshfreq", "number", title: "Refresh/Poll Frequency in minutes?", multiple: false, required: false) 34 | } 35 | } 36 | 37 | def installed() { 38 | log.debug "Installed with settings: ${settings}" 39 | initialize() 40 | } 41 | 42 | def updated() { 43 | log.debug "Updated with settings: ${settings}" 44 | unsubscribe() 45 | unschedule() 46 | initialize() 47 | } 48 | 49 | def initialize() { 50 | if (httpswitch) { 51 | subscribe(app, runApp) 52 | subscribe(httpswitch, "refreshTriggered", updateRefreshTiles) 53 | } 54 | if (virtualswitch) { 55 | subscribe(virtualswitch, "switch", virtualSwitchHandler) 56 | subscribe(httpswitch, "customswitch", switchOffHandler) 57 | subscribe(httpswitch, "customTriggered", updateCustomTriggered) 58 | subscribeToCommand(virtualswitch, "refresh", callRefresh) 59 | } 60 | if (virtualsensor) { 61 | subscribe(httpswitch, "contact2", virtualSensorHandler) 62 | subscribeToCommand(virtualsensor, "refresh", callRefresh) 63 | } 64 | if (refreshfreq > 0) { 65 | schedule(now() + refreshfreq*1000*60, httpRefresh) 66 | } 67 | } 68 | 69 | def callRefresh(evt) { 70 | httpswitch.refresh() 71 | } 72 | 73 | def updateCustomTriggered(evt) { 74 | sendEvent(settings["virtualswitch"], [name:"customTriggered", value:httpswitch*.currentValue("customTriggered")[0]]) 75 | } 76 | 77 | def runApp(evt) { 78 | log.debug "Manual refresh of " + settings["httpswitch"] + " triggered. Currently set to refresh every " + refreshfreq + " minutes." 79 | httpswitch.refresh() 80 | } 81 | def httpRefresh() { 82 | httpswitch.refresh() 83 | log.debug "Auto refresh of " + settings["httpswitch"] + " triggered. Currently set to refresh every " + refreshfreq + " minutes." 84 | schedule(now() + refreshfreq*1000*60, httpRefresh) 85 | } 86 | 87 | def switchOffHandler(evt) { 88 | //log.debug "$httpswitch.name was turned " + httpswitch*.currentValue("customswitch") 89 | log.debug "switchOffHandler called with event: deviceId ${evt.deviceId} name:${evt.name} source:${evt.source} value:${evt.value} isStateChange: ${evt.isStateChange()} isPhysical: ${evt.isPhysical()} isDigital: ${evt.isDigital()} data: ${evt.data} device: ${evt.device}" 90 | 91 | // TRYING VALUE OF customswitch FROM HTTP DEVICE RATHER THAN $evt.value 92 | //sendEvent(settings["virtualswitch"], [name:"switch", value:"$evt.value"]) 93 | //runIn(1,updateVirtualSwitch) 94 | //for (int i = 1; i<=3; i++) { runIn(i,updateVirtualSwitch) } 95 | for (int i = 1; i<=3; i++) { schedule(now() + i*1000, updateVirtualSwitch) } 96 | sendEvent(settings["virtualswitch"], [name:"customTriggered", value:httpswitch*.currentValue("customTriggered")[0]]) 97 | sendEvent(settings["virtualswitch"], [name:"switch", value:httpswitch*.currentValue("customswitch")[0]]) 98 | sendEvent(settings["httpswitch"], [name:"customswitch", value:virtualswitch*.currentValue("switch")]) 99 | for (int i = 1; i<=3; i++) { schedule(now() + i*1000, updateVirtualSwitch) } 100 | } 101 | def virtualSwitchHandler(evt) { 102 | log.debug "virtualSwitchHandler called with event: deviceId ${evt.deviceId} name:${evt.name} source:${evt.source} value:${evt.value} isStateChange: ${evt.isStateChange()} isPhysical: ${evt.isPhysical()} isDigital: ${evt.isDigital()} data: ${evt.data} device: ${evt.device}" 103 | log.trace "EPOCH value from main switch: " + httpswitch*.currentValue("customTriggeredEPOCH")[0] 104 | log.trace "EPOCH diff was: " + String.valueOf(now()-httpswitch*.currentValue("customTriggeredEPOCH")[0]) 105 | log.trace "Current EPOCH time: " + now() 106 | if (now()-httpswitch*.currentValue("customTriggeredEPOCH")[0] > 0) { 107 | httpswitch.CustomTrigger() 108 | //sendEvent(settings["httpswitch"], [name: "customTriggeredEPOCH", value: now(), isStateChange: true]) 109 | log.trace "EPOCH before update: " + httpswitch*.currentValue("customTriggeredEPOCH")[0] 110 | httpswitch.updateEPOCH("from Virtual Sync App") 111 | log.trace "Updated EPOCH from Virtual Sync App to: " + httpswitch*.currentValue("customTriggeredEPOCH")[0] 112 | //for (int i = 1; i<=3; i++) { runIn(i,updateVirtualSwitch) } 113 | for (int i = 1; i<=3; i++) { schedule(now() + i*1000, updateVirtualSwitch) } 114 | sendEvent(settings["virtualswitch"], [name:"customTriggered", value:httpswitch*.currentValue("customTriggered")[0]]) 115 | } else { 116 | //for (int i = 1; i<=3; i++) { runIn(i,updateVirtualSwitch) } 117 | //runIn(3,updateVirtualSwitch) 118 | for (int i = 1; i<=3; i++) { schedule(now() + i*1000, updateVirtualSwitch) } 119 | sendEvent(settings["virtualswitch"], [name:"customTriggered", value:httpswitch*.currentValue("customTriggered")[0]]) 120 | } 121 | } 122 | 123 | def updateVirtualSwitch() { 124 | //log.debug "virtualswitch " + virtualswitch.currentValue("switch") 125 | //log.debug "httpswitch " + httpswitch.currentValue("customswitch") 126 | if (virtualswitch.currentValue("switch") != httpswitch.currentValue("customswitch")) { 127 | log.debug "updateVirtualSwitch to ${httpswitch*.currentValue('customswitch')[0]}" 128 | sendEvent(settings["virtualswitch"], [name:"switch", value:httpswitch*.currentValue("customswitch")[0]]) 129 | } 130 | } 131 | 132 | def virtualSensorHandler(evt) { 133 | log.debug "virtualSensorHandler called with event: deviceId ${evt.deviceId} name:${evt.name} source:${evt.source} value:${evt.value} isStateChange: ${evt.isStateChange()} isPhysical: ${evt.isPhysical()} isDigital: ${evt.isDigital()} data: ${evt.data} device: ${evt.device}" 134 | sendEvent(settings["virtualsensor"], [name:"contact", value:"$evt.value"]) 135 | sendEvent(settings["virtualsensor"], [name:"sensor2Triggered", value:httpswitch*.currentValue("sensor2Triggered")[0]]) 136 | } 137 | 138 | def updateRefreshTiles(evt) { 139 | log.debug "Updating REFRESH tiles" 140 | //schedule(now() + 1000, updateRefreshEvents) 141 | runIn(1,updateRefreshEvents) 142 | } 143 | 144 | def updateRefreshEvents() { 145 | if (settings["virtualswitch"]) { sendEvent(settings["virtualswitch"], [name:"refreshTriggered", value:httpswitch*.currentValue("refreshTriggered")[0]]) } 146 | if (settings["virtualsensor"]) { sendEvent(settings["virtualsensor"], [name:"refreshTriggered", value:httpswitch*.currentValue("refreshTriggered")[0]]) } 147 | } 148 | --------------------------------------------------------------------------------