├── .gitattributes ├── .gitignore ├── .groovylintrc.json ├── Apps ├── LG │ ├── LG WebOS TV Discovery.groovy │ └── LG WebOS TV Discovery.json ├── Netatmo │ ├── Netatmo - Velux.groovy │ ├── Netatmo - Velux.json │ ├── Netatmo.groovy │ └── Netatmo.json ├── PanasonicComfortCloud │ ├── Panasonic - Comfort Cloud.groovy │ └── Panasonic - Comfort Cloud.json ├── Sonoff │ ├── Sonoff RF Bridge.groovy │ └── Sonoff RF Bridge.json ├── VictoriaMetrics │ ├── MetricLogger.groovy │ └── MetricLogger.json └── Warmup │ ├── Warmup - Cloud.groovy │ └── Warmup - Cloud.json ├── Drivers ├── Aeotec │ ├── Aeon WallMote Quad.groovy │ ├── Aeotec Heavy Duty Smart Switch.groovy │ ├── Aeotec Heavy Duty Smart Switch.json │ ├── Aeotec MultiSensor 6.groovy │ ├── Aeotec MultiSensor 6.json │ ├── Aeotec Water Sensor 6.groovy │ └── Aeotec Water Sensor 6.json ├── Eurotronic │ ├── Eurotronic Air Quality Sensor.groovy │ └── Eurotronic Air Quality Sensor.json ├── Fibaro │ ├── Fibaro Smoke Sensor.groovy │ └── Fibaro Smoke Sensor.json ├── Generic │ ├── Water Meter - API External.groovy │ ├── Z-Wave Repeater.groovy │ └── Z-Wave Repeater.json ├── Heatit │ ├── Heatit Z-Temp2.groovy │ └── Heatit Z-Temp2.json ├── Heltun │ ├── Heltun Touch Panel Switch - Button.groovy │ ├── Heltun Touch Panel Switch - Quinto.groovy │ └── Heltun Touch Panel Switch - Quinto.json ├── LG │ ├── LG WebOS Mouse.groovy │ └── LG WebOS TV.groovy ├── Loki │ ├── LokiLogLogger.groovy │ ├── LokiLogLogger.json │ ├── LokiZWaveLogger.groovy │ ├── LokiZWaveLogger.json │ ├── LokiZigbeeLogger.groovy │ └── LokiZigbeeLogger.json ├── Netatmo │ ├── Netatmo - Doorbell.groovy │ ├── Netatmo - Person.groovy │ ├── Netatmo - Presence.groovy │ ├── Netatmo - Sensor.groovy │ ├── Netatmo - Smoke Alarm.groovy │ ├── Netatmo - Velux - Blind.groovy │ ├── Netatmo - Velux - Departure switch.groovy │ ├── Netatmo - Velux - Gateway.groovy │ ├── Netatmo - Velux - Home.groovy │ ├── Netatmo - Velux - Room.groovy │ ├── Netatmo - Velux - Sensor switch.groovy │ ├── Netatmo - Velux - Shutter.groovy │ ├── Netatmo - Velux - Window.groovy │ └── Netatmo - Welcome.groovy ├── Orvibo │ ├── Orvibo Smart Temperature & Humidity Sensor.groovy │ └── Orvibo Smart Temperature & Humidity Sensor.json ├── PanasonicComfortCloud │ ├── Panasonic - Comfort Cloud - AC.groovy │ └── Panasonic - Comfort Cloud - Group.groovy ├── Popp │ ├── Popp Electric Strike Lock Control.groovy │ ├── Popp Electric Strike Lock Control.json │ ├── Popp Z-Rain Sensor.groovy │ └── Popp Z-Rain Sensor.json ├── Qubino │ ├── Qubino Flush Pilot Wire.groovy │ ├── Qubino Flush Pilot Wire.json │ ├── Qubino Flush Shutter - CMV.groovy │ └── Qubino Flush Shutter - CMV.json ├── Schwaiger │ ├── Schwaiger Temperature Sensor.groovy │ └── Schwaiger Temperature Sensor.json ├── Shelly │ ├── ShellyPlus Generic.groovy │ ├── ShellyPlus Generic.json │ ├── ShellyPlus Pilot Wire.groovy │ └── ShellyPlus Pilot Wire.json ├── Sonoff │ ├── Sonoff RF Bridge - Shade.groovy │ ├── Sonoff RF Bridge - Switch.groovy │ └── Sonoff RF Bridge.groovy ├── Warmup │ ├── Warmup - Cloud - Location.groovy │ └── Warmup - Cloud - Room.groovy ├── Xiaomi │ ├── Xiaomi Mijia - Sensor Temperature Humidity Child Device.groovy │ ├── Xiaomi Mijia DataCollector │ │ ├── mijia │ │ │ ├── mijia_v1_poller.py │ │ │ └── mijia_v2_poller.py │ │ └── send_data.py │ ├── Xiaomi Mijia.groovy │ └── Xiaomi Mijia.json └── Zipato │ ├── Zipato Mini RFID Keypad.groovy │ └── Zipato Mini RFID Keypad.json ├── README.md ├── Tools ├── LokiSuphecap │ └── LokiSuphacap.py └── Monitoring │ ├── cfg │ ├── grafana │ │ └── provisioning │ │ │ ├── dashboards │ │ │ ├── dashboard.yml │ │ │ └── victoriametrics.json │ │ │ ├── datasources │ │ │ └── datasource.yml │ │ │ ├── notifiers │ │ │ └── .keep │ │ │ └── plugins │ │ │ └── .keep │ └── loki.yml │ ├── docker-compose.yml │ └── start.sh ├── deploy.py └── repository.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | .gitignore text eol=lf 3 | .gitconfig text eol=lf 4 | .gitattributes text eol=lf 5 | .groovylintrc.json text eol=lf 6 | 7 | *.bat text eol=crlf 8 | *.cmd text eol=crlf 9 | *.ps1 text eol=crlf 10 | *.vbs text eol=crlf 11 | *.sh text eol=lf 12 | *.pl text eol=lf 13 | *.py text eol=lf 14 | *.groovy text eol=lf 15 | 16 | *.gz binary 17 | *.tar binary 18 | *.zip binary 19 | *.7z binary 20 | *.jar binary 21 | 22 | *.exe binary 23 | *.dll binary 24 | *.pyc binary 25 | *.pyd binary 26 | *.pyo binary 27 | 28 | *.json text eol=lf 29 | *.conf text eol=lf 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | temp/ 3 | data/ 4 | .vscode/ 5 | .creds/ 6 | .env 7 | *~ 8 | *.swp 9 | .pyc 10 | **/__pycache__ 11 | **/.DS_Store -------------------------------------------------------------------------------- /.groovylintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "recommended", 3 | "rules": { 4 | "CompileStatic": { 5 | "enabled": false 6 | }, 7 | "DuplicateNumberLiteral": { 8 | "enabled": false 9 | }, 10 | "DuplicateStringLiteral": { 11 | "enabled": false 12 | }, 13 | "IfStatementBraces": { 14 | "enabled": false 15 | }, 16 | "LineLength": { 17 | "length": 240 18 | }, 19 | "MethodCount": { 20 | "enabled": false 21 | }, 22 | "MethodParameterTypeRequired": { 23 | "enabled": false 24 | }, 25 | "MethodReturnTypeRequired": { 26 | "enabled": false 27 | }, 28 | "UnnecessaryGetter": { 29 | "enabled": false 30 | }, 31 | "UnnecessaryReturnKeyword": { 32 | "enabled": false 33 | }, 34 | "comments.ClassJavadoc": "off", 35 | "formatting.Indentation": { 36 | "spacesPerIndentLevel": 2, 37 | "severity": "info" 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Apps/LG/LG WebOS TV Discovery.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "LG WebOS TV Discovery", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2020-10-28", 6 | "documentationLink": "", 7 | "communityLink": "", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "apps": [ 11 | { 12 | "name": "LG WebOS TV Discovery", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/LG/LG%20WebOS%20TV%20Discovery.groovy", 15 | "version": "1.0.0", 16 | "required": true, 17 | "oauth": false, 18 | "primary": true 19 | } 20 | ], 21 | "drivers": [ 22 | { 23 | "name": "LG WebOS TV", 24 | "namespace": "syepes", 25 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/LG/LG%20WebOS%20TV.groovy", 26 | "version": "1.0.0", 27 | "required": true 28 | }, 29 | { 30 | "name": "LG WebOS Mouse", 31 | "namespace": "syepes", 32 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/LG/LG%20WebOS%20Mouse.groovy", 33 | "version": "1.0.0", 34 | "required": true 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /Apps/Netatmo/Netatmo - Velux.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Netatmo - Velux", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-velux-active-with-netatmo/100499", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "apps": [ 11 | { 12 | "name": "Netatmo - Velux", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/Netatmo/Netatmo%20-%20Velux.groovy", 15 | "version": "1.0.0", 16 | "required": true, 17 | "oauth": false, 18 | "primary": true 19 | } 20 | ], 21 | "drivers": [ 22 | { 23 | "name": "Netatmo - Velux - Gateway", 24 | "namespace": "syepes", 25 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Gateway.groovy", 26 | "version": "1.0.2", 27 | "required": true 28 | }, 29 | { 30 | "name": "Netatmo - Velux - Departure switch", 31 | "namespace": "syepes", 32 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Departure%20switch.groovy", 33 | "version": "1.0.1", 34 | "required": true 35 | }, 36 | { 37 | "name": "Netatmo - Velux - Sensor switch", 38 | "namespace": "syepes", 39 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Sensor%20switch.groovy", 40 | "version": "1.0.1", 41 | "required": true 42 | }, 43 | { 44 | "name": "Netatmo - Velux - Home", 45 | "namespace": "syepes", 46 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Home.groovy", 47 | "version": "1.0.1", 48 | "required": true 49 | }, 50 | { 51 | "name": "Netatmo - Velux - Room", 52 | "namespace": "syepes", 53 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Room.groovy", 54 | "version": "1.0.1", 55 | "required": true 56 | }, 57 | { 58 | "name": "Netatmo - Velux - Blind", 59 | "namespace": "syepes", 60 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Blind.groovy", 61 | "version": "1.0.0", 62 | "required": true 63 | }, 64 | { 65 | "name": "Netatmo - Velux - Shutter", 66 | "namespace": "syepes", 67 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Shutter.groovy", 68 | "version": "1.0.3", 69 | "required": true 70 | }, 71 | { 72 | "name": "Netatmo - Velux - Window", 73 | "namespace": "syepes", 74 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Window.groovy", 75 | "version": "1.0.3", 76 | "required": true 77 | } 78 | ] 79 | } -------------------------------------------------------------------------------- /Apps/Netatmo/Netatmo.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Netatmo", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2021-02-04", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-netatmo/32958/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "apps": [ 11 | { 12 | "name": "Netatmo", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/Netatmo/Netatmo.groovy", 15 | "version": "1.2.7", 16 | "required": true, 17 | "oauth": true, 18 | "primary": true 19 | } 20 | ], 21 | "drivers": [ 22 | { 23 | "name": "Netatmo - Doorbell", 24 | "namespace": "syepes", 25 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Doorbell.groovy", 26 | "version": "1.0.3", 27 | "required": true 28 | }, 29 | { 30 | "name": "Netatmo - Person", 31 | "namespace": "syepes", 32 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Person.groovy", 33 | "version": "1.0.2", 34 | "required": true 35 | }, 36 | { 37 | "name": "Netatmo - Presence", 38 | "namespace": "syepes", 39 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Presence.groovy", 40 | "version": "1.0.3", 41 | "required": true 42 | }, 43 | { 44 | "name": "Netatmo - Welcome", 45 | "namespace": "syepes", 46 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Welcome.groovy", 47 | "version": "1.0.3", 48 | "required": true 49 | }, 50 | { 51 | "name": "Netatmo - Sensor", 52 | "namespace": "syepes", 53 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Sensor.groovy", 54 | "version": "1.0.1", 55 | "required": true 56 | }, 57 | { 58 | "name": "Netatmo - Smoke Alarm", 59 | "namespace": "syepes", 60 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Smoke%20Alarm.groovy", 61 | "version": "1.0.2", 62 | "required": true 63 | } 64 | ] 65 | } -------------------------------------------------------------------------------- /Apps/PanasonicComfortCloud/Panasonic - Comfort Cloud.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Panasonic - Comfort Cloud", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-panasonic-ac-comfort-cloud/100503", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "apps": [ 11 | { 12 | "name": "Panasonic - Comfort Cloud", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/PanasonicComfortCloud/Panasonic%20-%20Comfort%20Cloud.groovy", 15 | "version": "1.0.2", 16 | "required": true, 17 | "oauth": false, 18 | "primary": true 19 | } 20 | ], 21 | "drivers": [ 22 | { 23 | "name": "Panasonic - Comfort Cloud - Group", 24 | "namespace": "syepes", 25 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/PanasonicComfortCloud/Panasonic%20-%20Comfort%20Cloud%20-%20Group.groovy", 26 | "version": "1.0.1", 27 | "required": true 28 | }, 29 | { 30 | "name": "Panasonic - Comfort Cloud - AC", 31 | "namespace": "syepes", 32 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/PanasonicComfortCloud/Panasonic%20-%20Comfort%20Cloud%20-%20AC.groovy", 33 | "version": "1.0.4", 34 | "required": true 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /Apps/Sonoff/Sonoff RF Bridge.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | /* 16 | Example json app parameter is defined as a json string 17 | Note: the string must be defined as one single line (https://www.webtoolkitonline.com/json-minifier.html), see the below examples: 18 | { 19 | "Shade:Bedroom":{ 20 | "close":"< B0 String that closes the Shade >", 21 | "open":"< B0 String that opens the Shade >", 22 | "stop":"< B0 String to stop the Shade >" 23 | }, 24 | "Switch:Radio":{ 25 | "on":"< B0 String turn on the Switch >", 26 | "off":"< B0 String turn off the Switch >" 27 | } 28 | } 29 | */ 30 | 31 | import groovy.transform.Field 32 | import groovy.json.JsonSlurper 33 | import com.hubitat.app.ChildDeviceWrapper 34 | 35 | @Field String VERSION = "1.0.0" 36 | 37 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 38 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 39 | 40 | definition( 41 | name: "Sonoff RF Bridge", 42 | namespace: "syepes", 43 | author: "Sebastian YEPES", 44 | description: "Sonoff RF Bridge", 45 | category: "", 46 | oauth: false, 47 | singleInstance: true, 48 | iconUrl: "https://play-lh.googleusercontent.com/3RC_WggdYWlA7ZFjH8YKHkDmMrLayPAN72MleyhtmnAa7NRD94yKfaqoXkqLmblJiw=w200-h200", 49 | iconX2Url: "https://play-lh.googleusercontent.com/3RC_WggdYWlA7ZFjH8YKHkDmMrLayPAN72MleyhtmnAa7NRD94yKfaqoXkqLmblJiw=w400-h400" 50 | ) 51 | 52 | preferences { 53 | page(name: "device", title: "Bridge device") 54 | page(name: "config", title: "Config") 55 | page(name: "check", title: "Check") 56 | } 57 | 58 | /* Preferences */ 59 | Map device() { 60 | logger("debug", "device()") 61 | 62 | return dynamicPage(name: "device", title: "Bridge device", nextPage:"config", install: false, uninstall: false) { 63 | section("Sonoff RF Bridge") { 64 | input name: "host_port", title: "Device IP and PORT", type: "text", defaultValue: "ip:port", required: true 65 | input name: "user", title: "User", type: "text", defaultValue: "", required: false 66 | input name: "password", title: "Password", type: "password", defaultValue: "", required: false 67 | } 68 | } 69 | } 70 | 71 | Map config() { 72 | logger("debug", "config()") 73 | 74 | return dynamicPage(name: "config", title: "Config", nextPage:"check", install: false, uninstall: false) { 75 | section("Devices") { 76 | input name: "json", title: "JSON Config", type: "text", required: true 77 | } 78 | section("Logging") { 79 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 80 | } 81 | } 82 | } 83 | 84 | Map check() { 85 | logger("debug", "check()") 86 | try { 87 | JsonSlurper slurper = new JsonSlurper() 88 | def vd_data = slurper.parseText(json) 89 | logger("debug", "check() - Successful") 90 | return dynamicPage(name: "check", title: "Check Config", install: true, uninstall: true) { 91 | section() { 92 | paragraph "Successfully checked the the config. Click Next" 93 | } 94 | } 95 | } catch (e) { 96 | logger("error", "check() - Failed, ${e}") 97 | return dynamicPage(name: "check", title: "Check Config", nextPage: "config", install: false, uninstall: false) { 98 | section() { 99 | paragraph "Unable parse the JSON Config, double check your config. Click Next" 100 | } 101 | } 102 | } 103 | } 104 | 105 | def installed() { 106 | logger("debug", "installed(${VERSION})") 107 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 108 | state.driverInfo = [ver:VERSION] 109 | } 110 | initialize() 111 | } 112 | 113 | def uninstalled() { 114 | logger("debug", "uninstalled()") 115 | 116 | unschedule() 117 | removeChildDevices(getChildDevices()) 118 | } 119 | 120 | def updated() { 121 | logger("debug", "updated() - settings: ${settings.inspect()} / InstallationState: ${app.getInstallationState()}") 122 | 123 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 124 | if (state.driverInfo == null || state.driverInfo.isEmpty()) { 125 | state.driverInfo = [ver:VERSION] 126 | } 127 | } 128 | 129 | initialize() 130 | } 131 | 132 | void initialize() { 133 | logger("debug", "initialize() - settings: ${settings.inspect()}") 134 | 135 | // Create virtual devices 136 | JsonSlurper slurper = new JsonSlurper() 137 | def vd_data = slurper.parseText(json) 138 | vd_data?.each { 139 | ChildDeviceWrapper vd = createDevices(it.key?.split(':')?.getAt(0), it.key?.split(':')?.getAt(1)) 140 | } 141 | 142 | // Cleanup any other devices that need to go away 143 | unschedule() 144 | def hub = location.hubs[0] 145 | List allInstalled = getChildDevices().collect{ it.getChildDevices().collect{ it.deviceNetworkId?.replaceAll("${hub.id}-SonoffRFBridge-","").replaceFirst("-",":") } }.flatten() 146 | List device = vd_data.keySet().toList() 147 | List delete = allInstalled.findAll{ !device.contains(it) } 148 | 149 | getChildDevices()?.each { bridge -> 150 | bridge?.getChildDevices()?.each { dev -> 151 | String devID = dev?.deviceNetworkId?.replaceAll("${hub.id}-SonoffRFBridge-","").replaceFirst("-",":") 152 | if (delete.contains(devID)) { 153 | logger("info", "Removing Device: ${dev.deviceNetworkId}") 154 | bridge.deleteChildDevice(dev.deviceNetworkId) 155 | } 156 | } 157 | } 158 | } 159 | 160 | private ChildDeviceWrapper createDevices(String type, String name) { 161 | logger("debug", "createDevices(${type},${name})") 162 | try { 163 | def hub = location.hubs[0] 164 | def cd = getChildDevice("${hub.id}-SonoffRFBridge") 165 | if (cd) { 166 | cd.addDevice([name: name, type: type]) 167 | 168 | } else { 169 | logger("info", "Creating Sonoff RF Bridge Device") 170 | cd = addChildDevice("syepes", "Sonoff RF Bridge", "${hub.id}-SonoffRFBridge", hub.id, [name: "Sonoff RF Bridge", label: "Sonoff RF Bridge", isComponent: true]) 171 | if (cd) { 172 | logger("info", "Creating Sonoff RF Device: ${name} (${type})") 173 | cd.addDevice([name: name, type: type]) 174 | } 175 | } 176 | 177 | } catch (e) { 178 | logger("error", "createDevices(${type},${name}) - e: ${e}") 179 | } 180 | } 181 | 182 | private removeChildDevices(delete) { 183 | logger("debug", "removeChildDevices() - Removing ${delete.size()} devices") 184 | delete.each { deleteChildDevice(it.deviceNetworkId) } 185 | } 186 | 187 | /** 188 | * @param level Level to log at, see LOG_LEVELS for options 189 | * @param msg Message to log 190 | */ 191 | private logger(level, msg) { 192 | if (level && msg) { 193 | Integer levelIdx = LOG_LEVELS.indexOf(level) 194 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 195 | if (setLevelIdx < 0) { 196 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 197 | } 198 | if (levelIdx <= setLevelIdx) { 199 | log."${level}" "${app.name} ${msg}" 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Apps/Sonoff/Sonoff RF Bridge.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Sonoff RF Bridge", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-10-12", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-sonoff-rf-433mhz-bridge/32959/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "apps": [ 11 | { 12 | "name": "Sonoff RF Bridge", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/Sonoff/Sonoff%20RF%20Bridge.groovy", 15 | "version": "1.0.0", 16 | "required": true, 17 | "oauth": false, 18 | "primary": true 19 | } 20 | ], 21 | "drivers": [ 22 | { 23 | "name": "Sonoff RF Bridge", 24 | "namespace": "syepes", 25 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Sonoff/Sonoff%20RF%20Bridge.groovy", 26 | "version": "1.0.0", 27 | "required": true 28 | }, 29 | { 30 | "name": "Sonoff RF Bridge - Shade", 31 | "namespace": "syepes", 32 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Sonoff/Sonoff%20RF%20Bridge%20-%20Shade.groovy", 33 | "version": "1.0.0", 34 | "required": true 35 | }, 36 | { 37 | "name": "Sonoff RF Bridge - Switch", 38 | "namespace": "syepes", 39 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Sonoff/Sonoff%20RF%20Bridge%20-%20Switch.groovy", 40 | "version": "1.0.0", 41 | "required": true 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /Apps/VictoriaMetrics/MetricLogger.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "MetricLogger", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "apps": [ 11 | { 12 | "name": "MetricLogger", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/VictoriaMetrics/MetricLogger.groovy", 15 | "version": "1.1.2", 16 | "required": true, 17 | "oauth": false, 18 | "primary": true 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /Apps/Warmup/Warmup - Cloud.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Warmup - Cloud", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-warmup-smart-6ie-cloud/100502", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "apps": [ 11 | { 12 | "name": "Warmup - Cloud", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/Warmup/Warmup%20-%20Cloud.groovy", 15 | "version": "1.0.0", 16 | "required": true, 17 | "oauth": false, 18 | "primary": true 19 | } 20 | ], 21 | "drivers": [ 22 | { 23 | "name": "Warmup - Cloud - Location", 24 | "namespace": "syepes", 25 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Warmup/Warmup%20-%20Cloud%20-%20Location.groovy", 26 | "version": "1.0.1", 27 | "required": true 28 | }, 29 | { 30 | "name": "Warmup - Cloud - Room", 31 | "namespace": "syepes", 32 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Warmup/Warmup%20-%20Cloud%20-%20Room.groovy", 33 | "version": "1.0.2", 34 | "required": true 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /Drivers/Aeotec/Aeotec Heavy Duty Smart Switch.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Aeotec Heavy Duty Smart Switch", 3 | "minimumHEVersion": "2.2.6", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-aeotec-heavy-duty-smart-switch/32951/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Aeotec Heavy Duty Smart Switch", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Aeotec/Aeotec%20Heavy%20Duty%20Smart%20Switch.groovy", 15 | "version": "1.1.6", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Aeotec/Aeotec MultiSensor 6.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Aeotec MultiSensor 6", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-aeotec-heavy-duty-smart-switch/32951/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Aeotec MultiSensor 6", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Aeotec/Aeotec%20MultiSensor%206.groovy", 15 | "version": "1.1.3", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Aeotec/Aeotec Water Sensor 6.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Aeotec Water Sensor 6", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2021-02-04", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-aeotec-water-sensor-6/34269/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Aeotec Water Sensor 6", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Aeotec/Aeotec%20Water%20Sensor%206.groovy", 15 | "version": "1.1.3", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Eurotronic/Eurotronic Air Quality Sensor.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Eurotronic Air Quality Sensor", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-eurotronic-air-quality-sensor-humidity-dewpoint-co2-voc/68482/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Eurotronic Air Quality Sensor", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Eurotronic/Eurotronic%20Air%20Quality%20Sensor.groovy", 15 | "version": "1.0.3", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Fibaro/Fibaro Smoke Sensor.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Fibaro Smoke Sensor", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-fibaro-smoke-sensor/32953/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Fibaro Smoke Sensor", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Fibaro/Fibaro%20Smoke%20Sensor.groovy", 15 | "version": "1.1.3", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Generic/Water Meter - API External.groovy: -------------------------------------------------------------------------------- 1 | 2 | import groovy.json.JsonSlurper 3 | import groovy.transform.Field 4 | 5 | @Field String VERSION = "2.0.0" 6 | 7 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 8 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 9 | 10 | metadata { 11 | definition (name: "Water Meter - API External", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Generic/Water%20Meter%20-%20API%20External.groovy") { 12 | capability "Actuator" 13 | capability "Sensor" 14 | capability "LiquidFlowRate" 15 | capability "Initialize" 16 | command "clearState" 17 | 18 | attribute "day_euro", "number" 19 | attribute "cumulative_euro", "number" 20 | attribute "day_cubic_meter", "number" 21 | attribute "cumulative_cubic_meter", "number" 22 | attribute "day_liter", "number" 23 | attribute "cumulative_liter", "number" 24 | } 25 | preferences { 26 | section { // General 27 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 28 | } 29 | } 30 | } 31 | 32 | def initialize() { 33 | logger("debug", "initialize()") 34 | 35 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 36 | state.driverInfo = [ver:VERSION] 37 | } 38 | 39 | if (state.deviceInfo == null) { 40 | state.deviceInfo = [:] 41 | } 42 | } 43 | 44 | def clearState() { 45 | logger("debug", "ClearStates() - Clearing device states") 46 | state.clear() 47 | 48 | if (state?.driverInfo == null) { 49 | state.driverInfo = [:] 50 | } else { 51 | state.driverInfo.clear() 52 | } 53 | 54 | if (state?.deviceInfo == null) { 55 | state.deviceInfo = [:] 56 | } else { 57 | state.deviceInfo.clear() 58 | } 59 | } 60 | 61 | def parse(String description) { 62 | logger("debug", "parse() - description: ${description?.inspect()}") 63 | def msg = parseDescriptionAsMap(description) 64 | logger("debug", "parse() - msg: ${msg?.inspect()}") 65 | 66 | if (msg?.body) { 67 | timestamp = msg?.body?.timestamp 68 | unit = msg?.body?.unit 69 | unit_name = msg?.body?.unit_name 70 | type = msg?.body?.index_type 71 | day = msg?.body?.index_day 72 | cumulative = msg?.body?.index_count 73 | 74 | sendEvent([name: "day_${unit_name}", value: day, unit: unit, type: type, descriptionText: timestamp, displayed: true]) 75 | sendEvent([name: "cumulative_${unit_name}", value: cumulative, unit: unit, type: type, descriptionText: timestamp, displayed: true]) 76 | 77 | // Calculate the LiquidFlowRate 78 | if (unit_name == "liter") { 79 | rate = day?.toInteger()/1440 80 | sendEvent([name: "rate", value: rate, unit: "LPM", type: "calculated", descriptionText: timestamp, displayed: true]) 81 | } 82 | 83 | state.deviceInfo.lastevent = (new Date().getTime()/1000) as long 84 | } 85 | } 86 | 87 | 88 | private parseDescriptionAsMap(description) { 89 | logger("trace", "parseDescriptionAsMap() - description: ${description.inspect()}") 90 | try { 91 | def descMap = description.split(",").inject([:]) { map, param -> 92 | def nameAndValue = param.split(":") 93 | if (nameAndValue.length == 2){ 94 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 95 | } else { 96 | map += [(nameAndValue[0].trim()):""] 97 | } 98 | } 99 | 100 | def headers = new String(descMap["headers"]?.decodeBase64()) 101 | def status_code = headers?.tokenize('\r\n')[0] 102 | headers = headers?.tokenize('\r\n')?.toList()[1..-1]?.collectEntries{ 103 | it.split(":",2).with{ [ (it[0]): (it.size()<2) ? null : it[1] ?: null ] } 104 | } 105 | 106 | def body = new String(descMap["body"]?.decodeBase64()) 107 | def body_json 108 | logger("trace", "parseDescriptionAsMap() - headers: ${headers.inspect()}, body: ${body.inspect()}") 109 | 110 | if (body && body != "") { 111 | if(body.startsWith("\"{") || body.startsWith("{") || body.startsWith("\"[") || body.startsWith("[")) { 112 | JsonSlurper slurper = new JsonSlurper() 113 | body_json = slurper.parseText(body) 114 | logger("trace", "parseDescriptionAsMap() - body_json: ${body_json}") 115 | } 116 | } 117 | 118 | return [desc: descMap.subMap(['mac','ip','port']), status_code: status_code, headers:headers, body:body_json] 119 | } catch (e) { 120 | logger("error", "parseDescriptionAsMap() - ${e.inspect()}") 121 | return [:] 122 | } 123 | } 124 | 125 | /** 126 | * @param level Level to log at, see LOG_LEVELS for options 127 | * @param msg Message to log 128 | */ 129 | private logger(level, msg) { 130 | if (level && msg) { 131 | Integer levelIdx = LOG_LEVELS.indexOf(level) 132 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 133 | if (setLevelIdx < 0) { 134 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 135 | } 136 | if (levelIdx <= setLevelIdx) { 137 | log."${level}" "${device.displayName} ${msg}" 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Drivers/Generic/Z-Wave Repeater.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Z-Wave Repeater", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Z-Wave Repeater", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Generic/Z-Wave%20Repeater.groovy", 15 | "version": "1.0.5", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Heatit/Heatit Z-Temp2.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Heatit Z-Temp2", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-heatit-z-temp-2-thermostat/49430/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Heatit Z-Temp2", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Heatit/Heatit%20Z-Temp2.groovy", 15 | "version": "1.1.5", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Heltun/Heltun Touch Panel Switch - Button.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | 17 | @Field String VERSION = "1.0.1" 18 | 19 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 20 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[2] 21 | 22 | metadata { 23 | definition (name: "Heltun Touch Panel Switch - Button", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Heltun/Heltun%20Touch%20Panel%20Switch%20-%20Button.groovy") { 24 | capability "Actuator" 25 | capability "Switch" 26 | capability "Momentary" // push 27 | capability "Pushable Button" // pushed 28 | capability "Holdable Button" // held 29 | capability "Releasable Button" // released 30 | capability "Refresh" 31 | capability "Initialize" 32 | } 33 | preferences { 34 | section { // General 35 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 36 | } 37 | } 38 | } 39 | 40 | def installed() { 41 | logger("debug", "installed(${VERSION})") 42 | 43 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 44 | state.driverInfo = [ver:VERSION] 45 | } 46 | 47 | if (state.deviceInfo == null) { 48 | state.deviceInfo = [:] 49 | } 50 | 51 | initialize() 52 | } 53 | 54 | def uninstalled() { 55 | logger("debug", "uninstalled()") 56 | unschedule() 57 | } 58 | 59 | def updated() { 60 | logger("debug", "updated()") 61 | 62 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 63 | installed() 64 | } 65 | unschedule() 66 | initialize() 67 | } 68 | 69 | def initialize() { 70 | logger("debug", "initialize()") 71 | 72 | sendEvent(name: "numberOfButtons", value: 1) 73 | schedule("0 0 12 */7 * ?", updateCheck) 74 | } 75 | 76 | def parse(value) { 77 | logger("debug", "parse() - value: ${value?.inspect()}") 78 | if (value) { 79 | sendEvent(value) 80 | } 81 | } 82 | 83 | def refresh() { 84 | logger("debug", "refresh() - state: ${state.inspect()}") 85 | parent.componentRefresh(device) 86 | } 87 | 88 | def on() { 89 | logger("debug", "on()") 90 | parent.componentOn(device) 91 | } 92 | 93 | def off() { 94 | logger("debug", "off()") 95 | parent.componentOff(device) 96 | } 97 | 98 | def push() { 99 | def btnNumber = device?.deviceNetworkId?.split('-')[1] 100 | push(btnNumber, 'digital') 101 | } 102 | 103 | def push(btnNumber) { 104 | push(btnNumber, 'digital') 105 | } 106 | 107 | def push(btnNumber, btnType) { 108 | logger("debug", "push(${btnNumber},${btnType})") 109 | sendEvent(name: "pushed", value: btnNumber, descriptionText: "Button was pushed", type: btnType, isStateChange: true) 110 | parent.componentPush(device) 111 | } 112 | 113 | def hold() { 114 | def btnNumber = device?.deviceNetworkId?.split('-')[1] 115 | hold(btnNumber, 'digital') 116 | } 117 | 118 | def hold(btnNumber) { 119 | hold(btnNumber, 'digital') 120 | } 121 | 122 | def hold(btnNumber, btnType) { 123 | logger("debug", "hold(${btnNumber},${btnType})") 124 | sendEvent(name: "hold", value: btnNumber, descriptionText: "Button was hold", type: btnType, isStateChange: true) 125 | } 126 | 127 | /** 128 | * @param level Level to log at, see LOG_LEVELS for options 129 | * @param msg Message to log 130 | */ 131 | private logger(level, msg) { 132 | if (level && msg) { 133 | Integer levelIdx = LOG_LEVELS.indexOf(level) 134 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 135 | if (setLevelIdx < 0) { 136 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 137 | } 138 | if (levelIdx <= setLevelIdx) { 139 | log."${level}" "${device.displayName} ${msg}" 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Drivers/Heltun/Heltun Touch Panel Switch - Quinto.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Heltun Touch Panel Switch - Quinto", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-heltun-touch-panel-switch/49430/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Heltun Touch Panel Switch - Quinto", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Heltun/Heltun%20Touch%20Panel%20Switch%20-%20Quinto.groovy", 15 | "version": "1.0.2", 16 | "required": true 17 | }, 18 | { 19 | "name": "Heltun Touch Panel Switch - Button", 20 | "namespace": "syepes", 21 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Heltun/Heltun%20Touch%20Panel%20Switch%20-%20Button.groovy", 22 | "version": "1.0.1", 23 | "required": true 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /Drivers/LG/LG WebOS Mouse.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * Original Authors: Sam Lalor, Andrew Stanley-Jones 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 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 12 | * for the specific language governing permissions and limitations under the License. 13 | * 14 | */ 15 | 16 | import groovy.transform.Field 17 | import groovy.json.JsonSlurper 18 | 19 | @Field String VERSION = "1.0.0" 20 | 21 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 22 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[2] 23 | @Field List = ["HOME", "BACK", "UP", "DOWN", "LEFT", "RIGHT", "RED", "BLUE", "YELLOW", "GREEN", ] 24 | @Field queue 25 | 26 | metadata { 27 | definition(name: "LG WebOS Mouse", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/LG/LG%20WebOS%20Mouse.groovy") { 28 | command "setMouseURI", ["string"] 29 | command "getURI" 30 | command "close" 31 | 32 | command "click" 33 | command "sendButton", ["string"] 34 | command "move", ["number", "number"] 35 | command "moveAbsolute", ["number", "number"] 36 | command "scroll", ["number", "number"] 37 | command "ok" 38 | command "home" 39 | command "left" 40 | command "right" 41 | command "red" 42 | command "blue" 43 | command "yellow" 44 | command "green" 45 | } 46 | preferences { 47 | section { // General 48 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 49 | } 50 | } 51 | } 52 | 53 | 54 | def updated() { 55 | logger("debug", "updated()") 56 | } 57 | 58 | def getURI() { 59 | logger("debug", "getURI()") 60 | if ((now() - (state?.refreshURIAt ?: 15001)) > 15000) { 61 | parent.getMouseURI() 62 | state.refreshURIAt = now() 63 | } 64 | } 65 | 66 | def close() { 67 | logger("debug", "close()") 68 | state.socketStatus = "closing" 69 | interfaces.webSocket.close() 70 | // Clear refreshURIAt to allow getURI to work for testing 71 | state.refreshURIAt = 0 72 | } 73 | 74 | def setMouseURI(String uri) { 75 | logger("debug", "setMouseURI() - uri: ${uri}") 76 | if ((uri != state.uri) || (state.socketStatus == "closing")) { 77 | reconnect(uri) 78 | } 79 | } 80 | 81 | def sendMessage(String msg) { 82 | logger("debug", "sendMessage() - msg: ${msg}") 83 | if (state.socketStatus == "open") { 84 | interfaces.webSocket.sendMessage(msg) 85 | } else { 86 | reconnect() 87 | if (!state.queue) { 88 | state.queue = [] 89 | } 90 | state.queue += [ [now(), msg] ] 91 | logger("debug", "sendMessage() - Queue length: ${state.queue.size()}") 92 | } 93 | } 94 | 95 | def click() { 96 | logger("debug", "click()") 97 | sendMessage("type:click\n\n") 98 | } 99 | 100 | def sendButton(String name) { 101 | logger("debug", "sendButton() - name: ${name}") 102 | sendMessage("type:button\nname:${name}\n\n") 103 | } 104 | 105 | def move(x, y) { 106 | logger("debug", "move(x:${x},y:${y})") 107 | sendMessage("type:move\ndx:${x}\ndy:${y}\ndrag: 0\n\n") 108 | } 109 | 110 | def moveAbsolute(x, y) { 111 | logger("debug", "moveAbsolute(x:${x},y:${y})") 112 | 113 | // Go to 0,0 114 | for (int i = 0; i<80; i++) { 115 | move(-10, -10) 116 | } 117 | 118 | int dx = x / 10 119 | int dy = y / 10 120 | int max = dx > dy ? dx : dy 121 | for (int i = 0; i 0 ? 10 : 0, dy > 0 ? 10 : 0) 123 | dx = dx > 0 ? dx - 1 : 0 124 | dy = dy > 0 ? dy - 1 : 0 125 | } 126 | move(dx % 10, dy % 10) 127 | } 128 | 129 | def scroll(dx, dy) { 130 | logger("debug", "scroll(dx:${dx},dy:${dy})") 131 | sendMessage("type:scroll\ndx:${dx}\ndy:${dy}\n\n") 132 | } 133 | 134 | def parse(status) { 135 | logger("debug", "parse(${status})") 136 | } 137 | 138 | def reconnect(new_uri = null) { 139 | logger("debug", "reconnect() - new uri: ${new_uri}, state: ${state.socketStatus}") 140 | if (!new_uri && (state.socketStatus != "closing")) return 141 | if (new_uri) state.uri = new_uri 142 | 143 | close() 144 | try { 145 | logger("debug", "reconnect() - Pointer Connecting to: ${state.uri}") 146 | interfaces.webSocket.connect(state.uri) 147 | state.socketStatus = "opening" 148 | } catch (e) { 149 | logger("warn", "reconnect() - Failed to open mouse socket: ${e}") 150 | } 151 | } 152 | 153 | def flushQueue() { 154 | logger("debug", "flushQueue()") 155 | 156 | if (state.socketStatus == "open" && state.queue) { 157 | logger("debug", "flushQueue() - Queue length: ${state.queue.size()}") 158 | def curQueue = state.queue 159 | state.queue = [] 160 | def flushed = 0 161 | def skipped = 0 162 | curQueue.each { it -> 163 | def queuedAt = it[0] 164 | def msg = it[1] 165 | logger("trace", "flushQueue() - Looking at: ${it} age: ${now() - queuedAt}") 166 | if ((now() - queuedAt) < 15000) { 167 | logger("trace", "flushQueue() - Sending Queued: ${msg}") 168 | sendMessage(msg) 169 | flushed++ 170 | } else { 171 | skipped++ 172 | } 173 | } 174 | logger("debug", "flushQueue() - sent ${flushed} skipped (too old): ${skipped}") 175 | } 176 | } 177 | 178 | def webSocketStatus(String status) { 179 | logger("debug", "webSocketStatus() - status: ${status}") 180 | if (status.startsWith("status:")) { 181 | state.socketStatus = status.replace("status: ", "") 182 | } else if (status.startsWith("failure:")) { 183 | state.socketStatus = "closing" 184 | } 185 | logger("debug", "webSocketStatus() - New status: ${state.socketStatus}") 186 | 187 | if (state.socketStatus == "open" && state.queue) { 188 | runInMillis(250, flushQueue) 189 | } 190 | if (state.socketStatus == "closing") { 191 | getURI() 192 | } 193 | } 194 | 195 | def ok() { 196 | logger("debug", "ok()") 197 | click() 198 | } 199 | 200 | def home() { 201 | logger("debug", "home()") 202 | sendButton("HOME") 203 | } 204 | 205 | def left() { 206 | logger("debug", "left()") 207 | sendButton("LEFT") 208 | } 209 | 210 | def right() { 211 | logger("debug", "right()") 212 | sendButton("RIGHT") 213 | } 214 | 215 | def red() { 216 | logger("debug", "red()") 217 | sendButton("RED") 218 | } 219 | 220 | def blue() { 221 | logger("debug", "blue()") 222 | sendButton("BLUE") 223 | } 224 | 225 | def yellow() { 226 | logger("debug", "yellow()") 227 | sendButton("YELLOW") 228 | } 229 | 230 | def green() { 231 | logger("debug", "green()") 232 | sendButton("GREEN") 233 | } 234 | 235 | /** 236 | * @param level Level to log at, see LOG_LEVELS for options 237 | * @param msg Message to log 238 | */ 239 | private logger(level, msg) { 240 | if (level && msg) { 241 | Integer levelIdx = LOG_LEVELS.indexOf(level) 242 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 243 | if (setLevelIdx<0) { 244 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 245 | } 246 | if (levelIdx<= setLevelIdx) { 247 | log."${level}" "${device.displayName} ${msg}" 248 | } 249 | } 250 | } -------------------------------------------------------------------------------- /Drivers/Loki/LokiLogLogger.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "LokiLogLogger", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2023-11-15", 6 | "documentationLink": "", 7 | "communityLink": "", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "LokiLogLogger", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Loki/LokiLogLogger.groovy", 15 | "version": "1.0.4", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Loki/LokiZWaveLogger.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "LokiZWaveLogger", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2020-10-28", 6 | "documentationLink": "", 7 | "communityLink": "", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "LokiZWaveLogger", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Loki/LokiZWaveLogger.groovy", 15 | "version": "1.0.3", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Loki/LokiZigbeeLogger.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "LokiZigbeeLogger", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2020-10-28", 6 | "documentationLink": "", 7 | "communityLink": "", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "LokiZigbeeLogger", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Loki/LokiZigbeeLogger.groovy", 15 | "version": "1.0.3", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Netatmo/Netatmo - Doorbell.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | import groovy.json.JsonSlurper 17 | 18 | @Field String VERSION = "1.0.3" 19 | 20 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 21 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[2] 22 | 23 | metadata { 24 | definition (name: "Netatmo - Doorbell", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Doorbell.groovy") { 25 | capability "Actuator" 26 | capability "Switch" 27 | capability "Motion Sensor" 28 | capability "Image Capture" 29 | capability "Refresh" 30 | capability "Initialize" 31 | 32 | command "motion" 33 | command "human" 34 | 35 | attribute "ring", "string" 36 | attribute "status", "string" 37 | attribute "sd_status", "string" 38 | attribute "alim_status", "string" 39 | attribute "quick_display_zone", "number" 40 | attribute "max_peers_reached", "string" 41 | attribute "websocket_connected", "string" 42 | attribute "homeName", "string" 43 | attribute "image_tag", "string" 44 | attribute "human", "string" 45 | } 46 | 47 | preferences { 48 | section { // General 49 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 50 | input name: "logDescText", title: "Log Description Text", type: "bool", defaultValue: false, required: false 51 | } 52 | section { // Snapshots 53 | input name: "doorBellIP", title: "DoorBell Local IP", description: "The address of the DoorBell in your local network", type: "text", required: true 54 | input name: "ringTimeout", title: "Ring timeout", description: "Ring Status times out after how many seconds", type: "number", range: "0..3600", defaultValue: 60, required: true 55 | input name: "motionHumans", title: "HumansAsMotion", description: "Humans detected count as motion", type: "bool", defaultValue: true, required: true 56 | input name: "motionTimeout", title: "Motion timeout", description: "Motion, Human, Vehicle and Animal detection times out after how many seconds", type: "number", range: "0..3600", defaultValue: 60, required: true 57 | input name: "scheduledTake", title: "Take a snapshot every", type: "enum", options:[[0:"No snapshots"], [2:"2min"], [5:"5min"], [10:"10min"], [15:"15min"], [30:"30min"], [2:"1h"], [3:"3h"], [4:"4h"], [6:"6h"], [8:"8h"], [12: "12h"]], defaultValue: 0, required: true 58 | } 59 | } 60 | } 61 | 62 | def installed() { 63 | logger("debug", "installed(${VERSION})") 64 | 65 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 66 | state.driverInfo = [ver:VERSION] 67 | } 68 | 69 | if (state.deviceInfo == null) { 70 | state.deviceInfo = [:] 71 | } 72 | 73 | sendEvent(name: "motion", value: "inactive") 74 | sendEvent(name: "human", value: "inactive") 75 | sendEvent(name: "ring", value: "none") 76 | initialize() 77 | } 78 | 79 | def uninstalled() { 80 | logger("debug", "uninstalled()") 81 | unschedule() 82 | } 83 | 84 | def updated() { 85 | logger("debug", "updated()") 86 | 87 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 88 | installed() 89 | } 90 | unschedule() 91 | initialize() 92 | } 93 | 94 | def initialize() { 95 | logger("debug", "initialize()") 96 | 97 | if (scheduledTake.toInteger()) { 98 | if (['2', '5', '10', '15', '30'].contains(scheduledTake) ) { 99 | schedule("0 */${scheduledTake} * ? * *", take) 100 | } else { 101 | schedule("0 0 */${scheduledTake} ? * *", take) 102 | } 103 | } 104 | } 105 | 106 | def refresh() { 107 | logger("debug", "refresh() - state: ${state.inspect()}") 108 | } 109 | 110 | def parse(String description) { 111 | logger("trace", "parse() - description: ${description?.inspect()}") 112 | return [] 113 | } 114 | 115 | def on() { 116 | logger("debug", "on()") 117 | if (logDescText) { 118 | log.info "${device.displayName} Was turned on" 119 | } else { 120 | logger("info", "Was turned on") 121 | } 122 | sendEvent(name: "switch", value: "on") 123 | } 124 | 125 | def off() { 126 | logger("debug", "off()") 127 | if (logDescText) { 128 | log.info "${device.displayName} Was turned off" 129 | } else { 130 | logger("info", "Was turned off") 131 | } 132 | sendEvent(name: "switch", value: "off") 133 | } 134 | 135 | def setHome(homeID,homeName) { 136 | logger("debug", "setHome(${homeID?.inspect()}, ${homeName?.inspect()})") 137 | state.deviceInfo['homeID'] = homeID 138 | sendEvent(name: "homeName", value: homeName) 139 | } 140 | 141 | def setAKey(key) { 142 | logger("debug", "setAKey(${key?.inspect()})") 143 | state.deviceInfo['accessKey'] = key 144 | } 145 | 146 | def ring(String type=null, String snapshot_url=null) { 147 | logger("debug", "ring(${type}, ${snapshot_url})") 148 | if (logDescText) { 149 | log.info "${device.displayName} Ring status: ${type}" 150 | } else { 151 | logger("info", "Ring status: ${type}") 152 | } 153 | 154 | sendEvent(name: "ring", value: type) 155 | if (snapshot_url != null) { 156 | sendEvent(name: "image_tag", value: '', isStateChange: true, displayed: true) 157 | } 158 | 159 | if (ringTimeout) { 160 | startTimer(ringTimeout, cancelRing) 161 | } else { 162 | logger("debug", "ring() - ring timeout has not been set in preferences, using 10 second default") 163 | startTimer(10, cancelRing) 164 | } 165 | } 166 | 167 | def cancelRing() { 168 | logger("debug", "cancelRing()") 169 | sendEvent(name: "ring", value: "none") 170 | } 171 | 172 | def human(String snapshot_url=null) { 173 | logger("debug", "human(${snapshot_url})") 174 | if (logDescText) { 175 | log.info "${device.displayName} Has detected motion (Human)" 176 | } else { 177 | logger("info", "Has detected motion (Human)") 178 | } 179 | sendEvent(name: "human", value: "active", displayed: true) 180 | if (snapshot_url != null) { 181 | sendEvent(name: "image_tag", value: '', isStateChange: true, displayed: true) 182 | } 183 | 184 | if (motionHumans) { 185 | motion(snapshot_url) 186 | } 187 | if (motionTimeout) { 188 | startTimer(motionTimeout, cancelHuman) 189 | } else { 190 | logger("debug", "human() - Motion timeout has not been set in preferences, using 10 second default") 191 | startTimer(10, cancelHuman) 192 | } 193 | } 194 | 195 | def cancelHuman() { 196 | logger("debug", "cancelHuman()") 197 | sendEvent(name: "human", value: "inactive") 198 | } 199 | 200 | def motion(String snapshot_url=null) { 201 | logger("debug", "motion(${snapshot_url})") 202 | if (logDescText) { 203 | log.info "${device.displayName} Has detected motion" 204 | } else { 205 | logger("info", "Has detected motion") 206 | } 207 | sendEvent(name: "motion", value: "active", displayed: true) 208 | if (snapshot_url != null) { 209 | sendEvent(name: "image_tag", value: '', isStateChange: true, displayed: true) 210 | } 211 | 212 | if (motionTimeout) { 213 | startTimer(motionTimeout, cancelMotion) 214 | } else { 215 | logger("debug", "motion() - Motion timeout has not been set in preferences, using 10 second default") 216 | startTimer(10, cancelMotion) 217 | } 218 | } 219 | 220 | def cancelMotion() { 221 | logger("debug", "cancelMotion()") 222 | sendEvent(name: "motion", value: "inactive") 223 | } 224 | 225 | // TODO: Needs work to actually store the image somewhere 226 | def take() { 227 | logger("debug", "take()") 228 | if (doorBellIP == null || doorBellIP == "") { 229 | logger("error", "take() - Please set camera local LAN IP") 230 | return 231 | } 232 | 233 | if (state.deviceInfo['accessKey'] == null || state.deviceInfo['accessKey'] == 'N/A') { 234 | logger("error", "take() - Please verify that the device access key is corect") 235 | return 236 | } 237 | 238 | def path = "${doorBellIP}/${state.deviceInfo['accessKey']}/live/snapshot_720.jpg" 239 | 240 | sendEvent(name: "image", value: "http://"+ path, isStateChange: true, displayed: true) 241 | sendEvent(name: "image_tag", value: '', isStateChange: true, displayed: true) 242 | } 243 | 244 | private startTimer(seconds, function) { 245 | def now = new Date() 246 | def runTime = new Date(now.getTime() + (seconds * 1000)) 247 | runOnce(runTime, function) // runIn isn't reliable, use runOnce instead 248 | } 249 | 250 | /** 251 | * @param level Level to log at, see LOG_LEVELS for options 252 | * @param msg Message to log 253 | */ 254 | private logger(level, msg) { 255 | if (level && msg) { 256 | Integer levelIdx = LOG_LEVELS.indexOf(level) 257 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 258 | if (setLevelIdx < 0) { 259 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 260 | } 261 | if (levelIdx <= setLevelIdx) { 262 | log."${level}" "${device.displayName} ${msg}" 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /Drivers/Netatmo/Netatmo - Person.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | import groovy.json.JsonSlurper 17 | 18 | @Field String VERSION = "1.0.2" 19 | 20 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 21 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[2] 22 | 23 | metadata { 24 | definition (name: "Netatmo - Person", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Person.groovy") { 25 | capability "Actuator" 26 | capability "Sensor" 27 | capability "Presence Sensor" 28 | capability "ContactSensor" 29 | capability "Refresh" 30 | 31 | command "seen" 32 | command "away" 33 | command "setAway" 34 | attribute "homeName", "string" 35 | attribute "image_tag", "string" 36 | attribute "last_seen", "string" 37 | } 38 | preferences { 39 | section { // General 40 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 41 | } 42 | } 43 | } 44 | 45 | def installed() { 46 | logger("debug", "installed(${VERSION})") 47 | 48 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 49 | state.driverInfo = [ver:VERSION] 50 | } 51 | 52 | if (state.deviceInfo == null) { 53 | state.deviceInfo = [:] 54 | } 55 | 56 | sendEvent(name: "contact", value: "open") 57 | sendEvent(name: "presence", value: "not present") 58 | initialize() 59 | } 60 | 61 | def uninstalled() { 62 | logger("debug", "uninstalled()") 63 | unschedule() 64 | } 65 | 66 | def updated() { 67 | logger("debug", "updated()") 68 | 69 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 70 | installed() 71 | } 72 | unschedule() 73 | initialize() 74 | } 75 | 76 | def initialize() { 77 | logger("debug", "initialize()") 78 | } 79 | 80 | def refresh() { 81 | logger("debug", "refresh() - state: ${state.inspect()}") 82 | } 83 | 84 | def parse(String description) { 85 | logger("trace", "parse() - description: ${description?.inspect()}") 86 | return [] 87 | } 88 | 89 | def setHome(homeID,homeName) { 90 | logger("debug", "setHome(${homeID?.inspect()}, ${homeName?.inspect()})") 91 | state.deviceInfo['homeID'] = homeID 92 | sendEvent(name: "homeName", value: homeName) 93 | } 94 | 95 | def setAway() { 96 | logger("debug", "setAway()") 97 | parent.setAway(state.deviceInfo['homeID'], device.name) 98 | } 99 | 100 | def seen(String snapshot_url = null) { 101 | logger("debug", "seen(${snapshot_url})") 102 | sendEvent(name: "presence", value: "present", displayed: true) 103 | if (snapshot_url != null) { 104 | sendEvent(name: "image_tag", value: '', isStateChange: true, displayed: true) 105 | } 106 | 107 | } 108 | 109 | private contactClose(String person){ 110 | sendEvent(name: "contact", value: "closed", displayed: true, isStateChange: true, descriptionText: "Activated by ${person}") 111 | runIn(180, "contactOpen") 112 | } 113 | 114 | private contactOpen(){ 115 | sendEvent(name: "contact", value: "open", displayed: false, isStateChange: true) 116 | } 117 | 118 | def away() { 119 | logger("debug", "away()") 120 | sendEvent(name: "presence", value: "not present") 121 | } 122 | 123 | /** 124 | * @param level Level to log at, see LOG_LEVELS for options 125 | * @param msg Message to log 126 | */ 127 | private logger(level, msg) { 128 | if (level && msg) { 129 | Integer levelIdx = LOG_LEVELS.indexOf(level) 130 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 131 | if (setLevelIdx < 0) { 132 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 133 | } 134 | if (levelIdx <= setLevelIdx) { 135 | log."${level}" "${device.displayName} ${msg}" 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Drivers/Netatmo/Netatmo - Sensor.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | import groovy.json.JsonSlurper 17 | 18 | @Field String VERSION = "1.0.1" 19 | 20 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 21 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[2] 22 | 23 | metadata { 24 | definition (name: "Netatmo - Sensor", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Sensor.groovy") { 25 | capability "Actuator" 26 | capability "Contact Sensor" 27 | capability "Motion Sensor" 28 | capability "Battery" 29 | capability "Refresh" 30 | 31 | attribute "homeName", "string" 32 | attribute "cameraName", "string" 33 | attribute "rf", "string" 34 | attribute "last_activity", "string" 35 | } 36 | preferences { 37 | section { // General 38 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 39 | input name: "motionTimeout", title: "Motion timeout", description: "Motion times out after how many seconds", type: "number", range: "0..3600", defaultValue: 60, required: true 40 | } 41 | } 42 | } 43 | 44 | def installed() { 45 | logger("debug", "installed(${VERSION})") 46 | 47 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 48 | state.driverInfo = [ver:VERSION] 49 | } 50 | 51 | if (state.deviceInfo == null) { 52 | state.deviceInfo = [:] 53 | } 54 | 55 | initialize() 56 | } 57 | 58 | def uninstalled() { 59 | logger("debug", "uninstalled()") 60 | unschedule() 61 | } 62 | 63 | def updated() { 64 | logger("debug", "updated()") 65 | 66 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 67 | installed() 68 | } 69 | unschedule() 70 | initialize() 71 | } 72 | 73 | def initialize() { 74 | logger("debug", "initialize()") 75 | } 76 | 77 | def refresh() { 78 | logger("debug", "refresh() - state: ${state.inspect()}") 79 | } 80 | 81 | def parse(String description) { 82 | logger("trace", "parse() - description: ${description?.inspect()}") 83 | return [] 84 | } 85 | 86 | def setHome(String homeID, String homeName) { 87 | logger("debug", "setHome(${homeID?.inspect()}, ${homeName?.inspect()})") 88 | state.deviceInfo['homeID'] = homeID 89 | sendEvent(name: "homeName", value: homeName) 90 | } 91 | 92 | def setCamera(String cameraID, String cameraName) { 93 | logger("debug", "setHome(${cameraID?.inspect()}, ${cameraName?.inspect()})") 94 | state.deviceInfo['cameraID'] = cameraID 95 | sendEvent(name: "cameraName", value: cameraName) 96 | } 97 | 98 | def motion(String type) { 99 | logger("debug", "motion(${type})") 100 | if (logDescText) { 101 | log.info "${device.displayName} Has detected motion (${type})" 102 | } else { 103 | logger("debug", "Has detected motion (${type})") 104 | } 105 | sendEvent(name: "motion", value: type, displayed: true) 106 | 107 | if (motionTimeout) { 108 | startTimer(motionTimeout, cancelMotion) 109 | } else { 110 | logger("debug", "motion() - Motion timeout has not been set in preferences, using 10 second default") 111 | startTimer(10, cancelMotion) 112 | } 113 | } 114 | 115 | def cancelMotion() { 116 | logger("debug", "cancelMotion()") 117 | sendEvent(name: "motion", value: "inactive") 118 | } 119 | 120 | private startTimer(seconds, function) { 121 | def now = new Date() 122 | def runTime = new Date(now.getTime() + (seconds * 1000)) 123 | runOnce(runTime, function) // runIn isn't reliable, use runOnce instead 124 | } 125 | 126 | /** 127 | * @param level Level to log at, see LOG_LEVELS for options 128 | * @param msg Message to log 129 | */ 130 | private logger(level, msg) { 131 | if (level && msg) { 132 | Integer levelIdx = LOG_LEVELS.indexOf(level) 133 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 134 | if (setLevelIdx < 0) { 135 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 136 | } 137 | if (levelIdx <= setLevelIdx) { 138 | log."${level}" "${device.displayName} ${msg}" 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Drivers/Netatmo/Netatmo - Smoke Alarm.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | import groovy.json.JsonSlurper 17 | 18 | @Field String VERSION = "1.0.2" 19 | 20 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 21 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[2] 22 | 23 | metadata { 24 | definition (name: "Netatmo - Smoke Alarm", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Smoke%20Alarm.groovy") { 25 | capability "Actuator" 26 | capability "SmokeDetector" 27 | capability "Battery" 28 | capability "Refresh" 29 | 30 | attribute "homeName", "string" 31 | } 32 | preferences { 33 | section { // General 34 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 35 | } 36 | } 37 | } 38 | 39 | def installed() { 40 | logger("debug", "installed(${VERSION})") 41 | 42 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 43 | state.driverInfo = [ver:VERSION] 44 | } 45 | 46 | if (state.deviceInfo == null) { 47 | state.deviceInfo = [:] 48 | } 49 | 50 | sendEvent(name: "smoke", value: "clear") 51 | initialize() 52 | } 53 | 54 | def uninstalled() { 55 | logger("debug", "uninstalled()") 56 | unschedule() 57 | } 58 | 59 | def updated() { 60 | logger("debug", "updated()") 61 | 62 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 63 | installed() 64 | } 65 | unschedule() 66 | initialize() 67 | } 68 | 69 | def initialize() { 70 | logger("debug", "initialize()") 71 | } 72 | 73 | def refresh() { 74 | logger("debug", "refresh() - state: ${state.inspect()}") 75 | } 76 | 77 | def parse(String description) { 78 | logger("trace", "parse() - description: ${description?.inspect()}") 79 | return [] 80 | } 81 | 82 | def setHome(String homeID, String homeName) { 83 | logger("debug", "setHome(${homeID?.inspect()}, ${homeName?.inspect()})") 84 | state.deviceInfo['homeID'] = homeID 85 | sendEvent(name: "homeName", value: homeName) 86 | } 87 | 88 | def smoke(String type) { 89 | logger("debug", "smoke(${type})") 90 | if (logDescText) { 91 | log.info "${device.displayName} Has ${type == 0 ? 'cleared' : 'detected'} Smoke Alarm" 92 | } else { 93 | logger("debug", "Has ${type == 0 ? 'cleared' : 'detected'} Smoke Alarm") 94 | } 95 | sendEvent(name: "smoke", value: type == 0 ? 'clear' : 'detected', displayed: true) 96 | } 97 | 98 | def battery(String type) { 99 | logger("debug", "battery(${type})") 100 | if (logDescText) { 101 | log.info "${device.displayName} Battery is ${type == 0 ? 'low' : 'very low'}" 102 | } else { 103 | logger("debug", "Battery is ${type == 0 ? 'low' : 'very low'}") 104 | } 105 | sendEvent(name: "battery", value: type == 0 ? '50' : '5', descriptionText: type == 0 ? 'low' : 'very low', displayed: true) 106 | } 107 | 108 | /** 109 | * @param level Level to log at, see LOG_LEVELS for options 110 | * @param msg Message to log 111 | */ 112 | private logger(level, msg) { 113 | if (level && msg) { 114 | Integer levelIdx = LOG_LEVELS.indexOf(level) 115 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 116 | if (setLevelIdx < 0) { 117 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 118 | } 119 | if (levelIdx <= setLevelIdx) { 120 | log."${level}" "${device.displayName} ${msg}" 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Drivers/Netatmo/Netatmo - Velux - Blind.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | 17 | @Field String VERSION = "1.0.0" 18 | 19 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 20 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 21 | 22 | metadata { 23 | definition (name: "Netatmo - Velux - Blind", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Blind.groovy") { 24 | capability "Actuator" 25 | capability "Refresh" 26 | capability "Switch" 27 | capability "WindowShade" 28 | command "stop" 29 | 30 | attribute "id", "string" 31 | attribute "type", "string" 32 | attribute "velux_type", "string" 33 | attribute "manufacturer", "string" 34 | attribute "bridge", "string" 35 | attribute "group_id", "number" 36 | attribute "homeName", "string" 37 | attribute "roomName", "string" 38 | 39 | attribute "reachable", "string" 40 | attribute "battery_state", "string" 41 | attribute "last_seen", "number" 42 | attribute "firmware_revision", "number" 43 | attribute "current_position", "number" 44 | attribute "target_position", "number" 45 | attribute "mode", "string" 46 | attribute "silent", "string" 47 | } 48 | preferences { 49 | section { // General 50 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 51 | input name: "logDescText", title: "Log Description Text", type: "bool", defaultValue: true, required: false 52 | } 53 | } 54 | } 55 | 56 | def installed() { 57 | logger("debug", "installed(${VERSION})") 58 | 59 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 60 | state.driverInfo = [ver:VERSION] 61 | } 62 | 63 | if (state.deviceInfo == null) { 64 | state.deviceInfo = [:] 65 | } 66 | 67 | initialize() 68 | } 69 | 70 | def uninstalled() { 71 | logger("debug", "uninstalled()") 72 | unschedule() 73 | } 74 | 75 | def updated() { 76 | logger("debug", "updated()") 77 | 78 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 79 | installed() 80 | } 81 | unschedule() 82 | initialize() 83 | } 84 | 85 | def initialize() { 86 | logger("debug", "initialize()") 87 | } 88 | 89 | def refresh() { 90 | logger("debug", "refresh() - state: ${state.inspect()}") 91 | def home = parent?.getParent()?.getParent() 92 | home.checkState(state.deviceInfo.homeID) 93 | } 94 | 95 | def parse(value) { 96 | logger("debug", "parse() - value: ${value?.inspect()}") 97 | if (value) { 98 | sendEvent(value) 99 | } 100 | } 101 | 102 | def close() { 103 | logger("debug", "close()") 104 | sendEvent([name: "windowShade", value: "closing", displayed: true]) 105 | setPosition(0) 106 | } 107 | def off() { 108 | logger("debug", "off()") 109 | close() 110 | } 111 | 112 | def open() { 113 | logger("debug", "open()") 114 | sendEvent(name: "windowShade", value: "opening", displayed: true) 115 | setPosition(100) 116 | } 117 | def on() { 118 | logger("debug", "on()") 119 | open() 120 | } 121 | 122 | def startPositionChange(value) { 123 | logger("debug", "startPositionChange(${value})") 124 | 125 | switch (value) { 126 | case "close": 127 | close() 128 | return 129 | case "open": 130 | open() 131 | return 132 | default: 133 | logger("error", "startPositionChange(${value}) - Unsupported state") 134 | } 135 | } 136 | 137 | def setPosition(BigDecimal value) { 138 | logger("debug", "setPosition(${value})") 139 | 140 | try { 141 | def app = parent?.getParent()?.getParent() 142 | String auth = app.state.authToken 143 | 144 | Map params = [ 145 | uri: "https://app.velux-active.com/syncapi/v1/setstate", 146 | headers: ["Authorization": "Bearer ${auth}"], 147 | body: ["app_type": "app_velux", "home":["id": state.deviceInfo.homeID, "modules":[["bridge": state.deviceInfo.bridge, "id": state.deviceInfo.id, "target_position": value]]]], 148 | timeout: 15 149 | ] 150 | 151 | logger("trace", "setPosition() - PARAMS: ${params.inspect()}") 152 | httpPostJson(params) { resp -> 153 | logger("trace", "setPosition() - respStatus: ${resp?.getStatus()}, respHeaders: ${resp?.getAllHeaders()?.inspect()}, respData: ${resp?.getData()}") 154 | logger("debug", "setPosition() - respStatus: ${resp?.getStatus()}, respData: ${resp?.getData()}") 155 | if (resp && resp.getStatus() == 200 && resp?.getData()?.body?.errors == null) { 156 | if (logDescText) { 157 | log.info "${device.displayName} Setting Position = ${value}" 158 | } else { 159 | logger("info", "setPosition() - Setting Position = ${value}") 160 | } 161 | } else { 162 | logger("error", "setPosition() - Failed: ${resp?.getData()?.body?.errors}") 163 | } 164 | } 165 | pauseExecution(2000) 166 | refresh() 167 | } catch (Exception e) { 168 | logger("error", "setPosition() - Request Exception: ${e.inspect()}") 169 | } 170 | } 171 | 172 | def stop() { 173 | logger("debug", "stop()") 174 | try { 175 | def app = parent?.getParent()?.getParent() 176 | String auth = app.state.authToken 177 | 178 | Map params = [ 179 | uri: "https://app.velux-active.com/syncapi/v1/setstate", 180 | headers: ["Authorization": "Bearer ${auth}"], 181 | body: ["app_type": "app_velux", "home":["id": state.deviceInfo.homeID, "modules":[["id": state.deviceInfo.bridge, "stop_movements": "all"]]]], 182 | timeout: 15 183 | ] 184 | 185 | logger("trace", "stop() - PARAMS: ${params.inspect()}") 186 | httpPostJson(params) { resp -> 187 | logger("trace", "stop() - respStatus: ${resp?.getStatus()}, respHeaders: ${resp?.getAllHeaders()?.inspect()}, respData: ${resp?.getData()}") 188 | logger("debug", "stop() - respStatus: ${resp?.getStatus()}, respData: ${resp?.getData()}") 189 | if (resp && resp.getStatus() == 200 && resp?.getData()?.body?.errors == null) { 190 | if (logDescText) { 191 | log.info "${device.displayName} Stopping all movements" 192 | } else { 193 | logger("info", "stop() - Stopping all movements") 194 | } 195 | } else { 196 | logger("error", "stop() - Failed: ${resp?.getData()?.body?.errors}") 197 | } 198 | } 199 | } catch (Exception e) { 200 | logger("error", "stop() - Request Exception: ${e.inspect()}") 201 | } 202 | } 203 | 204 | def stopPositionChange() { 205 | logger("debug", "stopPositionChange()") 206 | stop() 207 | } 208 | 209 | def setDetails(Map detail) { 210 | logger("debug", "setDetails(${detail?.inspect()})") 211 | state.deviceInfo['homeID'] = detail.homeID 212 | if (detail?.roomID) { state.deviceInfo['roomID'] = detail.roomID } 213 | if (detail?.id) { state.deviceInfo['id'] = detail.id } 214 | state.deviceInfo['bridge'] = detail.bridge 215 | sendEvent(name: "homeName", value: detail.homeName) 216 | sendEvent(name: "bridge", value: detail.bridge) 217 | } 218 | 219 | def setStates(Map states) { 220 | logger("debug", "setStates(${states?.inspect()})") 221 | states?.each { k, v -> 222 | String cv = device.currentValue(k) 223 | boolean isStateChange = (cv?.toString() != v?.toString()) ? true : false 224 | if (isStateChange) { 225 | logger("debug", "setStates() - Value change: ${k} = ${cv} != ${v}") 226 | } 227 | sendEvent(name: "${k}", value: "${v}", displayed: true, isStateChange: isStateChange) 228 | 229 | if (k == "current_position") { 230 | sendEvent(name: "position", value: v, displayed: true, isStateChange: isStateChange) 231 | if (v == 0) { 232 | if (isStateChange) { 233 | if (logDescText) { 234 | log.info "${device.displayName} Was closed" 235 | } else { 236 | logger("info", "setStates() - Was closed") 237 | } 238 | } 239 | sendEvent(name: "windowShade", value: "closed", displayed: true, isStateChange: isStateChange) 240 | sendEvent(name: "switch", value: "off", displayed: true) 241 | } else if (v == 100 ) { 242 | if (isStateChange) { 243 | if (logDescText) { 244 | log.info "${device.displayName} Is open" 245 | } else { 246 | logger("info", "setStates() - Is open") 247 | } 248 | } 249 | sendEvent(name: "windowShade", value: "open", displayed: true, isStateChange: isStateChange) 250 | sendEvent(name: "switch", value: "on", displayed: true) 251 | } else { 252 | if (isStateChange) { 253 | if (logDescText) { 254 | log.info "${device.displayName} Is partially open" 255 | } else { 256 | logger("info", "setStates() - Is partially open") 257 | } 258 | } 259 | sendEvent(name: "windowShade", value: "partially open", displayed: true, isStateChange: isStateChange) 260 | sendEvent(name: "switch", value: "on", displayed: true) 261 | } 262 | } 263 | if (k == "reachable" && v == "false") { 264 | logger("warn", "Device is not reachable") 265 | } 266 | } 267 | } 268 | 269 | /** 270 | * @param level Level to log at, see LOG_LEVELS for options 271 | * @param msg Message to log 272 | */ 273 | private logger(level, msg) { 274 | if (level && msg) { 275 | Integer levelIdx = LOG_LEVELS.indexOf(level) 276 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 277 | if (setLevelIdx < 0) { 278 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 279 | } 280 | if (levelIdx <= setLevelIdx) { 281 | log."${level}" "${device.displayName} ${msg}" 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /Drivers/Netatmo/Netatmo - Velux - Departure switch.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | 17 | @Field String VERSION = "1.0.1" 18 | 19 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 20 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 21 | 22 | metadata { 23 | definition (name: "Netatmo - Velux - Departure switch", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Departure%20switch.groovy") { 24 | capability "Actuator" 25 | capability "Refresh" 26 | capability "Battery" 27 | 28 | attribute "id", "string" 29 | attribute "type", "string" 30 | attribute "velux_type", "string" 31 | attribute "bridge", "string" 32 | attribute "homeName", "string" 33 | attribute "roomName", "string" 34 | 35 | attribute "firmware_revision", "number" 36 | attribute "reachable", "string" 37 | attribute "rf_strength", "number" 38 | attribute "rf_state", "string" 39 | attribute "battery_state", "string" 40 | attribute "battery_percent", "number" 41 | attribute "battery_level", "number" 42 | attribute "last_seen", "number" 43 | } 44 | preferences { 45 | section { // General 46 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 47 | input name: "logDescText", title: "Log Description Text", type: "bool", defaultValue: false, required: false 48 | } 49 | } 50 | } 51 | 52 | def installed() { 53 | logger("debug", "installed(${VERSION})") 54 | 55 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 56 | state.driverInfo = [ver:VERSION] 57 | } 58 | 59 | if (state.deviceInfo == null) { 60 | state.deviceInfo = [:] 61 | } 62 | 63 | initialize() 64 | } 65 | 66 | def uninstalled() { 67 | logger("debug", "uninstalled()") 68 | unschedule() 69 | } 70 | 71 | def updated() { 72 | logger("debug", "updated()") 73 | 74 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 75 | installed() 76 | } 77 | unschedule() 78 | initialize() 79 | } 80 | 81 | def initialize() { 82 | logger("debug", "initialize()") 83 | } 84 | 85 | def refresh() { 86 | logger("debug", "refresh() - state: ${state.inspect()}") 87 | def home = parent?.getParent() 88 | home.checkState(state.deviceInfo.homeID) 89 | } 90 | 91 | def parse(value) { 92 | logger("debug", "parse() - value: ${value?.inspect()}") 93 | if (value) { 94 | sendEvent(value) 95 | } 96 | } 97 | 98 | def setDetails(Map detail) { 99 | logger("debug", "setDetails(${detail?.inspect()})") 100 | state.deviceInfo['velux_type'] = "departure_switch" 101 | state.deviceInfo['homeID'] = detail.homeID 102 | if (detail?.roomID) { state.deviceInfo['roomID'] = detail.roomID } 103 | state.deviceInfo['bridge'] = detail.bridge 104 | sendEvent(name: "velux_type", value: "departure_switch") 105 | sendEvent(name: "homeName", value: detail.homeName) 106 | sendEvent(name: "bridge", value: detail.bridge) 107 | } 108 | 109 | def setStates(Map states) { 110 | logger("debug", "setStates(${states?.inspect()})") 111 | states?.each { k, v -> 112 | String cv = device.currentValue(k) 113 | boolean isStateChange = (cv?.toString() != v?.toString()) ? true : false 114 | if (isStateChange) { 115 | if (logDescText && k != "last_seen") { 116 | log.info "${device.displayName} Value change: ${k} = ${cv} != ${v}" 117 | } else { 118 | logger("debug", "setStates() - Value change: ${k} = ${cv} != ${v}") 119 | } 120 | } 121 | sendEvent(name: "${k}", value: "${v}", displayed: true, isStateChange: isStateChange) 122 | if (k == "battery_percent") { 123 | sendEvent(name: "battery", value: v, displayed: true, isStateChange: isStateChange) 124 | } 125 | if (k == "reachable" && v == "false") { 126 | logger("warn", "Device is not reachable") 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * @param level Level to log at, see LOG_LEVELS for options 133 | * @param msg Message to log 134 | */ 135 | private logger(level, msg) { 136 | if (level && msg) { 137 | Integer levelIdx = LOG_LEVELS.indexOf(level) 138 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 139 | if (setLevelIdx < 0) { 140 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 141 | } 142 | if (levelIdx <= setLevelIdx) { 143 | log."${level}" "${device.displayName} ${msg}" 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Drivers/Netatmo/Netatmo - Velux - Home.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | import groovy.json.JsonSlurper 17 | import com.hubitat.app.ChildDeviceWrapper 18 | 19 | @Field String VERSION = "1.0.1" 20 | 21 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 22 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 23 | 24 | metadata { 25 | definition (name: "Netatmo - Velux - Home", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Home.groovy") { 26 | capability "Actuator" 27 | capability "Refresh" 28 | 29 | command "open", [[name:"velux_type", type: "ENUM", description: "mode", constraints: ["all","window","shutter","blind"]]] 30 | command "close", [[name:"velux_type", type: "ENUM", description: "mode", constraints: ["all","window","shutter","blind"]]] 31 | command "stop", [[name:"velux_type", type: "ENUM", description: "mode", constraints: ["all","window","shutter","blind"]]] 32 | command "setPosition", [[name:"velux_type", type: "ENUM", description: "mode", constraints: ["all","window","shutter","blind"]], [name:"position", type: "NUMBER", description: ""]] 33 | command "setScenario", [[name:"scenario_type", type: "ENUM", description: "scenario", constraints: ["wake_up","bedtime","away", "home", "away+bedtime", "home+wake_up"]]] 34 | 35 | attribute "city", "string" 36 | attribute "place_improved", "enum", ["false","true"] 37 | attribute "trust_location", "enum", ["false","true"] 38 | attribute "therm_absence_notification", "enum", ["false","true"] 39 | attribute "therm_absence_autoaway", "enum", ["false","true"] 40 | } 41 | preferences { 42 | section { // General 43 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 44 | input name: "logDescText", title: "Log Description Text", type: "bool", defaultValue: false, required: false 45 | } 46 | } 47 | } 48 | 49 | def installed() { 50 | logger("debug", "installed(${VERSION})") 51 | 52 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 53 | state.driverInfo = [ver:VERSION] 54 | } 55 | 56 | if (state.deviceInfo == null) { 57 | state.deviceInfo = [:] 58 | } 59 | 60 | initialize() 61 | } 62 | 63 | def uninstalled() { 64 | logger("debug", "uninstalled()") 65 | unschedule() 66 | } 67 | 68 | def updated() { 69 | logger("debug", "updated()") 70 | 71 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 72 | installed() 73 | } 74 | unschedule() 75 | initialize() 76 | } 77 | 78 | def initialize() { 79 | logger("debug", "initialize()") 80 | } 81 | 82 | def refresh() { 83 | logger("debug", "refresh() - state: ${state.inspect()}") 84 | parent.checkState(state.deviceInfo.homeID) 85 | } 86 | 87 | def parse(String description) { 88 | logger("trace", "parse() - description: ${description?.inspect()}") 89 | return [] 90 | } 91 | 92 | def open(String velux_type="all") { 93 | logger("debug", "open(${velux_type?.inspect()})") 94 | getChildDevices()?.each { 95 | String type = it?.currentValue("velux_type") 96 | if (type =~ /room/) { 97 | it.open(velux_type) 98 | } 99 | } 100 | } 101 | 102 | def close(String velux_type="all") { 103 | logger("debug", "close(${velux_type?.inspect()})") 104 | getChildDevices()?.each { 105 | String type = it?.currentValue("velux_type") 106 | if (type =~ /room/) { 107 | it.close(velux_type) 108 | } 109 | } 110 | } 111 | 112 | def stop(String velux_type="all") { 113 | logger("debug", "stop(${velux_type?.inspect()})") 114 | getChildDevices()?.each { 115 | String type = it?.currentValue("velux_type") 116 | if (type =~ /room/) { 117 | it.stop(velux_type) 118 | } 119 | } 120 | } 121 | 122 | def setPosition(String velux_type="all", BigDecimal position) { 123 | logger("debug", "osetPositionpen(${velux_type?.inspect()},${position?.inspect()})") 124 | getChildDevices()?.each { 125 | String type = it?.currentValue("velux_type") 126 | if (type =~ /room/) { 127 | it.setPosition(velux_type, position) 128 | } 129 | } 130 | } 131 | 132 | def setScenario(String scenario_type="away+bedtime") { 133 | logger("debug", "setScenario(${scenario_type?.inspect()})") 134 | switch (scenario_type) { 135 | case 'away+bedtime': 136 | getChildDevices()?.each { 137 | String type = it?.currentValue("velux_type") 138 | if (type =~ /gateway/) { 139 | it.setScenario('away') 140 | pauseExecution(2000) 141 | it.setScenario('bedtime') 142 | } 143 | } 144 | break 145 | case 'home': 146 | getChildDevices()?.each { 147 | String type = it?.currentValue("velux_type") 148 | if (type =~ /gateway/) { 149 | it.setScenarioWithPin(scenario_type) 150 | } 151 | } 152 | break 153 | case 'home+wake_up': 154 | getChildDevices()?.each { 155 | String type = it?.currentValue("velux_type") 156 | if (type =~ /gateway/) { 157 | it.setScenarioWithPin('home') 158 | pauseExecution(2000) 159 | it.setScenario('wake_up') 160 | } 161 | } 162 | break 163 | default: 164 | getChildDevices()?.each { 165 | String type = it?.currentValue("velux_type") 166 | if (type =~ /gateway/) { 167 | it.setScenario(scenario_type) 168 | } 169 | } 170 | break 171 | } 172 | } 173 | 174 | def setDetails(Map detail) { 175 | logger("debug", "setDetails(${detail?.inspect()})") 176 | state.deviceInfo['homeID'] = detail.id 177 | sendEvent(name: "city", value: detail.city) 178 | sendEvent(name: "place_improved", value: detail.place_improved) 179 | sendEvent(name: "trust_location", value: detail.trust_location) 180 | sendEvent(name: "therm_absence_notification", value: detail.therm_absence_notification) 181 | sendEvent(name: "therm_absence_autoaway", value: detail.therm_absence_autoaway) 182 | } 183 | 184 | def setStates(Map states) { 185 | logger("debug", "setStates(${states?.inspect()})") 186 | states?.each { k, v -> 187 | String cv = device.currentValue(k) 188 | boolean isStateChange = (cv?.toString() != v?.toString()) ? true : false 189 | if (isStateChange) { 190 | if (logDescText) { 191 | log.info "${device.displayName} Value change: ${k} = ${cv} != ${v}" 192 | } else { 193 | logger("debug", "setStates() - Value change: ${k} = ${cv} != ${v}") 194 | } 195 | } 196 | sendEvent(name: "${k}", value: "${v}", displayed: true, isStateChange: isStateChange) 197 | } 198 | } 199 | 200 | ChildDeviceWrapper addModule(Map detail) { 201 | logger("debug", "addModule(${detail?.inspect()})") 202 | 203 | try { 204 | ChildDeviceWrapper cd = getChildDevice("${device.deviceNetworkId}-${detail?.id}") 205 | if(!cd) { 206 | logger("debug", "addModule() - Creating Module (${detail.inspect()}") 207 | ChildDeviceWrapper cdm = addChildDevice("syepes", "Netatmo - Velux - ${detail?.typeName}", "${device.deviceNetworkId}-${detail?.id}", [name: "${detail?.name} (${detail?.typeName})", label: "${detail?.name} (${detail?.typeName})", isComponent: true]) 208 | cdm.setDetails(detail) 209 | return cdm 210 | } else { 211 | logger("debug", "addModule() - Module: (${device.deviceNetworkId}-${detail?.id}) already exists") 212 | return cd 213 | } 214 | } catch (e) { 215 | logger("error", "addModule() - Module creation Exception: ${e.inspect()}") 216 | return null 217 | } 218 | } 219 | 220 | ChildDeviceWrapper addRoom(Map detail) { 221 | logger("debug", "addRoom(${detail?.inspect()})") 222 | 223 | try { 224 | ChildDeviceWrapper cd = getChildDevice("${device.deviceNetworkId}-${detail?.id}") 225 | if(!cd) { 226 | logger("debug", "addRoom() - Creating Room (${detail.inspect()}") 227 | ChildDeviceWrapper cdm = addChildDevice("syepes", "Netatmo - Velux - Room", "${device.deviceNetworkId}-${detail?.id}", [name: "${detail?.name} (${detail?.type})", label: "${detail?.name} (${detail?.type})", isComponent: true]) 228 | cdm.setDetails(detail) 229 | return cdm 230 | } else { 231 | logger("debug", "addRoom() - Room: ${device.name} (${device.deviceNetworkId}-${detail?.id}) already exists") 232 | cd.setDetails(detail) 233 | return cd 234 | } 235 | } catch (e) { 236 | logger("error", "addRoom() - Room creation Exception: ${e.inspect()}") 237 | return null 238 | } 239 | } 240 | 241 | /** 242 | * @param level Level to log at, see LOG_LEVELS for options 243 | * @param msg Message to log 244 | */ 245 | private logger(level, msg) { 246 | if (level && msg) { 247 | Integer levelIdx = LOG_LEVELS.indexOf(level) 248 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 249 | if (setLevelIdx < 0) { 250 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 251 | } 252 | if (levelIdx <= setLevelIdx) { 253 | log."${level}" "${device.displayName} ${msg}" 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /Drivers/Netatmo/Netatmo - Velux - Room.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | import groovy.json.JsonSlurper 17 | import com.hubitat.app.ChildDeviceWrapper 18 | 19 | @Field String VERSION = "1.0.1" 20 | 21 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 22 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 23 | 24 | metadata { 25 | definition (name: "Netatmo - Velux - Room", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Room.groovy") { 26 | capability "Actuator" 27 | capability "Refresh" 28 | capability "Sensor" 29 | capability "Illuminance Measurement" 30 | capability "Temperature Measurement" 31 | capability "Relative Humidity Measurement" 32 | capability "Carbon Dioxide Measurement" 33 | capability "AirQuality" 34 | 35 | command "open", [[name:"velux_type", type: "ENUM", description: "mode", constraints: ["all","window","shutter","blind"]]] 36 | command "close", [[name:"velux_type", type: "ENUM", description: "mode", constraints: ["all","window","shutter","blind"]]] 37 | command "stop", [[name:"velux_type", type: "ENUM", description: "mode", constraints: ["all","window","shutter","blind"]]] 38 | command "setPosition", [[name:"velux_type", type: "ENUM", description: "mode", constraints: ["all","window","shutter","blind"]], [name:"position", type: "NUMBER", description: ""]] 39 | 40 | attribute "id", "string" 41 | attribute "velux_type", "string" 42 | attribute "homeName", "string" 43 | attribute "lux", "number" 44 | attribute "co2", "number" 45 | attribute "air_quality", "number" 46 | attribute "airQualityIndex-Level", "enum", ["Healthy","Fine","Fair","Poor","Unhealthy"] 47 | attribute "algo_status", "number" 48 | attribute "auto_close_ts", "number" 49 | 50 | attribute "max_comfort_co2", "number" 51 | attribute "max_comfort_humidity", "number" 52 | attribute "max_comfort_temperature", "number" 53 | attribute "min_comfort_humidity", "number" 54 | attribute "min_comfort_temperature", "number" 55 | } 56 | preferences { 57 | section { // General 58 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 59 | input name: "logDescText", title: "Log Description Text", type: "bool", defaultValue: false, required: false 60 | } 61 | } 62 | } 63 | 64 | def installed() { 65 | logger("debug", "installed(${VERSION})") 66 | 67 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 68 | state.driverInfo = [ver:VERSION] 69 | } 70 | 71 | if (state.deviceInfo == null) { 72 | state.deviceInfo = [:] 73 | } 74 | 75 | initialize() 76 | } 77 | 78 | def uninstalled() { 79 | logger("debug", "uninstalled()") 80 | unschedule() 81 | } 82 | 83 | def updated() { 84 | logger("debug", "updated()") 85 | 86 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 87 | installed() 88 | } 89 | unschedule() 90 | initialize() 91 | } 92 | 93 | def initialize() { 94 | logger("debug", "initialize()") 95 | } 96 | 97 | def refresh() { 98 | logger("debug", "refresh() - state: ${state.inspect()}") 99 | def home = parent?.getParent() 100 | home.checkState(state.deviceInfo.homeID) 101 | } 102 | 103 | def parse(String description) { 104 | logger("trace", "parse() - description: ${description?.inspect()}") 105 | return [] 106 | } 107 | 108 | def close(String velux_type="all") { 109 | getChildDevices()?.each { 110 | String type = it?.currentValue("velux_type") 111 | if (velux_type == "all" && type =~ /shutter|window|blind/) { 112 | it.close() 113 | } else if (velux_type == type) { 114 | it.close() 115 | } 116 | } 117 | } 118 | 119 | def open(String velux_type="all") { 120 | getChildDevices()?.each { 121 | String type = it?.currentValue("velux_type") 122 | if (velux_type == "all" && type =~ /shutter|window|blind/) { 123 | it.open() 124 | } else if (velux_type == type) { 125 | it.open() 126 | } 127 | } 128 | } 129 | 130 | def stop(String velux_type="all") { 131 | getChildDevices()?.each { 132 | String type = it?.currentValue("velux_type") 133 | if (velux_type == "all" && type =~ /shutter|window|blind/) { 134 | it.stop() 135 | } else if (velux_type == type) { 136 | it.stop() 137 | } 138 | } 139 | } 140 | 141 | def setPosition(String velux_type="all", BigDecimal position) { 142 | getChildDevices()?.each { 143 | String type = it?.currentValue("velux_type") 144 | if (velux_type == "all" && type =~ /shutter|window|blind/) { 145 | it.setPosition(position) 146 | } else if (velux_type == type) { 147 | it.setPosition(position) 148 | } 149 | } 150 | } 151 | 152 | def setDetails(Map detail) { 153 | logger("debug", "setDetails(${detail?.inspect()})") 154 | state.deviceInfo['velux_type'] = "room" 155 | state.deviceInfo['homeID'] = detail.homeID 156 | state.deviceInfo['roomID'] = detail.id 157 | sendEvent(name: "velux_type", value: "room") 158 | sendEvent(name: "homeName", value: detail.homeName) 159 | } 160 | 161 | def setStates(Map states) { 162 | logger("debug", "setStates(${states?.inspect()})") 163 | states?.each { k, v -> 164 | String cv = device.currentValue(k) 165 | boolean isStateChange = (cv?.toString() != v?.toString()) ? true : false 166 | if (isStateChange) { 167 | if (logDescText) { 168 | log.info "${device.displayName} Value change: ${k} = ${cv} != ${v}" 169 | } else { 170 | logger("debug", "setStates() - Value change: ${k} = ${cv} != ${v}") 171 | } 172 | } 173 | 174 | if (k == "lux") { 175 | sendEvent(name: "illuminance", value: v, displayed: true, isStateChange: isStateChange) 176 | } 177 | if (k ==~ /.*temperature/) { 178 | if (k == "temperature") { 179 | sendEvent(name: "temperature", value: (Float.parseFloat("${v}") / 10), displayed: true, isStateChange: isStateChange) 180 | } else { 181 | sendEvent(name: "${k}", value: (Float.parseFloat("${v}") / 10), displayed: true, isStateChange: isStateChange) 182 | } 183 | return 184 | } 185 | if (k == "co2") { 186 | sendEvent(name: "carbonDioxide", value: v, displayed: true, isStateChange: isStateChange) 187 | } 188 | if (k == "air_quality") { 189 | String air_quality_level = "Unknown" 190 | switch (v) { 191 | case "0": 192 | air_quality_level = "Healthy" 193 | break 194 | case "1": 195 | air_quality_level = "Fine" 196 | break 197 | case "2": 198 | air_quality_level = "Fair" 199 | break 200 | case "3": 201 | air_quality_level = "Poor" 202 | break 203 | case "4": 204 | air_quality_level = "Unhealthy" 205 | break 206 | } 207 | sendEvent(name: "airQualityIndex", value: v, displayed: true, isStateChange: isStateChange) 208 | sendEvent(name: "airQualityIndex-Level", value: air_quality_level, displayed: true, isStateChange: isStateChange) 209 | } 210 | 211 | sendEvent(name: "${k}", value: "${v}", displayed: true, isStateChange: isStateChange) 212 | } 213 | } 214 | 215 | ChildDeviceWrapper addModule(Map detail) { 216 | logger("debug", "addModule(${detail?.inspect()})") 217 | 218 | try { 219 | ChildDeviceWrapper cd = getChildDevice("${device.deviceNetworkId}-${detail?.id}") 220 | if(!cd) { 221 | logger("debug", "addModule() - Creating Module (${detail.inspect()}") 222 | ChildDeviceWrapper cdm = addChildDevice("syepes", "Netatmo - Velux - ${detail?.typeName}", "${device.deviceNetworkId}-${detail?.id}", [name: "${detail?.name} (${detail?.typeName})", label: "${detail?.name} (${detail?.typeName})", isComponent: true]) 223 | cdm.setDetails(detail) 224 | return cdm 225 | } else { 226 | logger("debug", "addModule() - Module: (${device.deviceNetworkId}-${detail?.id}) already exists") 227 | cd.setDetails(detail) 228 | return cd 229 | } 230 | } catch (e) { 231 | logger("error", "addModule() - Module creation Exception: ${e.inspect()}") 232 | return null 233 | } 234 | } 235 | 236 | /** 237 | * @param level Level to log at, see LOG_LEVELS for options 238 | * @param msg Message to log 239 | */ 240 | private logger(level, msg) { 241 | if (level && msg) { 242 | Integer levelIdx = LOG_LEVELS.indexOf(level) 243 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 244 | if (setLevelIdx < 0) { 245 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 246 | } 247 | if (levelIdx <= setLevelIdx) { 248 | log."${level}" "${device.displayName} ${msg}" 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /Drivers/Netatmo/Netatmo - Velux - Sensor switch.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | import groovy.json.JsonSlurper 17 | 18 | @Field String VERSION = "1.0.1" 19 | 20 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 21 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 22 | 23 | metadata { 24 | definition (name: "Netatmo - Velux - Sensor switch", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Sensor%20switch.groovy") { 25 | capability "Actuator" 26 | capability "Refresh" 27 | capability "Sensor" 28 | capability "Temperature Measurement" 29 | capability "Relative Humidity Measurement" 30 | capability "Carbon Dioxide Measurement" 31 | capability "Battery" 32 | 33 | attribute "id", "string" 34 | attribute "type", "string" 35 | attribute "velux_type", "string" 36 | attribute "bridge", "string" 37 | attribute "homeName", "string" 38 | attribute "roomName", "string" 39 | 40 | attribute "firmware_revision", "number" 41 | attribute "reachable", "string" 42 | attribute "rf_strength", "number" 43 | attribute "rf_state", "string" 44 | attribute "battery_state", "string" 45 | attribute "battery_percent", "number" 46 | attribute "battery_level", "number" 47 | attribute "last_seen", "number" 48 | attribute "carbonDioxide-Level", "enum", ["Good","Mediocre","Bad","Harmful","Risk"] 49 | } 50 | preferences { 51 | section { // General 52 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 53 | input name: "logDescText", title: "Log Description Text", type: "bool", defaultValue: false, required: false 54 | } 55 | } 56 | } 57 | 58 | def installed() { 59 | logger("debug", "installed(${VERSION})") 60 | 61 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 62 | state.driverInfo = [ver:VERSION] 63 | } 64 | 65 | if (state.deviceInfo == null) { 66 | state.deviceInfo = [:] 67 | } 68 | 69 | initialize() 70 | } 71 | 72 | def uninstalled() { 73 | logger("debug", "uninstalled()") 74 | unschedule() 75 | } 76 | 77 | def updated() { 78 | logger("debug", "updated()") 79 | 80 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 81 | installed() 82 | } 83 | unschedule() 84 | initialize() 85 | } 86 | 87 | def initialize() { 88 | logger("debug", "initialize()") 89 | } 90 | 91 | def refresh() { 92 | logger("debug", "refresh() - state: ${state.inspect()}") 93 | def home = parent?.getParent()?.getParent() 94 | home.checkState(state.deviceInfo.homeID) 95 | } 96 | 97 | def parse(String description) { 98 | logger("trace", "parse() - description: ${description?.inspect()}") 99 | return [] 100 | } 101 | 102 | def setDetails(Map detail) { 103 | logger("debug", "setDetails(${detail?.inspect()})") 104 | state.deviceInfo['velux_type'] = "sensor_switch" 105 | state.deviceInfo['homeID'] = detail.homeID 106 | if (detail?.roomID) { state.deviceInfo['roomID'] = detail.roomID } 107 | state.deviceInfo['bridge'] = detail.bridge 108 | sendEvent(name: "velux_type", value: "sensor_switch") 109 | sendEvent(name: "homeName", value: detail.homeName) 110 | sendEvent(name: "bridge", value: detail.bridge) 111 | } 112 | 113 | def setStates(Map states) { 114 | logger("debug", "setStates(${states?.inspect()})") 115 | states?.each { k, v -> 116 | String cv = device.currentValue(k) 117 | boolean isStateChange = (cv?.toString() != v?.toString()) ? true : false 118 | if (isStateChange) { 119 | if (logDescText && k != "last_seen") { 120 | log.info "${device.displayName} Value change: ${k} = ${cv} != ${v}" 121 | } else { 122 | logger("debug", "setStates() - Value change: ${k} = ${cv} != ${v}") 123 | } 124 | } 125 | sendEvent(name: "${k}", value: "${v}", displayed: true, isStateChange: isStateChange) 126 | if (k == "battery_percent") { 127 | sendEvent(name: "battery", value: v, displayed: true, isStateChange: isStateChange) 128 | } 129 | if (k == "reachable" && v == "false") { 130 | logger("warn", "Device is not reachable") 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * @param level Level to log at, see LOG_LEVELS for options 137 | * @param msg Message to log 138 | */ 139 | private logger(level, msg) { 140 | if (level && msg) { 141 | Integer levelIdx = LOG_LEVELS.indexOf(level) 142 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 143 | if (setLevelIdx < 0) { 144 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 145 | } 146 | if (levelIdx <= setLevelIdx) { 147 | log."${level}" "${device.displayName} ${msg}" 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Drivers/Netatmo/Netatmo - Welcome.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | import groovy.json.JsonSlurper 17 | 18 | @Field String VERSION = "1.0.3" 19 | 20 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 21 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[2] 22 | 23 | metadata { 24 | definition (name: "Netatmo - Welcome", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Welcome.groovy") { 25 | capability "Actuator" 26 | capability "Switch" 27 | capability "Motion Sensor" 28 | capability "Image Capture" 29 | capability "Refresh" 30 | capability "Initialize" 31 | 32 | command "alarm" 33 | command "motion" 34 | command "setAway" 35 | attribute "alarm", "string" 36 | attribute "status", "string" 37 | attribute "sd_status", "string" 38 | attribute "alim_status", "string" 39 | attribute "homeName", "string" 40 | attribute "person", "string" 41 | attribute "image_tag", "string" 42 | } 43 | preferences { 44 | section { // General 45 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 46 | input name: "logDescText", title: "Log Description Text", type: "bool", defaultValue: false, required: false 47 | } 48 | section { // Snapshots 49 | input name: "cameraIP", title: "Camera Local IP", description: "The address of the camera in your local network", type: "text", required: true 50 | input name: "motionTimeout", title: "Motion timeout", description: "Motion times out after how many seconds", type: "number", range: "0..3600", defaultValue: 60, required: true 51 | input name: "alarmTimeout", title: "Alarm timeout", description: "Alarm times out after how many seconds", type: "number", range: "0..3600", defaultValue: 60, required: true 52 | input name: "scheduledTake", title: "Take a snapshot every", type: "enum", options:[[0:"No snapshots"], [2:"2min"], [5:"5min"], [10:"10min"], [15:"15min"], [30:"30min"], [2:"1h"], [3:"3h"], [4:"4h"], [6:"6h"], [8:"8h"], [12: "12h"]], defaultValue: 0, required: true 53 | } 54 | } 55 | } 56 | 57 | def installed() { 58 | logger("debug", "installed(${VERSION})") 59 | 60 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 61 | state.driverInfo = [ver:VERSION] 62 | } 63 | 64 | if (state.deviceInfo == null) { 65 | state.deviceInfo = [:] 66 | } 67 | 68 | sendEvent(name: "alarm", value: "inactive") 69 | sendEvent(name: "motion", value: "inactive") 70 | initialize() 71 | } 72 | 73 | def uninstalled() { 74 | logger("debug", "uninstalled()") 75 | unschedule() 76 | } 77 | 78 | def updated() { 79 | logger("debug", "updated()") 80 | 81 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 82 | installed() 83 | } 84 | unschedule() 85 | initialize() 86 | } 87 | 88 | def initialize() { 89 | logger("debug", "initialize()") 90 | 91 | if (scheduledTake.toInteger()) { 92 | if (['2', '5', '10', '15', '30'].contains(scheduledTake) ) { 93 | schedule("0 */${scheduledTake} * ? * *", take) 94 | } else { 95 | schedule("0 0 */${scheduledTake} ? * *", take) 96 | } 97 | } 98 | } 99 | 100 | def refresh() { 101 | logger("debug", "refresh() - state: ${state.inspect()}") 102 | } 103 | 104 | def parse(String description) { 105 | logger("trace", "parse() - description: ${description?.inspect()}") 106 | return [] 107 | } 108 | 109 | def setHome(homeID,homeName) { 110 | logger("debug", "setHome(${homeID?.inspect()}, ${homeName?.inspect()})") 111 | state.deviceInfo['homeID'] = homeID 112 | sendEvent(name: "homeName", value: homeName) 113 | } 114 | 115 | def setAKey(key) { 116 | logger("debug", "setAKey(${key?.inspect()})") 117 | state.deviceInfo['accessKey'] = key 118 | } 119 | 120 | def setAway() { 121 | logger("debug", "setAway()") 122 | parent.setAway(state.deviceInfo['homeID']) 123 | } 124 | 125 | def on() { 126 | logger("debug", "on()") 127 | if (logDescText) { 128 | log.info "${device.displayName} Was turned on" 129 | } else { 130 | logger("info", "Was turned on") 131 | } 132 | sendEvent(name: "switch", value: "on") 133 | } 134 | 135 | def off() { 136 | logger("debug", "off()") 137 | if (logDescText) { 138 | log.info "${device.displayName} Was turned off" 139 | } else { 140 | logger("info", "Was turned off") 141 | } 142 | sendEvent(name: "switch", value: "off") 143 | } 144 | 145 | def motion(String snapshot_url = null, String person) { 146 | logger("debug", "motion(${snapshot_url}, ${person})") 147 | if (logDescText) { 148 | log.info "${device.displayName} Has detected motion (${person})" 149 | } else { 150 | logger("info", "Has detected motion (${person})") 151 | } 152 | sendEvent(name: "motion", value: "active", displayed: true, descriptionText: "Activated by ${person}") 153 | sendEvent(name: "person", value: person, displayed: true) 154 | if (snapshot_url != null) { 155 | sendEvent(name: "image_tag", value: '', isStateChange: true, displayed: true) 156 | } 157 | 158 | if (motionTimeout) { 159 | startTimer(motionTimeout, cancelMotion) 160 | } else { 161 | logger("debug", "motion() - Motion timeout has not been set in preferences, using 10 second default") 162 | startTimer(10, cancelMotion) 163 | } 164 | } 165 | 166 | def cancelMotion() { 167 | logger("debug", "cancelMotion()") 168 | sendEvent(name: "motion", value: "inactive") 169 | } 170 | 171 | def alarm(String snapshot_url = null) { 172 | logger("debug", "alarm(${snapshot_url})") 173 | if (logDescText) { 174 | log.info "${device.displayName} Has detected alarm (Sound)" 175 | } else { 176 | logger("info", "Has detected alarm (Sound)") 177 | } 178 | sendEvent(name: "alarm", value: "active", displayed: true) 179 | if (snapshot_url != null) { 180 | sendEvent(name: "image_tag", value: '', isStateChange: true, displayed: true) 181 | } 182 | 183 | if (alarmTimeout) { 184 | startTimer(alarmTimeout, cancelAlarm) 185 | } else { 186 | logger("debug", "alarm() - Alarm timeout has not been set in preferences, using 10 second default") 187 | startTimer(10, cancelAlarm) 188 | } 189 | } 190 | 191 | def cancelAlarm() { 192 | logger("debug", "cancelAlarm()") 193 | sendEvent(name: "alarm", value: "inactive") 194 | } 195 | 196 | // TODO: Needs work to actually store the image somewhere 197 | def take() { 198 | logger("debug", "take()") 199 | if (cameraIP == null || cameraIP == "") { 200 | logger("error", "take() - Please set camera local LAN IP") 201 | return 202 | } 203 | 204 | if (state.deviceInfo['accessKey'] == null || state.deviceInfo['accessKey'] == 'N/A') { 205 | logger("error", "take() - Please verify that the device access key is corect") 206 | return 207 | } 208 | 209 | def path = "${cameraIP}/${state.deviceInfo['accessKey']}/live/snapshot_720.jpg" 210 | 211 | sendEvent(name: "image", value: "http://"+ path, isStateChange: true, displayed: true) 212 | sendEvent(name: "image_tag", value: '', isStateChange: true, displayed: true) 213 | } 214 | 215 | private startTimer(seconds, function) { 216 | def now = new Date() 217 | def runTime = new Date(now.getTime() + (seconds * 1000)) 218 | runOnce(runTime, function) // runIn isn't reliable, use runOnce instead 219 | } 220 | 221 | /** 222 | * @param level Level to log at, see LOG_LEVELS for options 223 | * @param msg Message to log 224 | */ 225 | private logger(level, msg) { 226 | if (level && msg) { 227 | Integer levelIdx = LOG_LEVELS.indexOf(level) 228 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 229 | if (setLevelIdx < 0) { 230 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 231 | } 232 | if (levelIdx <= setLevelIdx) { 233 | log."${level}" "${device.displayName} ${msg}" 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /Drivers/Orvibo/Orvibo Smart Temperature & Humidity Sensor.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | 17 | @Field String VERSION = "1.0.1" 18 | 19 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 20 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 21 | 22 | metadata { 23 | definition (name: "Orvibo Smart Temperature & Humidity Sensor", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Orvibo/Orvibo%20Smart%20Temperature%20%26%20Humidity%20Sensor.groovy") { 24 | capability "Actuator" 25 | capability "Sensor" 26 | capability "RelativeHumidityMeasurement" 27 | capability "TemperatureMeasurement" 28 | capability "Battery" 29 | capability "Initialize" 30 | 31 | command "clearState" 32 | 33 | fingerprint profileId: "0104", inClusters: "0000,0001,0019,0402,0405", model: "898ca74409a740b28d5841661e72268d" // ST30 34 | } 35 | 36 | preferences { 37 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 38 | input name: "logDescText", title: "Log Description Text", type: "bool", defaultValue: false, required: false 39 | 40 | //Temp and Humidity Offsets 41 | input name: "tempOffset", type: "decimal", title: "Temperature", description: "Offset", range:"*..*" 42 | input name: "humidityOffset", type: "decimal", title: "Humidity", description: "Offset", range: "*..*" 43 | } 44 | } 45 | 46 | // installed() runs just after a sensor is paired 47 | def installed() { 48 | logger("debug", "installed(${VERSION})") 49 | 50 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 51 | state.driverInfo = [ver:VERSION] 52 | } 53 | 54 | if (state.deviceInfo == null) { 55 | state.deviceInfo = [:] 56 | } 57 | 58 | initialize() 59 | } 60 | 61 | def initialize() { 62 | logger("debug", "initialize()") 63 | 64 | if (getDataValue("model") && getDataValue("model") != "ST30") { 65 | if (device.data.model == "898ca74409a740b28d5841661e72268d") { 66 | updateDataValue("model", "ST30") 67 | } 68 | } 69 | } 70 | 71 | def updated() { 72 | logger("debug", "updated()") 73 | 74 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 75 | installed() 76 | } 77 | 78 | unschedule() 79 | } 80 | 81 | def clearState() { 82 | logger("debug", "ClearState() - Clearing device states") 83 | state.clear() 84 | 85 | if (state?.driverInfo == null) { 86 | state.driverInfo = [:] 87 | } else { 88 | state.driverInfo.clear() 89 | } 90 | 91 | if (state?.deviceInfo == null) { 92 | state.deviceInfo = [:] 93 | } else { 94 | state.deviceInfo.clear() 95 | } 96 | 97 | installed() 98 | } 99 | 100 | def parse(String description) { 101 | logger("trace", "parse() - description: ${description?.inspect()}") 102 | 103 | if (description?.startsWith('cat')) { 104 | Map descMap = zigbee.parseDescriptionAsMap(description) 105 | logger("trace", "parse(catchall): ${descMap}") 106 | } else if (description?.startsWith('re')) { 107 | description = description - "read attr - " 108 | Map descMap = (description).split(",").inject([:]) { 109 | map, param -> 110 | def nameAndValue = param.split(":") 111 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 112 | } 113 | // Reverse payload byte order for little-endian data types - required for Hubitat firmware 2.0.5 or newer 114 | def intEncoding = Integer.parseInt(descMap.encoding, 16) 115 | if (descMap.value != null && intEncoding > 0x18 && intEncoding < 0x3e) { 116 | descMap.value = reverseHexString(descMap.value) 117 | logger("trace", "Little-endian payload data type; Hex value reversed to: ${descMap.value}") 118 | } 119 | 120 | // Send message data to appropriate parsing function based on the type of report 121 | switch (descMap.cluster) { 122 | case "0001": // Battery report 123 | if (descMap.attrId == "0021") { 124 | parseBattery(descMap.value) 125 | } 126 | break 127 | case "0402": // Temperature report 128 | parseTemperature(descMap.value) 129 | break 130 | case "0405": // Humidity report 131 | parseHumidity(descMap.value) 132 | break 133 | default: 134 | logger("warn", "Unknown read attribute message: ${descMap}") 135 | } 136 | } 137 | return [:] 138 | } 139 | 140 | // Reverses order of bytes in hex string 141 | private def reverseHexString(hexString) { 142 | def reversed = "" 143 | for (int i = hexString.length(); i > 0; i -= 2) { 144 | reversed += hexString.substring(i - 2, i ) 145 | } 146 | return reversed 147 | } 148 | 149 | // Calculate temperature with 0.01 precision in C or F unit as set by hub location settings 150 | private parseTemperature(hexString) { 151 | logger("trace", "parseTemperature(${hexString})") 152 | 153 | Map map = [:] 154 | float temp = hexStrToSignedInt(hexString)/100 155 | def tempScale = location.temperatureScale 156 | def debugText = "Reported temperature: raw = ${temp}°C" 157 | if (temp < -50) { 158 | logger("warn", "Out-of-bounds temperature value received. Battery voltage may be too low") 159 | } else { 160 | if (tempScale == "F") { 161 | temp = ((temp * 1.8) + 32) 162 | debugText += ", converted = ${temp}°F" 163 | } 164 | if (tempOffset) { 165 | temp = (temp + tempOffset) 166 | debugText += ", offset = ${tempOffset}" 167 | } 168 | logger("debug", debugText) 169 | temp = temp.round(2) 170 | 171 | map.name = "temperature" 172 | map.value = temp 173 | map.unit = "°$tempScale" 174 | map.descriptionText = "Temperature is ${temp}°${tempScale}" 175 | map.displayed = true 176 | 177 | if (logDescText) { 178 | log.info "${device.displayName} ${map.descriptionText}" 179 | } else if(map?.descriptionText) { 180 | logger("info", "${map.descriptionText}") 181 | } 182 | 183 | sendEvent(map) 184 | } 185 | } 186 | 187 | // Calculate humidity with 0.1 precision 188 | private parseHumidity(hexString) { 189 | logger("trace", "parseHumidity(${hexString})") 190 | 191 | Map map = [:] 192 | float humidity = Integer.parseInt(hexString,16)/100 193 | def debugText = "Reported humidity: raw = ${humidity}" 194 | if (humidity > 100) { 195 | logger("warn", "Out-of-bounds humidity value received. Battery voltage may be too low") 196 | return "" 197 | } else { 198 | if (humidityOffset) { 199 | debugText += ", offset = ${humidityOffset}" 200 | humidity = (humidity + humidityOffset) 201 | } 202 | logger("debug", debugText) 203 | humidity = humidity.round(1) 204 | 205 | map.name = "humidity" 206 | map.value = humidity 207 | map.unit = "%" 208 | map.descriptionText = "Humidity is ${map.value} ${map.unit}" 209 | map.displayed = true 210 | 211 | if (logDescText) { 212 | log.info "${device.displayName} ${map.descriptionText}" 213 | } else if(map?.descriptionText) { 214 | logger("info", "${map.descriptionText}") 215 | } 216 | 217 | sendEvent(map) 218 | } 219 | } 220 | 221 | private parseBattery(hexString) { 222 | logger("trace", "parseBattery(${hexString})") 223 | 224 | def rawValue = Integer.parseInt(hexString,16) 225 | if (0 <= rawValue && rawValue <= 200) { 226 | Map map = [:] 227 | def roundedPct = Math.round(rawValue / 2) 228 | map.name = "battery" 229 | map.value = roundedPct 230 | map.unit = "%" 231 | map.descriptionText = "Battery level is ${roundedPct}%" 232 | map.displayed = true 233 | 234 | if (logDescText) { 235 | log.info "${device.displayName} ${map.descriptionText}" 236 | } else if(map?.descriptionText) { 237 | logger("info", "${map.descriptionText}") 238 | } 239 | 240 | sendEvent(map) 241 | } 242 | } 243 | 244 | /** 245 | * @param level Level to log at, see LOG_LEVELS for options 246 | * @param msg Message to log 247 | */ 248 | private logger(level, msg) { 249 | if (level && msg) { 250 | Integer levelIdx = LOG_LEVELS.indexOf(level) 251 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 252 | if (setLevelIdx < 0) { 253 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 254 | } 255 | if (levelIdx <= setLevelIdx) { 256 | log."${level}" "${device.displayName} ${msg}" 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /Drivers/Orvibo/Orvibo Smart Temperature & Humidity Sensor.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Orvibo Smart Temperature & Humidity Sensor", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-orvibo-smart-temperature-humidity-sensor/57034/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Orvibo Smart Temperature & Humidity Sensor", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Orvibo/Orvibo%20Smart%20Temperature%20%26%20Humidity%20Sensor.groovy", 15 | "version": "1.0.1", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/PanasonicComfortCloud/Panasonic - Comfort Cloud - Group.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | import groovy.json.JsonSlurper 17 | import com.hubitat.app.ChildDeviceWrapper 18 | 19 | @Field String VERSION = "1.0.1" 20 | 21 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 22 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 23 | 24 | metadata { 25 | definition (name: "Panasonic - Comfort Cloud - Group", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Netatmo/Netatmo%20-%20Velux%20-%20Home.groovy") { 26 | capability "Actuator" 27 | capability "Switch" 28 | capability "Refresh" 29 | } 30 | preferences { 31 | section { // General 32 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 33 | } 34 | } 35 | } 36 | 37 | def installed() { 38 | logger("debug", "installed(${VERSION})") 39 | 40 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 41 | state.driverInfo = [ver:VERSION] 42 | } 43 | 44 | if (state.deviceInfo == null) { 45 | state.deviceInfo = [:] 46 | } 47 | 48 | initialize() 49 | } 50 | 51 | def uninstalled() { 52 | logger("debug", "uninstalled()") 53 | unschedule() 54 | } 55 | 56 | def updated() { 57 | logger("debug", "updated()") 58 | 59 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 60 | installed() 61 | } 62 | unschedule() 63 | initialize() 64 | } 65 | 66 | def initialize() { 67 | logger("debug", "initialize()") 68 | } 69 | 70 | def refresh() { 71 | logger("debug", "refresh() - state: ${state.inspect()}") 72 | parent.checkState() 73 | } 74 | 75 | def parse(String description) { 76 | logger("trace", "parse() - description: ${description?.inspect()}") 77 | return [] 78 | } 79 | 80 | def off() { 81 | logger("debug", "off()") 82 | List devices = getChildDevices() 83 | devices.each { device -> 84 | def deviceId = device.currentState("id")?.value 85 | logger("debug", "off() - Device: ${device} (${deviceId})") 86 | if (deviceId) { 87 | parent.deviceControl(["deviceGuid": deviceId, "parameters":["operate": 0]]) 88 | sendEvent(name: "switch", value: "off", displayed: true, descriptionText: "${device} (${deviceId})") 89 | } 90 | } 91 | } 92 | 93 | def on() { 94 | logger("debug", "on()") 95 | List devices = getChildDevices() 96 | devices.each { device -> 97 | def deviceId = device.currentState("id")?.value 98 | logger("debug", "on() - Device: ${device} (${deviceId})") 99 | if (deviceId) { 100 | parent.deviceControl(["deviceGuid": deviceId, "parameters":["operate": 1]]) 101 | sendEvent(name: "switch", value: "on", displayed: true, descriptionText: "${device} (${deviceId})") 102 | } 103 | } 104 | } 105 | 106 | def setDetails(Map detail) { 107 | logger("debug", "setDetails(${detail?.inspect()})") 108 | state.deviceInfo['groupId'] = detail.groupId 109 | state.deviceInfo['groupName'] = detail.groupName 110 | } 111 | 112 | ChildDeviceWrapper addDevice(Map detail) { 113 | if (detail && detail.deviceType ==~ /2|3/) { 114 | addAC(detail) 115 | } else { 116 | logger("error", "addDevice() - Unknown Device type: ${detail.inspect()}") 117 | } 118 | } 119 | 120 | ChildDeviceWrapper addAC(Map detail) { 121 | logger("info", "Creating Device: ${detail.deviceName} (${detail.deviceModuleNumber})") 122 | logger("debug", "addAC(${detail?.inspect()})") 123 | 124 | try { 125 | ChildDeviceWrapper cd = getChildDevice("${device.deviceNetworkId}-${detail?.id}") 126 | if(!cd) { 127 | logger("debug", "addAC() - Creating AC (${detail.inspect()}") 128 | ChildDeviceWrapper cdm = addChildDevice("syepes", "Panasonic - Comfort Cloud - AC", "${device.deviceNetworkId}-${detail?.id}", [name: "${detail?.name}", label: "${detail?.name}", isComponent: true]) 129 | cdm.setDetails(detail) 130 | return cdm 131 | } else { 132 | logger("debug", "addAC() - AC: (${device.deviceNetworkId}-${detail?.id}) already exists") 133 | return cd 134 | } 135 | } catch (e) { 136 | logger("error", "addAC() - AC creation Exception: ${e.inspect()}") 137 | return null 138 | } 139 | } 140 | 141 | /** 142 | * @param level Level to log at, see LOG_LEVELS for options 143 | * @param msg Message to log 144 | */ 145 | private logger(level, msg) { 146 | if (level && msg) { 147 | Integer levelIdx = LOG_LEVELS.indexOf(level) 148 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 149 | if (setLevelIdx < 0) { 150 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 151 | } 152 | if (levelIdx <= setLevelIdx) { 153 | log."${level}" "${device.displayName} ${msg}" 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Drivers/Popp/Popp Electric Strike Lock Control.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Popp Electric Strike Lock Control", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2021-02-04", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-popp-electric-strike-lock-control/32954/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Popp Electric Strike Lock Control", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Popp/Popp%20Electric%20Strike%20Lock%20Control.groovy", 15 | "version": "1.1.3", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Popp/Popp Z-Rain Sensor.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Popp Z-Rain Sensor", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-popp-z-rain-sensor/34270/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Popp Z-Rain Sensor", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Popp/Popp%20Z-Rain%20Sensor.groovy", 15 | "version": "1.1.3", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Qubino/Qubino Flush Pilot Wire.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Qubino Flush Pilot Wire", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-qubino-flush-pilot-wire/32955/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Qubino Flush Pilot Wire", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Qubino/Qubino%20Flush%20Pilot%20Wire.groovy", 15 | "version": "1.1.5", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Qubino/Qubino Flush Shutter - CMV.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Qubino Flush Shutter - CMV", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Qubino Flush Shutter - CMV", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Qubino/Qubino%20Flush%20Shutter%20-%20CMV.groovy", 15 | "version": "1.1.3", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Schwaiger/Schwaiger Temperature Sensor.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Schwaiger Temperature Sensor", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-schwaiger-temperature-sensor/32956/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Schwaiger Temperature Sensor", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Schwaiger/Schwaiger%20Temperature%20Sensor.groovy", 15 | "version": "1.1.3", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Shelly/ShellyPlus Generic.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "ShellyPlus Generic", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-12-25", 6 | "documentationLink": "", 7 | "communityLink": "", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "ShellyPlus Generic", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Shelly/ShellyPlus%20Generic.groovy", 15 | "version": "1.0.0", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Shelly/ShellyPlus Pilot Wire.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "ShellyPlus Pilot Wire", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-12-25", 6 | "documentationLink": "", 7 | "communityLink": "", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "ShellyPlus Pilot Wire", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Shelly/ShellyPlus%20Pilot%20Wire.groovy", 15 | "version": "1.0.0", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Drivers/Sonoff/Sonoff RF Bridge - Shade.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | 17 | @Field String VERSION = "1.0.0" 18 | 19 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 20 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 21 | 22 | metadata { 23 | definition (name: "Sonoff RF Bridge - Shade", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Sonoff/Sonoff%20RF%20Bridge%20-%20Shade.groovy") { 24 | capability "Actuator" 25 | capability "WindowShade" 26 | capability "Switch" 27 | command "stop" 28 | } 29 | preferences { 30 | section { // General 31 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 32 | } 33 | } 34 | } 35 | 36 | def installed() { 37 | logger("debug", "installed(${VERSION})") 38 | 39 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 40 | state.driverInfo = [ver:VERSION] 41 | } 42 | 43 | initialize() 44 | } 45 | 46 | def uninstalled() { 47 | logger("debug", "uninstalled()") 48 | unschedule() 49 | } 50 | 51 | def updated() { 52 | logger("debug", "updated()") 53 | 54 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 55 | installed() 56 | } 57 | unschedule() 58 | initialize() 59 | } 60 | 61 | def initialize() { 62 | logger("debug", "initialize()") 63 | } 64 | 65 | def parse(value) { 66 | logger("debug", "parse() - value: ${value?.inspect()}") 67 | if (value) { 68 | sendEvent(value) 69 | } 70 | } 71 | 72 | def close() { 73 | logger("debug", "close()") 74 | sendEvent([name: "windowShade", value: "closing", displayed: true]) 75 | parent.childClose(device.deviceNetworkId) 76 | } 77 | 78 | def off() { 79 | logger("debug", "off()") 80 | close() 81 | } 82 | 83 | def open() { 84 | logger("debug", "open()") 85 | sendEvent(name: "windowShade", value: "opening", displayed: true) 86 | parent.childOpen(device.deviceNetworkId) 87 | } 88 | 89 | def on() { 90 | logger("debug", "on()") 91 | open() 92 | } 93 | 94 | def startPositionChange(value) { 95 | logger("debug", "startPositionChange(${value})") 96 | 97 | switch (value) { 98 | case "close": 99 | close() 100 | return 101 | case "open": 102 | open() 103 | return 104 | default: 105 | logger("error", "startPositionChange(${value}) - Unsupported state") 106 | } 107 | } 108 | 109 | def setPosition(BigDecimal value) { 110 | logger("debug", "setPosition(${value})") 111 | sendEvent(name: "windowShade", value: "partially open", displayed: true) 112 | parent.childPosition(device.deviceNetworkId, value) 113 | } 114 | 115 | def setLevel(BigDecimal value) { 116 | logger("debug", "setLevel(${value})") 117 | setPosition(value) 118 | } 119 | 120 | def stop() { 121 | logger("debug", "stop()") 122 | sendEvent(name: "windowShade", value: "partially open", displayed: true) 123 | parent.childStop(device.deviceNetworkId) 124 | } 125 | 126 | def stopPositionChange() { 127 | logger("debug", "stopPositionChange()") 128 | stop() 129 | } 130 | 131 | /** 132 | * @param level Level to log at, see LOG_LEVELS for options 133 | * @param msg Message to log 134 | */ 135 | private logger(level, msg) { 136 | if (level && msg) { 137 | Integer levelIdx = LOG_LEVELS.indexOf(level) 138 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 139 | if (setLevelIdx < 0) { 140 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 141 | } 142 | if (levelIdx <= setLevelIdx) { 143 | log."${level}" "${device.displayName} ${msg}" 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Drivers/Sonoff/Sonoff RF Bridge - Switch.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | 17 | @Field String VERSION = "1.0.0" 18 | 19 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 20 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 21 | 22 | metadata { 23 | definition (name: "Sonoff RF Bridge - Switch", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Sonoff/Sonoff%20RF%20Bridge%20-%20Switch.groovy") { 24 | capability "Actuator" 25 | capability "Switch" 26 | } 27 | preferences { 28 | section { // General 29 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 30 | } 31 | } 32 | } 33 | 34 | def installed() { 35 | logger("debug", "installed(${VERSION})") 36 | 37 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 38 | state.driverInfo = [ver:VERSION] 39 | } 40 | 41 | initialize() 42 | } 43 | 44 | def uninstalled() { 45 | logger("debug", "uninstalled()") 46 | unschedule() 47 | } 48 | 49 | def updated() { 50 | logger("debug", "updated()") 51 | 52 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 53 | installed() 54 | } 55 | unschedule() 56 | initialize() 57 | } 58 | 59 | def initialize() { 60 | logger("debug", "initialize()") 61 | } 62 | 63 | def parse(value) { 64 | logger("debug", "parse() - value: ${value?.inspect()}") 65 | if (value) { 66 | sendEvent(value) 67 | } 68 | } 69 | 70 | def on() { 71 | logger("debug", "on()") 72 | sendEvent([name: "switch", value: "on", displayed: true]) 73 | parent.childOn(device.deviceNetworkId) 74 | } 75 | 76 | def off() { 77 | logger("debug", "off()") 78 | sendEvent(name: "switch", value: "off", displayed: true) 79 | parent.childOff(device.deviceNetworkId) 80 | } 81 | 82 | /** 83 | * @param level Level to log at, see LOG_LEVELS for options 84 | * @param msg Message to log 85 | */ 86 | private logger(level, msg) { 87 | if (level && msg) { 88 | Integer levelIdx = LOG_LEVELS.indexOf(level) 89 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 90 | if (setLevelIdx < 0) { 91 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 92 | } 93 | if (levelIdx <= setLevelIdx) { 94 | log."${level}" "${device.displayName} ${msg}" 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Drivers/Warmup/Warmup - Cloud - Location.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | import groovy.json.JsonSlurper 17 | import com.hubitat.app.ChildDeviceWrapper 18 | 19 | @Field String VERSION = "1.0.1" 20 | 21 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 22 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[1] 23 | 24 | @Field static Map typeGeoMode = [ 25 | 0: "off", 26 | 1: "on and visible to others", 27 | 2: "on and invisible", 28 | ] 29 | @Field static Map typeCurrency = [ 30 | 0: "£", 31 | 1: "€", 32 | 2: "\$", 33 | 3: "¥", 34 | 4: "Pln", 35 | 5: "Kr", 36 | 6: "Kn" 37 | ] 38 | @Field static Map typeTempFormat = [ 39 | false: "°C", 40 | true: "°F" 41 | ] 42 | 43 | metadata { 44 | definition (name: "Warmup - Cloud - Location", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Warmup/Warmup%20-%20Cloud%20-%20Location.groovy") { 45 | capability "Actuator" 46 | capability "Switch" 47 | capability "Refresh" 48 | 49 | command "setLocationModes", [[name:"mode", type: "ENUM", description: "Mode", constraints: ["off", "frost", "geo"]]] 50 | 51 | attribute "id", "string" 52 | attribute "name", "string" 53 | attribute "locMode", "string" 54 | attribute "smartGeo", "string" 55 | attribute "smartGeoMode", "string" 56 | attribute "currency", "string" 57 | attribute "temperatureFormat", "string" 58 | } 59 | preferences { 60 | section { // General 61 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 62 | input name: "logDescText", title: "Log Description Text", type: "bool", defaultValue: true, required: false 63 | } 64 | } 65 | } 66 | 67 | def installed() { 68 | logger("debug", "installed(${VERSION})") 69 | 70 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 71 | state.driverInfo = [ver:VERSION] 72 | } 73 | 74 | if (state.deviceInfo == null) { 75 | state.deviceInfo = [:] 76 | } 77 | 78 | initialize() 79 | } 80 | 81 | def uninstalled() { 82 | logger("debug", "uninstalled()") 83 | unschedule() 84 | } 85 | 86 | def updated() { 87 | logger("debug", "updated()") 88 | 89 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 90 | installed() 91 | } 92 | unschedule() 93 | initialize() 94 | } 95 | 96 | def initialize() { 97 | logger("debug", "initialize()") 98 | } 99 | 100 | def refresh() { 101 | logger("debug", "refresh() - state: ${state.inspect()}") 102 | parent.checkState() 103 | } 104 | 105 | def parse(String description) { 106 | logger("trace", "parse() - description: ${description?.inspect()}") 107 | return [] 108 | } 109 | 110 | def off() { 111 | logger("debug", "off()") 112 | setLocationModes("off") 113 | } 114 | 115 | def on() { 116 | logger("debug", "on()") 117 | setLocationModes("geo") 118 | } 119 | 120 | // Modes: [off, frost] 121 | void setLocationModes(String mode) { 122 | logger("debug", "setLocationModes(mode=${mode})") 123 | 124 | String locId = device.currentValue("id") 125 | String locName = device.currentValue('name') 126 | 127 | Map query = [request:[method: "setModes", 128 | values: [holEnd: "-", 129 | fixedTemp: "", 130 | holStart: "-", 131 | geoMode: "0", 132 | holTemp: "-", 133 | locId: locId, 134 | locMode: mode 135 | ] 136 | ] 137 | ] 138 | 139 | def app = parent 140 | app.apiPost(query) { resp -> 141 | logger("trace", "setLocationModes(mode=${mode}) - respStatus: ${resp?.getStatus()}, respHeaders: ${resp?.getAllHeaders()?.inspect()}, respData: ${resp?.getData()}") 142 | if (resp && resp.getStatus() == 200 && resp?.getData()?.status?.result == "success") { 143 | if (logDescText) { 144 | log.info "${device.displayName} Set Location (${locName}/${locId}) Mode: ${mode}" 145 | } else { 146 | logger("debug", "setLocationModes(mode=${mode}) - Set Location (${locName}/${locId}) Mode: ${mode}") 147 | } 148 | } else { 149 | logger("warn", "setLocationModes(mode=${mode}) - Setting Location (${locName}/${locId}) Mode: ${mode} Failed") 150 | } 151 | } 152 | } 153 | 154 | def setDetails(Map detail) { 155 | logger("debug", "setDetails(${detail?.inspect()})") 156 | sendEvent(name: "id", value: detail.id) 157 | sendEvent(name: "name", value: detail.name) 158 | 159 | state.deviceInfo['locId'] = detail.id 160 | state.deviceInfo['locName'] = detail.name 161 | 162 | detail?.each { k, v -> 163 | if (k =~ /^(fenceArray|id|now|name)$/) { return } 164 | state.deviceInfo[k] = v 165 | 166 | if (k == "currency") { 167 | String cur = typeCurrency.find { it.key == v }?.value 168 | sendEvent(name: "${k}", value: "${cur}", displayed: true) 169 | } 170 | if (k == "tempFormat") { 171 | String temp = typeTempFormat.find { it.key?.toString() == v?.toString() }?.value 172 | sendEvent(name: "temperatureFormat", value: "${temp}", displayed: true) 173 | } 174 | if (k =~ /^(smartGeo|locMode)$/) { 175 | sendEvent(name: "${k}", value: "${v}", displayed: true) 176 | if (k == "locMode" && ["frost", "geo"].contains(v)) { 177 | sendEvent(name: "switch", value: "on", displayed: true, descriptionText: "Mode: ${v}") 178 | } else if (k == "locMode" && "off" == v) { 179 | sendEvent(name: "switch", value: "off", displayed: true, descriptionText: "Mode: ${v}") 180 | } 181 | } 182 | if (k == "geoMode") { 183 | String mode = typeGeoMode.find { it.key == v }?.value 184 | sendEvent(name: "smartGeoMode", value: "${mode}", displayed: true) 185 | } 186 | } 187 | } 188 | 189 | ChildDeviceWrapper addRoom(Map detail) { 190 | logger("info", "Creating Device: ${detail.roomName} (${detail.locName})") 191 | logger("debug", "addRoom(${detail?.inspect()})") 192 | 193 | try { 194 | ChildDeviceWrapper cd = getChildDevice("${device.deviceNetworkId}-${detail?.id}") 195 | if(!cd) { 196 | logger("debug", "addRoom() - Creating Room (${detail.inspect()}") 197 | ChildDeviceWrapper cdm = addChildDevice("syepes", "Warmup - Cloud - Room", "${device.deviceNetworkId}-${detail?.id}", [name: "${detail?.name}", label: "${detail?.name}", isComponent: true]) 198 | cdm.setDetails(detail) 199 | return cdm 200 | } else { 201 | logger("debug", "addRoom() - Room: ${cd.name} (${detail.locName}) (${device.deviceNetworkId}-${detail?.id}) already exists") 202 | return cd 203 | } 204 | } catch (e) { 205 | logger("error", "addRoom() - Room creation Exception: ${e.inspect()}") 206 | return null 207 | } 208 | } 209 | 210 | /** 211 | * @param level Level to log at, see LOG_LEVELS for options 212 | * @param msg Message to log 213 | */ 214 | private logger(level, msg) { 215 | if (level && msg) { 216 | Integer levelIdx = LOG_LEVELS.indexOf(level) 217 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 218 | if (setLevelIdx < 0) { 219 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 220 | } 221 | if (levelIdx <= setLevelIdx) { 222 | log."${level}" "${device.displayName} ${msg}" 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /Drivers/Xiaomi/Xiaomi Mijia - Sensor Temperature Humidity Child Device.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.transform.Field 16 | 17 | @Field String VERSION = "1.0.1" 18 | 19 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 20 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[2] 21 | 22 | metadata { 23 | definition (name: "Xiaomi Mijia - Sensor Temperature Humidity Child Device", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Xiaomi/Xiaomi%20Mijia%20-%20Sensor%20Temperature%20Humidity%20Child%20Device.groovy") { 24 | capability "Actuator" 25 | capability "Sensor" 26 | capability "Temperature Measurement" 27 | capability "Relative Humidity Measurement" 28 | capability "Battery" 29 | 30 | attribute "name", "string" 31 | attribute "location", "string" 32 | attribute "status", "string" 33 | } 34 | preferences { 35 | section { // General 36 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 37 | } 38 | } 39 | } 40 | 41 | def installed() { 42 | logger("debug", "installed(${VERSION})") 43 | 44 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 45 | state.driverInfo = [ver:VERSION] 46 | } 47 | 48 | if (state.deviceInfo == null) { 49 | state.deviceInfo = [:] 50 | } 51 | 52 | initialize() 53 | } 54 | 55 | def uninstalled() { 56 | logger("debug", "uninstalled()") 57 | unschedule() 58 | } 59 | 60 | def updated() { 61 | logger("debug", "updated()") 62 | 63 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 64 | installed() 65 | } 66 | unschedule() 67 | initialize() 68 | } 69 | 70 | def initialize() { 71 | logger("debug", "initialize()") 72 | } 73 | 74 | def parse(value) { 75 | logger("debug", "parse() - value: ${value?.inspect()}") 76 | def result = [] 77 | try { 78 | if (value) { 79 | value?.each { k, v -> 80 | switch (k) { 81 | case 'battery': 82 | Map map = [ name: "battery", value: v, unit: "%", displayed: true, descriptionText: "Battery is ${v} %"] 83 | if(v.toInteger() < 5) { 84 | logger("warn", "Has a low battery") 85 | } else { 86 | if(logDescText && map?.descriptionText) { 87 | log.info "${device.displayName} ${map.descriptionText}" 88 | } else if(map?.descriptionText) { 89 | logger("info", "${map.descriptionText}") 90 | } 91 | } 92 | sendEvent(map) 93 | break 94 | case 'location': 95 | Map map = [ name: "location", value: v, displayed: true] 96 | sendEvent(map) 97 | break 98 | case 'temperature': 99 | Map map = [ name: "temperature", value: v, unit: "°C", displayed: true, descriptionText: "Temperature is ${v} °C"] 100 | if(logDescText && map?.descriptionText) { 101 | log.info "${device.displayName} ${map.descriptionText}" 102 | } else if(map?.descriptionText) { 103 | logger("info", "${map.descriptionText}") 104 | } 105 | sendEvent(map) 106 | break 107 | case 'humidity': 108 | Map map = [ name: "humidity", value: v, unit: "%", displayed: true, descriptionText: "Humidity is ${v} %"] 109 | if(logDescText && map?.descriptionText) { 110 | log.info "${device.displayName} ${map.descriptionText}" 111 | } else if(map?.descriptionText) { 112 | logger("info", "${map.descriptionText}") 113 | } 114 | sendEvent(map) 115 | break 116 | case 'name': 117 | state.deviceInfo.name = v 118 | break 119 | case 'mac': 120 | state.deviceInfo.mac = v 121 | break 122 | case 'firmware': 123 | state.deviceInfo.firmware = v 124 | break 125 | case 'label': 126 | case 'type': 127 | break 128 | default: 129 | logger("warn", "parse() - type: ${k} - Unhandled") 130 | break 131 | } 132 | } 133 | 134 | state.deviceInfo.lastevent = (new Date().getTime()/1000) as long 135 | } 136 | } catch (e) { 137 | logger("error", "parse() - ${e}, value: ${value?.inspect()}") 138 | } 139 | return result 140 | } 141 | 142 | 143 | /** 144 | * @param level Level to log at, see LOG_LEVELS for options 145 | * @param msg Message to log 146 | */ 147 | private logger(level, msg) { 148 | if (level && msg) { 149 | Integer levelIdx = LOG_LEVELS.indexOf(level) 150 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 151 | if (setLevelIdx < 0) { 152 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 153 | } 154 | if (levelIdx <= setLevelIdx) { 155 | log."${level}" "${device.displayName} ${msg}" 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Drivers/Xiaomi/Xiaomi Mijia DataCollector/mijia/mijia_v1_poller.py: -------------------------------------------------------------------------------- 1 | """" 2 | Read data from Xiaomi Mijia Bluetooth Temp & Humidity sensor. 3 | 4 | Reading from the sensor is handled by the command line tool "gatttool" that 5 | is part of bluez on Linux. 6 | No other operating systems are supported at the moment 7 | """ 8 | 9 | from datetime import datetime, timedelta 10 | from threading import Lock, current_thread 11 | import re 12 | from subprocess import PIPE, Popen, TimeoutExpired 13 | import logging 14 | import time 15 | import signal 16 | import os 17 | #from gattlib import GATTRequester 18 | from btlewrap.base import BluetoothBackendException 19 | 20 | MI_TEMPERATURE = "temperature" 21 | MI_HUMIDITY = "humidity" 22 | MI_BATTERY = "battery" 23 | 24 | # logging.basicConfig(level=logging.DEBUG) 25 | LOGGER = logging.getLogger(__name__) 26 | LOCK = Lock() 27 | 28 | 29 | class MijiaV1Poller(object): 30 | """" 31 | A class to read data from Xiaomi Mijia Bluetooth sensors. 32 | """ 33 | 34 | def __init__(self, mac, cache_timeout=300, retries=3, adapter='hci0'): 35 | """ 36 | Initialize a Xiaomi Mijia Poller for the given MAC address. 37 | """ 38 | 39 | self._mac = mac 40 | self._adapter = adapter 41 | self._cache = None 42 | self._cache_timeout = timedelta(seconds=cache_timeout) 43 | self._last_read = None 44 | self._fw_last_read = datetime.now() 45 | self.retries = retries 46 | self.ble_timeout = 10 47 | self.lock = Lock() 48 | self._firmware_version = None 49 | self._battery_level = None 50 | self._bat_last_read = datetime.now() 51 | 52 | def name(self): 53 | """ 54 | Return the name of the sensor. 55 | """ 56 | name = read_ble(self._mac, "0x03", retries=self.retries, timeout=self.ble_timeout, adapter=self._adapter) 57 | if not name: 58 | raise BluetoothBackendException("Could not read NAME from Mi Temp sensor %s" % (self._mac)) 59 | return ''.join(chr(n) for n in name) 60 | 61 | def fill_cache(self): 62 | firmware_version = self.firmware_version() 63 | if not firmware_version: 64 | # If a sensor doesn't work, wait 2 minutes before retrying 65 | self._last_read = datetime.now() - self._cache_timeout + timedelta(seconds=120) 66 | return 67 | 68 | self._cache = write_readnotif_ble(self._mac, "0x10", "0100", retries=self.retries, timeout=self.ble_timeout, adapter=self._adapter) 69 | 70 | self._check_data() 71 | if self._cache is not None: 72 | self._last_read = datetime.now() 73 | else: 74 | # If a sensor doesn't work, wait 2 minutes before retrying 75 | self._last_read = datetime.now() - self._cache_timeout + timedelta(seconds=120) 76 | 77 | def battery_level(self): 78 | """ 79 | Return the battery level. 80 | """ 81 | if (self._battery_level is None) or (datetime.now() - timedelta(hours=1) > self._bat_last_read): 82 | self._bat_last_read = datetime.now() 83 | res = read_ble(self._mac, '0x18', retries=self.retries, adapter=self._adapter) 84 | if res is None: 85 | self._battery_level = 0 86 | else: 87 | self._battery_level = res[0] 88 | return self._battery_level 89 | 90 | def firmware_version(self): 91 | """ Return the firmware version. """ 92 | if (self._firmware_version is None) or (datetime.now() - timedelta(hours=24) > self._fw_last_read): 93 | self._fw_last_read = datetime.now() 94 | res = read_ble(self._mac, '0x24', retries=self.retries, adapter=self._adapter) 95 | if res is None: 96 | self._firmware_version = None 97 | else: 98 | self._firmware_version = "".join(map(chr, res)) 99 | return self._firmware_version 100 | 101 | def parameter_value(self, parameter, read_cached=True): 102 | """ 103 | Return a value of one of the monitored paramaters. 104 | 105 | This method will try to retrieve the data from cache and only 106 | request it by bluetooth if no cached value is stored or the cache is 107 | expired. 108 | This behaviour can be overwritten by the "read_cached" parameter. 109 | """ 110 | 111 | LOGGER.debug("Call to parameter_value (%s)", parameter) 112 | 113 | # Special handling for battery attribute 114 | if parameter == MI_BATTERY: 115 | return self.battery_level() 116 | 117 | # Use the lock to make sure the cache isn't updated multiple times 118 | with self.lock: 119 | if (read_cached is False) or \ 120 | (self._last_read is None) or \ 121 | (datetime.now() - self._cache_timeout > self._last_read): 122 | self.fill_cache() 123 | else: 124 | LOGGER.debug("Using cache (%s < %s)", datetime.now() - self._last_read, self._cache_timeout) 125 | 126 | if self._cache and (len(self._cache) == 14): 127 | return self._parse_data()[parameter] 128 | else: 129 | raise IOError("Could not read data from Mi sensor %s", self._mac) 130 | 131 | def _check_data(self): 132 | if self._cache is None: 133 | return 134 | datasum = 0 135 | for i in self._cache: 136 | datasum += i 137 | if datasum == 0: 138 | self._cache = None 139 | 140 | def _parse_data(self): 141 | data = self._cache 142 | temp, humid = "".join(map(chr, data)).replace("T=", "").replace("H=", "").rstrip(' \t\r\n\0').split(" ") 143 | res = {} 144 | res[MI_TEMPERATURE] = temp 145 | res[MI_HUMIDITY] = humid 146 | return res 147 | 148 | 149 | def write_readnotif_ble(mac, handle, value, retries=3, timeout=20, adapter='hci0'): 150 | """ 151 | Write to a BLE address 152 | 153 | @param: mac - MAC address in format XX:XX:XX:XX:XX:XX 154 | @param: handle - BLE characteristics handle in format 0xXX 155 | @param: value - value to write to the given handle 156 | @param: timeout - timeout in seconds 157 | """ 158 | 159 | global LOCK 160 | attempt = 0 161 | delay = 10 162 | LOGGER.debug("Enter write_readnotif_ble (%s)", current_thread()) 163 | 164 | while attempt <= retries: 165 | cmd = "gatttool --device={} --char-write-req -a {} -n {} --adapter={} --listen".format(mac, handle, value, adapter) 166 | with LOCK: 167 | LOGGER.debug("Created lock in thread %s", current_thread()) 168 | LOGGER.debug("Running gatttool with a timeout of %s, cmd: %s", timeout, cmd) 169 | 170 | with Popen(cmd, shell=True, stdout=PIPE, preexec_fn=os.setsid) as process: 171 | try: 172 | result = process.communicate(timeout=timeout)[0] 173 | LOGGER.debug("Finished gatttool") 174 | except TimeoutExpired: 175 | # send signal to the process group 176 | os.killpg(process.pid, signal.SIGINT) 177 | result = process.communicate()[0] 178 | LOGGER.debug("Killed hanging gatttool") 179 | 180 | LOGGER.debug("Released lock in thread %s", current_thread()) 181 | 182 | result = result.decode("utf-8").strip(' \n\t') 183 | LOGGER.debug("Got %s from gatttool", result) 184 | # Parse the output 185 | res = re.search("( [0-9a-fA-F][0-9a-fA-F])+", result) 186 | if res: 187 | LOGGER.debug("Exit write_readnotif_ble with result (%s)", current_thread()) 188 | return [int(x, 16) for x in res.group(0).split()] 189 | 190 | attempt += 1 191 | LOGGER.debug("Waiting for %s seconds before retrying", delay) 192 | if attempt < retries: 193 | time.sleep(delay) 194 | delay *= 2 195 | 196 | LOGGER.debug("Exit write_readnotif_ble, no data (%s)", current_thread()) 197 | return None 198 | 199 | 200 | def read_ble(mac, handle, retries=3, timeout=20, adapter='hci0'): 201 | """ 202 | Read from a BLE address 203 | 204 | @param: mac - MAC address in format XX:XX:XX:XX:XX:XX 205 | @param: handle - BLE characteristics handle in format 0xXX 206 | @param: timeout - timeout in seconds 207 | """ 208 | 209 | global LOCK 210 | attempt = 0 211 | delay = 10 212 | LOGGER.debug("Enter read_ble (%s)", current_thread()) 213 | 214 | while attempt <= retries: 215 | cmd = "gatttool --device={} --char-read -a {} --adapter={}".format(mac, handle, adapter) 216 | with LOCK: 217 | LOGGER.debug("Created lock in thread %s", current_thread()) 218 | LOGGER.debug("Running gatttool with a timeout of %s, cmd: %s", timeout, cmd) 219 | 220 | with Popen(cmd, shell=True, stdout=PIPE, preexec_fn=os.setsid) as process: 221 | try: 222 | result = process.communicate(timeout=timeout)[0] 223 | LOGGER.debug("Finished gatttool") 224 | except TimeoutExpired: 225 | # send signal to the process group 226 | os.killpg(process.pid, signal.SIGINT) 227 | result = process.communicate()[0] 228 | LOGGER.debug("Killed hanging gatttool") 229 | 230 | LOGGER.debug("Released lock in thread %s", current_thread()) 231 | 232 | result = result.decode("utf-8").strip(' \n\t') 233 | LOGGER.debug("Got %s from gatttool", result) 234 | # Parse the output 235 | res = re.search("( [0-9a-fA-F][0-9a-fA-F])+", result) 236 | if res: 237 | LOGGER.debug("Exit read_ble with result (%s)", current_thread()) 238 | return [int(x, 16) for x in res.group(0).split()] 239 | 240 | attempt += 1 241 | LOGGER.debug("Waiting for %s seconds before retrying", delay) 242 | if attempt < retries: 243 | time.sleep(delay) 244 | delay *= 2 245 | 246 | LOGGER.debug("Exit read_ble, no data (%s)", current_thread()) 247 | return None 248 | -------------------------------------------------------------------------------- /Drivers/Xiaomi/Xiaomi Mijia DataCollector/mijia/mijia_v2_poller.py: -------------------------------------------------------------------------------- 1 | """" 2 | Read data from Mi Temp environmental (Temp and humidity) sensor. 3 | """ 4 | 5 | from datetime import datetime, timedelta 6 | import logging 7 | from threading import Lock 8 | from btlewrap.base import BluetoothInterface, BluetoothBackendException 9 | 10 | _HANDLE_READ_BATTERY_LEVEL = 0x001B 11 | _HANDLE_READ_FIRMWARE_VERSION = 0x0012 12 | _HANDLE_READ_NAME = 0x03 13 | _HANDLE_READ_WRITE_SENSOR_DATA = 0x001C 14 | 15 | MI_TEMPERATURE = "temperature" 16 | MI_HUMIDITY = "humidity" 17 | MI_BATTERY = "battery" 18 | 19 | # logging.basicConfig(level=logging.DEBUG) 20 | _LOGGER = logging.getLogger(__name__) 21 | 22 | 23 | class MijiaV2Poller: 24 | """" 25 | A class to read data from Mi Temp plant sensors. 26 | """ 27 | 28 | def __init__(self, mac, backend, cache_timeout=300, retries=3, adapter='hci0'): 29 | """ 30 | Initialize a Mi Temp Poller for the given MAC address. 31 | """ 32 | 33 | self._mac = mac 34 | self._bt_interface = BluetoothInterface(backend, adapter=adapter) 35 | self._cache = None 36 | self._cache_timeout = timedelta(seconds=cache_timeout) 37 | self._last_read = None 38 | self._fw_last_read = None 39 | self.retries = retries 40 | self.ble_timeout = 10 41 | self.lock = Lock() 42 | self._firmware_version = None 43 | self.battery = None 44 | 45 | def name(self): 46 | """Return the name of the sensor.""" 47 | with self._bt_interface.connect(self._mac) as connection: 48 | name = connection.read_handle(_HANDLE_READ_NAME) # pylint: disable=no-member 49 | 50 | if not name: 51 | raise BluetoothBackendException("Could not read NAME using handle %s from Mi Temp sensor %s" % (hex(_HANDLE_READ_NAME), self._mac)) 52 | return ''.join(chr(n) for n in name) 53 | 54 | def fill_cache(self): 55 | """Fill the cache with new data from the sensor.""" 56 | _LOGGER.debug('Filling cache with new sensor data.') 57 | try: 58 | self.firmware_version() 59 | except BluetoothBackendException: 60 | # If a sensor doesn't work, wait 2 minutes before retrying 61 | self._last_read = datetime.now() - self._cache_timeout + timedelta(seconds=120) 62 | raise 63 | 64 | with self._bt_interface.connect(self._mac) as connection: 65 | try: 66 | connection.wait_for_notification(_HANDLE_READ_WRITE_SENSOR_DATA, self, self.ble_timeout) # pylint: disable=no-member 67 | # If a sensor doesn't work, wait 2 minutes before retrying 68 | except BluetoothBackendException: 69 | self._last_read = datetime.now() - self._cache_timeout + timedelta(seconds=120) 70 | return 71 | 72 | def battery_level(self): 73 | """Return the battery level. 74 | 75 | The battery level is updated when reading the firmware version. This 76 | is done only once every 24h 77 | """ 78 | self.firmware_version() 79 | return self.battery 80 | 81 | def firmware_version(self): 82 | """Return the firmware version.""" 83 | if (self._firmware_version is None) or (datetime.now() - timedelta(hours=24) > self._fw_last_read): 84 | self._fw_last_read = datetime.now() 85 | with self._bt_interface.connect(self._mac) as connection: 86 | res_firmware = connection.read_handle(_HANDLE_READ_FIRMWARE_VERSION) # pylint: disable=no-member 87 | _LOGGER.debug('Received result for handle %s: %s', _HANDLE_READ_FIRMWARE_VERSION, res_firmware) 88 | res_battery = connection.read_handle(_HANDLE_READ_BATTERY_LEVEL) # pylint: disable=no-member 89 | _LOGGER.debug('Received result for handle %s: %d', _HANDLE_READ_BATTERY_LEVEL, res_battery) 90 | 91 | if res_firmware is None: 92 | self._firmware_version = None 93 | else: 94 | self._firmware_version = res_firmware.decode("utf-8") 95 | 96 | if res_battery is None: 97 | self.battery = 0 98 | else: 99 | self.battery = int(ord(res_battery)) 100 | return self._firmware_version 101 | 102 | def parameter_value(self, parameter, read_cached=True): 103 | """Return a value of one of the monitored paramaters. 104 | 105 | This method will try to retrieve the data from cache and only 106 | request it by bluetooth if no cached value is stored or the cache is 107 | expired. 108 | This behaviour can be overwritten by the "read_cached" parameter. 109 | """ 110 | # Special handling for battery attribute 111 | if parameter == MI_BATTERY: 112 | return self.battery_level() 113 | 114 | # Use the lock to make sure the cache isn't updated multiple times 115 | with self.lock: 116 | if (read_cached is False) or \ 117 | (self._last_read is None) or \ 118 | (datetime.now() - self._cache_timeout > self._last_read): 119 | self.fill_cache() 120 | else: 121 | _LOGGER.debug("Using cache (%s < %s)", datetime.now() - self._last_read, self._cache_timeout) 122 | 123 | if self.cache_available(): 124 | return self._parse_data()[parameter] 125 | raise BluetoothBackendException("Could not read data from Mi Temp sensor %s" % self._mac) 126 | 127 | def _check_data(self): 128 | """Ensure that the data in the cache is valid. 129 | 130 | If it's invalid, the cache is wiped. 131 | """ 132 | if not self.cache_available(): 133 | return 134 | 135 | parsed = self._parse_data() 136 | _LOGGER.debug('Received new data from sensor: Temp=%.1f, Humidity=%.1f', parsed[MI_TEMPERATURE], parsed[MI_HUMIDITY]) 137 | 138 | if parsed[MI_HUMIDITY] > 100: # humidity over 100 procent 139 | self.clear_cache() 140 | return 141 | 142 | def clear_cache(self): 143 | """Manually force the cache to be cleared.""" 144 | self._cache = None 145 | self._last_read = None 146 | 147 | def cache_available(self): 148 | """Check if there is data in the cache.""" 149 | return self._cache is not None 150 | 151 | def _parse_data(self): 152 | data = self._cache 153 | res = dict() 154 | res[MI_TEMPERATURE] = round(int.from_bytes([data[0], data[1]], "little")/100.0, 1) 155 | res[MI_HUMIDITY] = int.from_bytes([data[2]], "little") 156 | return res 157 | 158 | @staticmethod 159 | def _format_bytes(raw_data): 160 | """Prettyprint a byte array.""" 161 | if raw_data is None: 162 | return 'None' 163 | return ' '.join([format(c, "02x") for c in raw_data]).upper() 164 | 165 | def handleNotification(self, handle, raw_data): # pylint: disable=unused-argument,invalid-name 166 | """ gets called by the bluepy backend when using wait_for_notification 167 | """ 168 | if raw_data is None: 169 | return 170 | 171 | self._cache = raw_data 172 | self._check_data() 173 | if self.cache_available(): 174 | self._last_read = datetime.now() 175 | else: 176 | # If a sensor doesn't work, wait 2 minutes before retrying 177 | self._last_read = datetime.now() - self._cache_timeout + timedelta(seconds=120) 178 | -------------------------------------------------------------------------------- /Drivers/Xiaomi/Xiaomi Mijia DataCollector/send_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # pip3 install bluepy tendo 4 | import sys 5 | import urllib.request 6 | import base64 7 | import time 8 | import btlewrap 9 | import json 10 | from btlewrap.base import BluetoothBackendException 11 | from mijia.mijia_v1_poller import MijiaV1Poller, MI_HUMIDITY, MI_TEMPERATURE, MI_BATTERY 12 | from mijia.mijia_v2_poller import MijiaV2Poller, MI_HUMIDITY, MI_TEMPERATURE, MI_BATTERY 13 | import requests 14 | import logging 15 | from logging.handlers import RotatingFileHandler 16 | from tendo import singleton 17 | me = singleton.SingleInstance() 18 | 19 | # Configuration des logs 20 | logger = logging.getLogger() 21 | logger.setLevel(logging.INFO) 22 | formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(message)s') 23 | file_handler = RotatingFileHandler('send_data.log', 'a', 1000000, 1) 24 | file_handler.setLevel(logging.INFO) 25 | file_handler.setFormatter(formatter) 26 | logger.addHandler(file_handler) 27 | steam_handler = logging.StreamHandler() 28 | steam_handler.setLevel(logging.INFO) 29 | steam_handler.setFormatter(formatter) 30 | logger.addHandler(steam_handler) 31 | 32 | 33 | # Create virtual sensors in dummy hardware 34 | try: 35 | import bluepy.btle # noqa: F401 pylint: disable=unused-import 36 | BACKEND = btlewrap.BluepyBackend 37 | except ImportError: 38 | BACKEND = btlewrap.GatttoolBackend 39 | 40 | 41 | # Get Measurements from v1 and v2 devices 42 | def get_measurements(address, device): 43 | version = int(device['ver']) 44 | 45 | if 1 == version: 46 | poller = MijiaV1Poller(address) 47 | elif 2 == version: 48 | poller = MijiaV2Poller(address, BACKEND) 49 | else: 50 | logger.error("Unsupported Mijia sensor version") 51 | return '' 52 | 53 | loop = 0 54 | try: 55 | temp = poller.parameter_value(MI_TEMPERATURE) 56 | except: 57 | temp = "Not set" 58 | 59 | while loop < 2 and temp == "Not set": 60 | logger.warning('Error reading value retry after 3 seconds...') 61 | time.sleep(3) 62 | if 1 == version: 63 | poller = MijiaV1Poller(address) 64 | elif 2 == version: 65 | poller = MijiaV2Poller(address, BACKEND) 66 | loop += 1 67 | try: 68 | temp = poller.parameter_value(MI_TEMPERATURE) 69 | except: 70 | temp = "Not set" 71 | 72 | if temp == "Not set": 73 | # print("Error reading value\n") 74 | return '' 75 | 76 | data = {} 77 | data['type'] = device['type'] 78 | data['location'] = device['location'] 79 | data['label'] = device['label'] 80 | data['name'] = "{}".format(poller.name().strip('\u0000')) 81 | data['mac'] = address 82 | data['firmware'] = "{}".format(poller.firmware_version().strip('\u0000')) 83 | data['temperature'] = "{}".format(poller.parameter_value(MI_TEMPERATURE)) 84 | data['humidity'] = "{}".format(poller.parameter_value(MI_HUMIDITY)) 85 | data['battery'] = "{}".format(poller.parameter_value(MI_BATTERY)) 86 | json_data = json.dumps(data) 87 | return json_data 88 | 89 | 90 | def send_data(url, payload): 91 | header = {'content-type': 'application/json'} 92 | rc = requests.post(url, data=json.dumps(payload), headers=header, verify=False) 93 | return rc 94 | 95 | 96 | # List of devices and attributes 97 | sensor_list = { 98 | '4C:65:A8:D4:CA:5C': {'label': 'Sensor - Interior - Temp - Bedroom - Main', 'location': 'Bedroom', 'type': 'Sensor Temperature Humidity', 'ver': 1} 99 | } 100 | 101 | # Set the Hubitat IP 102 | he_url = 'http://HUBITAT-IP:39501' 103 | 104 | for k, v in sensor_list.items(): 105 | try: 106 | data = get_measurements(k, v) 107 | logger.debug("Name: %s (%s), data: %s" % (k, v['label'], data)) 108 | 109 | if data != '': 110 | rc = send_data(he_url, data) 111 | logger.info("Name: %s (%s), status_code: %s" % (k, v['label'], rc.status_code)) 112 | 113 | except: 114 | logger.error("Unexpected error: %s (%s) - %s" % (k, v['label'], sys.exc_info()[0])) 115 | -------------------------------------------------------------------------------- /Drivers/Xiaomi/Xiaomi Mijia.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) Sebastian YEPES 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 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 | 15 | import groovy.json.JsonSlurper 16 | import groovy.transform.Field 17 | 18 | @Field String VERSION = "1.0.2" 19 | 20 | @Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] 21 | @Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[2] 22 | 23 | metadata { 24 | definition (name: "Xiaomi Mijia", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Xiaomi/Xiaomi%20Mijia.groovy") { 25 | capability "Actuator" 26 | capability "Refresh" 27 | capability "Initialize" 28 | capability "Configuration" 29 | 30 | command "checkState" 31 | command "cleanChild" 32 | attribute "status", "string" 33 | } 34 | 35 | preferences { 36 | section { // General 37 | input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false 38 | input name: "stateCheckInterval", title: "State Check", description: "Device last seen check interval", type: "enum", options:[[0:"Disabled"], [5:"5min"], [10:"10min"], [15:"15min"], [20:"20min"], [30:"30min"], [60:"1h"], [120:"2h"]], defaultValue: 20, required: true 39 | } 40 | } 41 | } 42 | 43 | def installed() { 44 | logger("debug", "installed(${VERSION})") 45 | 46 | if (state.driverInfo == null || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 47 | state.driverInfo = [ver:VERSION] 48 | } 49 | 50 | if (state.deviceInfo == null) { 51 | state.deviceInfo = [:] 52 | } 53 | 54 | initialize() 55 | } 56 | 57 | def initialize() { 58 | logger("debug", "initialize()") 59 | } 60 | 61 | def updated() { 62 | logger("debug", "updated()") 63 | 64 | if (!state.driverInfo?.ver || state.driverInfo.isEmpty() || state.driverInfo.ver != VERSION) { 65 | installed() 66 | } 67 | 68 | if (!state.deviceInfo) { 69 | refresh() 70 | } 71 | 72 | unschedule() 73 | configure() 74 | } 75 | 76 | def refresh() { 77 | logger("debug", "refresh() - state: ${state.inspect()}") 78 | } 79 | 80 | def configure() { 81 | logger("debug", "configure()") 82 | 83 | if (stateCheckInterval.toInteger()) { 84 | schedule("0 */5 * ? * *", checkState) 85 | } 86 | } 87 | 88 | def cleanChild() { 89 | logger("debug", "cleanChild() - childDevices: ${childDevices?.size()}") 90 | childDevices?.each{ deleteChildDevice(it.deviceNetworkId) } 91 | } 92 | 93 | def clearState() { 94 | logger("debug", "ClearStates() - Clearing device states") 95 | 96 | state.clear() 97 | 98 | if (state?.driverInfo == null) { 99 | state.driverInfo = [:] 100 | } else { 101 | state.driverInfo.clear() 102 | } 103 | 104 | if (state?.deviceInfo == null) { 105 | state.deviceInfo = [:] 106 | } else { 107 | state.deviceInfo.clear() 108 | } 109 | } 110 | 111 | def parse(String description) { 112 | logger("trace", "parse() - description: ${description?.inspect()}") 113 | def result = [] 114 | 115 | def descMap = parseDescriptionAsMap(description) 116 | if (!descMap?.isEmpty()) { 117 | def body = parseJson(descMap?.body) 118 | if(body) { 119 | def cd = findOrCreateChild(body.type,body.mac, body.label, body.location) 120 | if (cd) { 121 | cd.parse(body) 122 | } 123 | } 124 | } 125 | 126 | logger("debug", "parse() - descMap: ${descMap?.inspect()} with result: ${result?.inspect()}") 127 | result 128 | } 129 | 130 | // Finds / Creates the child device 131 | private def findOrCreateChild(String type, String id, String label, String location) { 132 | logger("debug", "findOrCreateChild(${type},${id},${label},${location})") 133 | try { 134 | String thisId = device.id 135 | def cd = getChildDevice("${thisId}-${id}") 136 | if (!cd) { 137 | switch (type) { 138 | case "Sensor Temperature Humidity": 139 | cd = addChildDevice("Xiaomi Mijia - ${type} Child Device", "${thisId}-${id}", [name: "${type} - ${location}", label: "${label}", isComponent: true]) 140 | cd.sendEvent([name: "status", value: "online", displayed: true]) 141 | break 142 | default : 143 | logger("error", "findOrCreateChild(${type},${id},${label},${location}) - Device type not found") 144 | break 145 | } 146 | } 147 | return cd 148 | } catch (e) { 149 | logger("error", "findOrCreateChild(${type},${id},${label},${location}) - e: ${e}") 150 | } 151 | } 152 | 153 | private parseDescriptionAsMap(description) { 154 | logger("trace", "parseDescriptionAsMap() - description: ${description.inspect()}") 155 | try { 156 | def descMap = description.split(",").inject([:]) { map, param -> 157 | def nameAndValue = param.split(":") 158 | if (nameAndValue.length == 2){ 159 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 160 | } else { 161 | map += [(nameAndValue[0].trim()):""] 162 | } 163 | } 164 | 165 | def headers = new String(descMap["headers"]?.decodeBase64()) 166 | def status_code = headers?.tokenize('\r\n')[0] 167 | headers = headers?.tokenize('\r\n')?.toList()[1..-1]?.collectEntries{ 168 | it.split(":",2).with{ [ (it[0]): (it.size()<2) ? null : it[1] ?: null ] } 169 | } 170 | 171 | def body = new String(descMap["body"]?.decodeBase64()) 172 | def body_json 173 | logger("trace", "parseDescriptionAsMap() - headers: ${headers.inspect()}, body: ${body.inspect()}") 174 | 175 | if (body && body != "") { 176 | if(body.startsWith("\"{") || body.startsWith("{") || body.startsWith("\"[") || body.startsWith("[")) { 177 | JsonSlurper slurper = new JsonSlurper() 178 | body_json = slurper.parseText(body) 179 | logger("trace", "parseDescriptionAsMap() - body_json: ${body_json}") 180 | } 181 | } 182 | 183 | return [desc: descMap.subMap(['mac','ip','port']), status_code: status_code, headers:headers, body:body_json] 184 | } catch (e) { 185 | logger("error", "parseDescriptionAsMap() - ${e.inspect()}") 186 | return [:] 187 | } 188 | } 189 | 190 | def checkState() { 191 | logger("debug", "checkState()") 192 | def now = new Date() 193 | def cd_devices = getChildDevices() 194 | 195 | cd_devices.each { child -> 196 | def lastActivity = child.getLastActivity() 197 | def prev = Date.parse("yyyy-MM-dd HH:mm:ss","${lastActivity}".replace("+00:00","+0000")) 198 | long unxNow = (now.getTime()/1000) as long 199 | long unxPrev = (prev.getTime()/1000) as long 200 | long lastSeen = Math.abs((unxNow-unxPrev)/60) 201 | logger("debug", "checkState() - ${child} - lastActivity: ${lastActivity}, lastSeen: ${lastSeen}min") 202 | 203 | if (lastSeen > 0) { 204 | if (lastSeen > stateCheckInterval?.toInteger()) { 205 | logger("warn", "${child} - Is offline (lastSeen: ${lastSeen}min)") 206 | child.sendEvent([name: "status", value: "offline", displayed: true]) 207 | } else { 208 | child.sendEvent([name: "status", value: "online", displayed: true]) 209 | } 210 | } 211 | } 212 | } 213 | 214 | /** 215 | * @param level Level to log at, see LOG_LEVELS for options 216 | * @param msg Message to log 217 | */ 218 | private logger(level, msg) { 219 | if (level && msg) { 220 | Integer levelIdx = LOG_LEVELS.indexOf(level) 221 | Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel) 222 | if (setLevelIdx < 0) { 223 | setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL) 224 | } 225 | if (levelIdx <= setLevelIdx) { 226 | log."${level}" "${device.displayName} ${msg}" 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /Drivers/Xiaomi/Xiaomi Mijia.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Xiaomi Mijia", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2020-10-28", 6 | "documentationLink": "", 7 | "communityLink": "", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Xiaomi Mijia", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Xiaomi/Xiaomi%20Mijia.groovy", 15 | "version": "1.0.2", 16 | "required": true 17 | }, 18 | { 19 | "name": "Xiaomi Mijia - Sensor Temperature Humidity Child Device", 20 | "namespace": "syepes", 21 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Xiaomi/Xiaomi%20Mijia%20-%20Sensor%20Temperature%20Humidity%20Child%20Device.groovy", 22 | "version": "1.0.1", 23 | "required": true 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /Drivers/Zipato/Zipato Mini RFID Keypad.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Zipato Mini RFID Keypad", 3 | "minimumHEVersion": "2.2.3", 4 | "author": "Sebastian YEPES FERNANDEZ", 5 | "dateReleased": "2022-08-18", 6 | "documentationLink": "", 7 | "communityLink": "https://community.hubitat.com/t/release-zipato-mini-rfid-keypad/35119/1", 8 | "licenseFile": "https://github.com/syepes/Hubitat#license", 9 | "releaseNotes": "", 10 | "drivers": [ 11 | { 12 | "name": "Zipato Mini RFID Keypad", 13 | "namespace": "syepes", 14 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Zipato/Zipato%20Mini%20RFID%20Keypad.groovy", 15 | "version": "1.1.3", 16 | "required": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Hubitat Elevation® - Apps/Drivers & Tools 3 | ================ 4 | 5 | --- 6 | 7 | ## Drivers / Apps 8 | 9 | Vendor | Device | Model(Status) 10 | --- | --- | --- 11 | Aeotec | [Heavy Duty Smart Switch Gen5](https://aeotec.com/outdoor-z-wave-switch/) | ZW078(Working) 12 | Aeotec | [Range Extender 6+7](https://aeotec.com/z-wave-repeater/) | ZW117(Working) 13 | Aeotec | [MultiSensor 6](https://aeotec.com/z-wave-sensor/) | ZW100(Working FW: +1.13) 14 | Aeotec | [Water Sensor 6](https://aeotec.com/z-wave-water-sensor/) | ZW122(Working) 15 | Eurotronic | [Air Quality Sensor](https://eurotronic.org/produkte/sensoren/luftguetesensor/) | 700088(Working) 16 | Fibaro | [Smoke Sensor](https://manuals.fibaro.com/smoke-sensor/) | FGSD-002(Working) 17 | Popp | [Electric Strike Lock Control](https://www.popp.eu/products/actuators/strike-lock-control/) | 012501(Working) 18 | Popp | [Z-Rain](https://www.popp.eu/z-rain/) | 700168(Working) 19 | Heatit | [Z-Temp2](https://www.heatit.com/z-wave/heatit-z-temp-2-2//) | FW 1.01 (Working) 20 | Heltun | [Touch Panel Switch](https://www.heltun.com/z-wave-touch-panel-switch) | TPS01-05 FW 2.02(Working) 21 | LG | [WebOS TV](http://webostv.developer.lge.com/) | (Working) 22 | Netatmo | [Security - Doorbell](https://www.netatmo.com/en-us/security/doorbell) | Working with [limitations](https://forum.netatmo.com/viewtopic.php?f=5&t=18880) 23 | Netatmo | [Security - Smart Indoor Camera](https://www.netatmo.com/en-us/security/cam-indoor) | Working 24 | Netatmo | [Security - Smart Outdoor Camera](https://www.netatmo.com/en-us/security/cam-outdoor) | Working 25 | Netatmo | [Security - Smart Door and Window Sensor](https://www.netatmo.com/en-eu/security/cam-indoor/tag) | Working 26 | Netatmo | [Weather - Smart Home Weather Station](https://www.netatmo.com/en-us/security/cam-outdoor) | WIP (But don't own this device) 27 | Netatmo | [Weather - Smart Rain Gauge](https://www.netatmo.com/en-us/security/cam-outdoor) | WIP (But don't own this device) 28 | Netatmo | [Weather - Smart Anemometer](https://www.netatmo.com/en-us/security/cam-outdoor) | WIP (But don't own this device) 29 | Netatmo - Velux | [VELUX ACTIVE with NETATMO](https://www.netatmo.com/fr-fr/partners/velux) | Working (KIX 300), Special thanks to [ZTHawk](https://github.com/ZTHawk) for the decoding help. 30 | Orvibo | [Smart Temperature & Humidity Sensor](https://www.orvibo.com/en/product/temp_hum_sensor.html) | ST30 (Working) 31 | Panasonic | [Comfort Cloud - Air Conditioner](https://www.panasonic.com/global/hvac/air-conditioning/download_comfortcloud_app.html) | Working 32 | Qubino | [Flush Pilot Wire](https://cdn.shopify.com/s/files/1/0066/8149/3559/files/qubino-flush-pilot-wire-plus-user-manual-v1-1-eng.pdf) | ZMNHJD1(Working) 33 | Qubino | [Flush Shutter](https://qubino.com/products/flush-shutter/) | ZMNHCD1(Working)
Custom built for CMV / VMC usages: [1](https://www.domo-blog.fr/domotiser-vmc-avec-module-fibaro-fgr-222-223-jeedom/), [2](https://forum.jeedom.com/viewtopic.php?t=46694) 34 | Shelly Plus | [Plus 1PM](https://www.shelly.cloud/en-fr/products/product-overview/shelly-plus-1-pm), [Plus 2PM](https://www.shelly.cloud/en-fr/products/product-overview/shelly-plus-2-pm) | Plus 1+4PM(Working) 35 | ShellyPlus Pilot Wire | [Plus 2PM](https://www.shelly.cloud/en-fr/products/product-overview/shelly-plus-2-pm) + Two 1N4007 diodes | Working 36 | Schwaiger | [Thermostat - Temperature Sensor](http://www.schwaiger.de/en/temperature-sensor.html) | ZHD01(Working) 37 | Sonoff | [RF Bridge 433.9MHz](https://sonoff.tech/product/accessories/433-rf-bridge) | R2 V1.0 Tasmota + Portisch (Working) 38 | Warmup | [Smart WiFi Thermostat](https://my.warmup.com/home) | 6iE (Working) 39 | Xiaomi Mijia | BLE Temperature and Humidity Sensor | [v1](https://www.amazon.com/FOONEE-Hygrometer-Thermometer-Temperature-Screen-Remote/dp/B07HQJGF53) & [v2](https://www.amazon.com/gooplayer-Bluetooth-Thermometer-Wireless-Hygrometer/dp/B08619Y2QR) (Working with [external dependency](https://github.com/syepes/Hubitat/tree/master/Drivers/Xiaomi/Xiaomi%20Mijia%20DataCollector/)) 40 | Zipato | [Mini RFID Keypad](https://www.zipato.com/product/mini-keypad-rfid) | ZHD01(Working) 41 | 42 | ## Tools 43 | 44 | Name | Description | Status 45 | --- | --- | --- 46 | MetricLogger | Forwards all the state changes and metrics from devices to [VictoriaMetrics](https://victoriametrics.com/) | Working 47 | LokiLogLogger | Forwards all the Hub logs generated by the Hub to [Loki](https://grafana.com/oss/loki/) | Working 48 | LokiZigbeeLogger | Forwards all the Zigbee logs to [Loki](https://grafana.com/oss/loki/) | Working 49 | LokiZWaveLogger | Forwards all the ZWave logs to [Loki](https://grafana.com/oss/loki/) | Working 50 | 51 | ## Development and Contributions 52 | 53 | All Apps/Drivers are provided 'as is', I won't be responsible for any damages, bugs or liabilities whatsoever... 54 | If you have any idea for an improvement or find a bug do not hesitate in opening an issue. 55 | 56 | **Note:** All drivers and apps will automatically check for a new release "every 7 days at noon", you will find this information in the *State Variable:* driverInfo 57 | 58 | **Donations to support current or new device development are accepted via Paypal**: 59 | If your device is missing a driver I can develop it for you, if you send me a sample device ***:-)*** 60 | 61 | ## License 62 | 63 | All content is distributed under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) 64 | Copyright © 2022, [Sebastian YEPES](mailto:syepes@gmail.com) 65 | -------------------------------------------------------------------------------- /Tools/LokiSuphecap/LokiSuphacap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # https://www.suphammer.net/suphacap 4 | # pip3 install pyserial pytz tendo 5 | import serial 6 | import time 7 | import requests 8 | import json 9 | import datetime 10 | import pytz 11 | from tendo import singleton 12 | me = singleton.SingleInstance() 13 | 14 | ser = serial.Serial( 15 | port='/dev/ttyUSB0', 16 | baudrate=115200 17 | ) 18 | ser.reset_input_buffer() 19 | 20 | time.sleep(1) 21 | ser.write('c'.encode()) 22 | time.sleep(0.5) 23 | ser.write('q1 i1 o1\n'.encode()) 24 | time.sleep(1) 25 | 26 | url = 'http://192.168.1.1:3100/api/prom/push' 27 | headers = {'Content-type': 'application/json'} 28 | 29 | 30 | def gethex(decimal): 31 | return hex(int(decimal))[2:].upper() 32 | 33 | 34 | while 1: 35 | line = ser.readline().strip().decode() 36 | rec = line.split(',') 37 | # print('Length: ' + str(len(rec))) 38 | 39 | if len(rec) == 6 and rec[0] == 'r': 40 | # print('Raw: ' + str(rec)) 41 | try: 42 | curr_datetime = datetime.datetime.now(pytz.timezone('Europe/Paris')) 43 | curr_datetime = curr_datetime.isoformat('T') 44 | cc = rec[5] 45 | if cc == "": 46 | cc = 'NONE' 47 | 48 | payload = { 49 | 'streams': [ 50 | { 51 | 'labels': '{event_type=\"suphacap\",level=\"trace\",home_id=\"' + rec[2] + '\",node_src=\"' + gethex(rec[3]) + '\",node_dst=\"' + gethex(rec[4]) + '\",content=\"' + cc + '\"}', 52 | 'entries': [ 53 | { 54 | 'ts': curr_datetime, 55 | 'line': '[' + cc + '] [NONE] ' + rec[1] 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | payload = json.dumps(payload) 62 | # print(payload) 63 | 64 | answer = requests.post(url, data=payload, headers=headers) 65 | if answer.status_code >= 300: 66 | print(answer) 67 | 68 | except Exception as e: 69 | print('Error: ' + str(e)) 70 | 71 | if len(rec) == 10 and rec[0] == 'r': 72 | # print('Raw: ' + str(rec)) 73 | try: 74 | curr_datetime = datetime.datetime.now(pytz.timezone('Europe/Paris')) 75 | curr_datetime = curr_datetime.isoformat('T') 76 | cc = rec[5] 77 | if cc == "": 78 | cc = 'NONE' 79 | 80 | payload = { 81 | 'streams': [ 82 | { 83 | 'labels': '{event_type=\"suphacap\",level=\"trace\",home_id=\"' + rec[2] + '\",node_src=\"' + gethex(rec[3]) + '\",node_dst=\"' + gethex(rec[4]) + '\",content=\"' + cc + '\",crc_error=\"' + rec[7] + '\"}', 84 | 'entries': [ 85 | { 86 | 'ts': curr_datetime, 87 | 'line': '[' + cc + '] [' + rec[9] + '] ' + rec[1] 88 | } 89 | ] 90 | } 91 | ] 92 | } 93 | payload = json.dumps(payload) 94 | # print(payload) 95 | 96 | answer = requests.post(url, data=payload, headers=headers) 97 | if answer.status_code >= 300: 98 | print(answer) 99 | 100 | except Exception as e: 101 | print('Error: ' + str(e)) 102 | 103 | else: 104 | continue 105 | -------------------------------------------------------------------------------- /Tools/Monitoring/cfg/grafana/provisioning/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'Prometheus' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | editable: true 10 | options: 11 | path: /etc/grafana/provisioning/dashboards 12 | -------------------------------------------------------------------------------- /Tools/Monitoring/cfg/grafana/provisioning/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | # config file version 2 | apiVersion: 1 3 | 4 | # list of datasources that should be deleted from the database 5 | deleteDatasources: 6 | - name: Prometheus 7 | orgId: 1 8 | 9 | datasources: 10 | - name: Prometheus 11 | type: prometheus 12 | access: proxy 13 | url: http://vm:8428 14 | basicAuth: false 15 | isDefault: true 16 | jsonData: 17 | graphiteVersion: "1.1" 18 | tlsAuth: false 19 | tlsAuthWithCACert: false 20 | version: 1 21 | editable: true 22 | - name: Loki 23 | type: loki 24 | url: "http://loki:3100/" 25 | -------------------------------------------------------------------------------- /Tools/Monitoring/cfg/grafana/provisioning/notifiers/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Tools/Monitoring/cfg/grafana/provisioning/plugins/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Tools/Monitoring/cfg/loki.yml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | http_listen_port: 3100 5 | 6 | ingester: 7 | lifecycler: 8 | address: 127.0.0.1 9 | ring: 10 | kvstore: 11 | store: inmemory 12 | replication_factor: 1 13 | final_sleep: 0s 14 | chunk_idle_period: 1h # Any chunk not receiving new logs in this time will be flushed 15 | max_chunk_age: 1h # All chunks will be flushed when they hit this age, default is 1h 16 | chunk_target_size: 1048576 # Loki will attempt to build chunks up to 1.5MB, flushing first if chunk_idle_period or max_chunk_age is reached first 17 | chunk_retain_period: 30s # Must be greater than index read cache TTL if using an index cache (Default index read cache TTL is 5m) 18 | max_transfer_retries: 0 # Chunk transfers disabled 19 | 20 | schema_config: 21 | configs: 22 | - from: 2020-10-24 23 | store: boltdb-shipper 24 | object_store: filesystem 25 | schema: v11 26 | index: 27 | prefix: index_ 28 | period: 24h 29 | 30 | storage_config: 31 | boltdb_shipper: 32 | active_index_directory: /data/boltdb-shipper-active 33 | cache_location: /data/boltdb-shipper-cache 34 | cache_ttl: 24h # Can be increased for faster performance over longer query periods, uses more disk space 35 | shared_store: filesystem 36 | filesystem: 37 | directory: /data/chunks 38 | 39 | compactor: 40 | working_directory: /data/boltdb-shipper-compactor 41 | shared_store: filesystem 42 | 43 | limits_config: 44 | reject_old_samples: false 45 | reject_old_samples_max_age: 168h 46 | 47 | chunk_store_config: 48 | max_look_back_period: 0s 49 | 50 | table_manager: 51 | retention_deletes_enabled: false 52 | retention_period: 0s 53 | 54 | ruler: 55 | storage: 56 | type: local 57 | local: 58 | directory: /data/rules 59 | rule_path: /data/rules-temp 60 | alertmanager_url: http://localhost:9093 61 | ring: 62 | kvstore: 63 | store: inmemory 64 | enable_api: true 65 | enable_alertmanager_v2: true 66 | -------------------------------------------------------------------------------- /Tools/Monitoring/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | networks: 4 | data: 5 | driver: bridge 6 | 7 | services: 8 | vm-agt: 9 | image: victoriametrics/vmagent 10 | container_name: vm-agt 11 | hostname: vm-agt 12 | restart: always 13 | stop_grace_period: 5m 14 | networks: 15 | - net 16 | ports: 17 | - 8429:8429/tcp 18 | - 8189:8189/tcp 19 | volumes: 20 | - ./cfg/vmagent_prometheus.yml:/vmagent_prometheus.yml 21 | - ./data/vmagt:/storage 22 | command: 23 | - "-promscrape.config=/vmagent_prometheus.yml" 24 | - "-remoteWrite.tmpDataPath=/storage" 25 | - "-loggerLevel=INFO" 26 | - "-httpListenAddr=:8429" 27 | - "-influxListenAddr=:8189" 28 | - "-influxSkipSingleField" 29 | - "-remoteWrite.showURL" 30 | - "-remoteWrite.url=http://vm:8428/api/v1/write" 31 | - "-remoteWrite.url=https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push" 32 | - "-remoteWrite.basicAuth.username=USRID" 33 | - "-remoteWrite.basicAuth.password=TOKEN" 34 | vm: 35 | image: victoriametrics/victoria-metrics:latest 36 | container_name: vm 37 | hostname: vm 38 | restart: always 39 | stop_grace_period: 5m 40 | networks: 41 | - data 42 | ports: 43 | - 8428:8428/tcp 44 | volumes: 45 | - ./data/vm:/data 46 | command: 47 | - "-storageDataPath=/data" 48 | - "-loggerLevel=INFO" 49 | - "-loggerFormat=default" 50 | - "-httpListenAddr=:8428" 51 | - "-retentionPeriod=2y" 52 | - "-influxSkipSingleField" 53 | - "-selfScrapeInterval=30s" 54 | - "-inmemoryDataFlushInterval=15s" 55 | grafana: 56 | image: grafana/grafana:latest 57 | container_name: grafana 58 | hostname: grafana 59 | restart: always 60 | networks: 61 | - data 62 | ports: 63 | - 3000:3000 64 | environment: 65 | - GF_INSTALL_PLUGINS=flant-statusmap-panel 66 | - GF_SECURITY_ADMIN_USER=admin 67 | - GF_SECURITY_ADMIN_PASSWORD=admin 68 | - GF_USERS_ALLOW_SIGN_UP=false 69 | volumes: 70 | - ./cfg/grafana/provisioning/:/etc/grafana/provisioning/ 71 | - ./data/grafana:/var/lib/grafana 72 | depends_on: 73 | - vm 74 | - loki 75 | hubitat_exporter: 76 | image: syepes/hubitat_exporter:latest 77 | container_name: hubitat_exporter 78 | hostname: hubitat_exporter 79 | restart: always 80 | stop_grace_period: 3s 81 | networks: 82 | - net 83 | ports: 84 | - 8000:8000/tcp 85 | environment: 86 | - HE_IP=ABC 87 | - HE_APP_ID=ABC 88 | - HE_API_TOKEN=ABC 89 | - HE_AUTH_USR=ABC 90 | - HE_AUTH_PWD=ABC 91 | - HE_DD=true 92 | -------------------------------------------------------------------------------- /Tools/Monitoring/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mkdir -p data/grafana/ data/loki/ &> /dev/null 3 | sudo chown -R 472:472 data/grafana &> /dev/null 4 | sudo chown -R 10001:10001 data/loki &> /dev/null 5 | docker-compose up -d -------------------------------------------------------------------------------- /deploy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # pip3 install python-dotenv requests simplejson 3 | 4 | try: 5 | # base 6 | import argparse 7 | import os 8 | import re 9 | # import subprocess 10 | import json 11 | import pathlib 12 | # dotenv 13 | from dotenv import load_dotenv, find_dotenv 14 | # import urllib.parse 15 | import time 16 | import requests 17 | import http.cookiejar 18 | import sys 19 | except ModuleNotFoundError as e: 20 | print("ModuleNotFoundError exception while attempting to import the needed modules: " + str(e)) 21 | exit(99) 22 | 23 | 24 | def find_files(find='.'): 25 | data = [] 26 | for root, directories, filenames in os.walk(find): 27 | for filename in filenames: 28 | data.append({'name': filename.replace('.groovy', ''), 'path': os.path.join(root, filename)}) 29 | return data 30 | 31 | 32 | def merge_data(local, remote): 33 | data = [] 34 | for l in local: 35 | # print(l) 36 | for h in remote: 37 | if h['name'] == l['name']: 38 | data.append(dict(l, **h)) 39 | return data 40 | 41 | 42 | def update_driver(s, drv): 43 | print("> Processing Driver: " + drv['name'] + " (" + str(drv['id']) + ")") 44 | response = s.get( 45 | url=he_url + "/driver/ajax/code", 46 | params={'id': drv['id']} 47 | ) 48 | # print(response.text) 49 | 50 | if (response.json()['status'] != "success"): 51 | print("\tFailed downloading") 52 | return None 53 | 54 | version = response.json()['version'] 55 | print("\tCurrent version: " + str(version)) 56 | 57 | print("\tUploading driver") 58 | with open(drv['path'], 'r') as f: 59 | sourceContents = f.read() 60 | 61 | response = s.post( 62 | url=he_url + "/driver/ajax/update", 63 | data={'id': drv['id'], 64 | 'version': version, 65 | 'source': sourceContents 66 | } 67 | ) 68 | # print(response.text) 69 | 70 | if(response.json()['status'] == "success"): 71 | print("\tSuccessfully uploaded") 72 | elif (response.json()['status'] == "error"): 73 | print("\tFailed uploading: " + response.json()['errorMessage']) 74 | return None 75 | else: 76 | print("\tFailed uploading: " + response.json()) 77 | return None 78 | 79 | 80 | def update_app(s, app): 81 | print("> Processing App: " + app['name'] + " (" + str(app['id']) + ")") 82 | response = s.get( 83 | url=he_url + "/app/ajax/code", 84 | params={'id': app['id']} 85 | ) 86 | # print(response.text) 87 | 88 | if (response.json()['status'] != "success"): 89 | print("\tFailed downloading") 90 | return None 91 | 92 | version = response.json()['version'] 93 | print("\tCurrent version: " + str(version)) 94 | 95 | print("\tUploading app") 96 | with open(app['path'], 'r') as f: 97 | sourceContents = f.read() 98 | 99 | response = s.post( 100 | url=he_url + "/app/ajax/update", 101 | data={'id': app['id'], 102 | 'version': version, 103 | 'source': sourceContents 104 | } 105 | ) 106 | # print(response.text) 107 | 108 | if(response.json()['status'] == "success"): 109 | print("\tSuccessfully uploaded") 110 | elif (response.json()['status'] == "error"): 111 | print("\tFailed uploading: " + response.json()['errorMessage']) 112 | return None 113 | else: 114 | print("\tFailed uploading: " + response.json()) 115 | return None 116 | 117 | 118 | def he_login(path): 119 | credentialStorageFolderPath = pathlib.Path(path, ".creds") 120 | cookieJarFilePath = pathlib.Path(credentialStorageFolderPath, "cookie-jar.txt") 121 | # print("str(cookieJarFilePath.resolve()): " + str(cookieJarFilePath.resolve())) 122 | 123 | session = requests.Session() 124 | cookieJarFilePath.resolve().parent.mkdir(parents=True, exist_ok=True) 125 | session.cookies = http.cookiejar.MozillaCookieJar(filename=str(cookieJarFilePath.resolve())) 126 | 127 | # Ensure that the cookie jar file exists and contains a working cookie to authenticate into the hubita web interface 128 | if os.path.isfile(session.cookies.filename): 129 | session.cookies.load(ignore_discard=True) 130 | else: 131 | # Collect username and password from the user 132 | print("Hubitat username: ") 133 | hubitatUsername = input() 134 | print("Hubitat password: ") 135 | hubitatPassword = input() 136 | print("Entered " + hubitatUsername + " and " + hubitatPassword) 137 | 138 | response = session.post( 139 | he_url + "/login", 140 | data={ 141 | 'username': hubitatUsername, 142 | 'password': hubitatPassword, 143 | 'submit': 'Login' 144 | } 145 | ) 146 | # print("cookies: " + str(response.cookies.get_dict())) 147 | session.cookies.save(ignore_discard=True) 148 | 149 | return session 150 | 151 | 152 | # ------------------------------------ MAIN 153 | load_dotenv(find_dotenv(), verbose=True) 154 | fs_base = pathlib.Path(os.getcwd()).resolve() 155 | 156 | he_url = os.getenv("HE_URL") 157 | if he_url == None: 158 | print("HE_URL is not defined in the .env file") 159 | exit(99) 160 | 161 | print("Connecting to: " + he_url) 162 | session = he_login(fs_base) 163 | 164 | 165 | # ------------------------------------ DRIVERS 166 | # Find Local Drivers 167 | local_drivers = find_files(pathlib.Path(fs_base, "Drivers").resolve()) 168 | # print(local_drivers) 169 | 170 | # Load Remote Drivers 171 | resp = session.get(url=he_url + "/driver/list/data") 172 | 173 | # Check loging session 174 | if 'X-Frame-Options' in resp.headers and resp.headers['X-Frame-Options'] == 'DENY': 175 | print("Your HE Login Session has expired or been reseted, delete the file: .creds/cookie-jar.txt") 176 | exit(1) 177 | 178 | he_drivers = resp.json() 179 | # print(he_drivers) 180 | 181 | # Filter out system drivers 182 | he_drivers_usr = [x for x in he_drivers if x['type'] == 'usr'] 183 | # print(he_drivers_usr) 184 | 185 | drvs = merge_data(he_drivers_usr, local_drivers) 186 | print("Found HE Drivers: " + str(len(drvs))) 187 | # print("Found HE Drivers: " + str(drv)) 188 | 189 | for d in drvs: 190 | update_driver(session, d) 191 | 192 | 193 | # ------------------------------------ APP 194 | # Find Local Apps 195 | local_apps = find_files(pathlib.Path(fs_base, "Apps").resolve()) 196 | # print(local_apps) 197 | 198 | # Load Remote Apps 199 | resp = session.get(url=he_url + "/app/list/data") 200 | 201 | # Check loging session 202 | if 'X-Frame-Options' in resp.headers and resp.headers['X-Frame-Options'] == 'DENY': 203 | print("Your HE Login Session has expired or been reseted, delete the file: .creds/cookie-jar.txt") 204 | exit(1) 205 | 206 | # print(resp) 207 | he_apps = resp.json() 208 | # print(he_apps) 209 | 210 | # Filter out system apps 211 | he_apps_usr = [x for x in he_apps if x['type'] == 'usr'] 212 | # print(he_apps_usr) 213 | 214 | apps = merge_data(he_apps_usr, local_apps) 215 | print("Found HE Apps: " + str(len(apps))) 216 | # print("Found HE Apps: " + str(apps)) 217 | 218 | for a in apps: 219 | update_app(session, a) 220 | 221 | 222 | exit(0) 223 | -------------------------------------------------------------------------------- /repository.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Sebastian YEPES FERNANDEZ", 3 | "gitHubUrl": "https://github.com/syepes/Hubitat/", 4 | "payPalUrl": "https://paypal.me/syepesf?locale.x=en_US", 5 | "packages": [ 6 | { 7 | "name": "LG WebOS TV", 8 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/LG/LG%20WebOS%20TV%20Discovery.json", 9 | "description": "", 10 | "category": "Integrations", 11 | "tags": [ 12 | "LAN", 13 | "Multimedia" 14 | ] 15 | }, 16 | { 17 | "name": "Netatmo", 18 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/Netatmo/Netatmo.json", 19 | "description": "", 20 | "category": "Integrations", 21 | "tags": [ 22 | "LAN", 23 | "Cloud", 24 | "Safety & Security" 25 | ] 26 | }, 27 | { 28 | "name": "Netatmo - Velux", 29 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/Netatmo/Netatmo%20-%20Velux.json", 30 | "description": "", 31 | "category": "Integrations", 32 | "tags": [ 33 | "LAN", 34 | "Cloud", 35 | "Safety & Security", 36 | "Doors & Windows" 37 | ] 38 | }, 39 | { 40 | "name": "MetricLogger", 41 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/VictoriaMetrics/MetricLogger.json", 42 | "description": "", 43 | "category": "Integrations", 44 | "tags": [ 45 | "LAN", 46 | "Monitoring", 47 | "Tools & Utilities" 48 | ] 49 | }, 50 | { 51 | "name": "Aeotec Heavy Duty Smart Switch", 52 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Aeotec/Aeotec%20Heavy%20Duty%20Smart%20Switch.json", 53 | "description": "", 54 | "category": "Utility", 55 | "tags": [ 56 | "ZWave", 57 | "Lights & Switches" 58 | ] 59 | }, 60 | { 61 | "name": "Aeotec MultiSensor 6", 62 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Aeotec/Aeotec%20MultiSensor%206.json", 63 | "description": "", 64 | "category": "Utility", 65 | "tags": [ 66 | "ZWave", 67 | "Multi Sensors" 68 | ] 69 | }, 70 | { 71 | "name": "Aeotec Water Sensor 6", 72 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Aeotec/Aeotec%20Water%20Sensor%206.json", 73 | "description": "", 74 | "category": "Utility", 75 | "tags": [ 76 | "ZWave", 77 | "Multi Sensors" 78 | ] 79 | }, 80 | { 81 | "name": "Fibaro Smoke Sensor", 82 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Fibaro/Fibaro%20Smoke%20Sensor.json", 83 | "description": "", 84 | "category": "Utility", 85 | "tags": [ 86 | "ZWave", 87 | "Safety & Security" 88 | ] 89 | }, 90 | { 91 | "name": "Z-Wave Range Extender", 92 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Generic/Z-Wave%20Repeater.json", 93 | "description": "", 94 | "category": "Utility", 95 | "tags": [ 96 | "ZWave", 97 | "Repeaters & Extenders" 98 | ] 99 | }, 100 | { 101 | "name": "Heatit Z-Temp2", 102 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Heatit/Heatit%20Z-Temp2.json", 103 | "description": "", 104 | "category": "Utility", 105 | "tags": [ 106 | "ZWave", 107 | "Temperature & Humidity" 108 | ] 109 | }, 110 | { 111 | "name": "LokiLogLogger", 112 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Loki/LokiLogLogger.json", 113 | "description": "", 114 | "category": "Integrations", 115 | "tags": [ 116 | "LAN", 117 | "Monitoring", 118 | "Tools & Utilities" 119 | ] 120 | }, 121 | { 122 | "name": "LokiZWaveLogger", 123 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Loki/LokiZWaveLogger.json", 124 | "description": "", 125 | "category": "Integrations", 126 | "tags": [ 127 | "LAN", 128 | "ZWave", 129 | "Monitoring", 130 | "Tools & Utilities" 131 | ] 132 | }, 133 | { 134 | "name": "LokiZigbeeLogger", 135 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Loki/LokiZigbeeLogger.json", 136 | "description": "", 137 | "category": "Integrations", 138 | "tags": [ 139 | "LAN", 140 | "Zigbee", 141 | "Monitoring", 142 | "Tools & Utilities" 143 | ] 144 | }, 145 | { 146 | "name": "Popp Electric Strike Lock Control", 147 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Popp/Popp%20Electric%20Strike%20Lock%20Control.json", 148 | "description": "", 149 | "category": "Utility", 150 | "tags": [ 151 | "ZWave", 152 | "Locks" 153 | ] 154 | }, 155 | { 156 | "name": "Popp Z-Rain Sensor", 157 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Popp/Popp%20Z-Rain%20Sensor.json", 158 | "description": "", 159 | "category": "Utility", 160 | "tags": [ 161 | "ZWave", 162 | "Weather", 163 | "Water" 164 | ] 165 | }, 166 | { 167 | "name": "Qubino Flush Pilot Wire", 168 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Qubino/Qubino%20Flush%20Pilot%20Wire.json", 169 | "description": "", 170 | "category": "Utility", 171 | "tags": [ 172 | "ZWave", 173 | "Lights & Switches" 174 | ] 175 | }, 176 | { 177 | "name": "Qubino Flush Shutter - CMV", 178 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Qubino/Qubino%20Flush%20Shutter%20-%20CMV.json", 179 | "description": "", 180 | "category": "Utility", 181 | "tags": [ 182 | "ZWave", 183 | "Lights & Switches" 184 | ] 185 | }, 186 | { 187 | "name": "Schwaiger Temperature Sensor", 188 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Schwaiger/Schwaiger%20Temperature%20Sensor.json", 189 | "description": "", 190 | "category": "Utility", 191 | "tags": [ 192 | "ZWave", 193 | "Temperature & Humidity" 194 | ] 195 | }, 196 | { 197 | "name": "Sonoff RF Bridge", 198 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/Sonoff/Sonoff%20RF%20Bridge.json", 199 | "description": "", 200 | "category": "Integrations", 201 | "tags": [ 202 | "LAN", 203 | "IR & RF", 204 | "Window Coverings", 205 | "Lights & Switches" 206 | ] 207 | }, 208 | { 209 | "name": "Xiaomi Mijia", 210 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Xiaomi/Xiaomi%20Mijia.json", 211 | "description": "", 212 | "category": "Integrations", 213 | "tags": [ 214 | "LAN", 215 | "Temperature & Humidity" 216 | ] 217 | }, 218 | { 219 | "name": "Zipato Mini RFID Keypad", 220 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Zipato/Zipato%20Mini%20RFID%20Keypad.json", 221 | "description": "", 222 | "category": "Utility", 223 | "tags": [ 224 | "ZWave", 225 | "Safety & Security" 226 | ] 227 | }, 228 | { 229 | "name": "Eurotronic Air Quality Sensor", 230 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Eurotronic/Eurotronic%20Air%20Quality%20Sensor.json", 231 | "description": "", 232 | "category": "Integrations", 233 | "tags": [ 234 | "ZWave", 235 | "Temperature & Humidity" 236 | ] 237 | }, 238 | { 239 | "name": "Panasonic - Comfort Cloud", 240 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/PanasonicComfortCloud/Panasonic%20-%20Comfort%20Cloud.json", 241 | "description": "", 242 | "category": "Integrations", 243 | "tags": [ 244 | "LAN", 245 | "Cloud", 246 | "Climate Control", 247 | "Temperature & Humidity" 248 | ] 249 | }, 250 | { 251 | "name": "Warmup - Cloud", 252 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Apps/Warmup/Warmup%20-%20Cloud.json", 253 | "description": "", 254 | "category": "Integrations", 255 | "tags": [ 256 | "LAN", 257 | "Cloud", 258 | "Climate Control", 259 | "Climate Control", 260 | "Temperature & Humidity" 261 | ] 262 | }, 263 | { 264 | "name": "ShellyPlus Generic", 265 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Shelly/ShellyPlus%20Generic.json", 266 | "description": "", 267 | "category": "Integrations", 268 | "tags": [ 269 | "LAN", 270 | "Cloud", 271 | "Lights & Switches", 272 | "Temperature" 273 | ] 274 | }, 275 | { 276 | "name": "ShellyPlus Pilot Wire", 277 | "location": "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/Shelly/ShellyPlus%20Pilot%20Wire.json", 278 | "description": "", 279 | "category": "Integrations", 280 | "tags": [ 281 | "LAN", 282 | "Cloud", 283 | "Climate Control", 284 | "Lights & Switches", 285 | "Temperature" 286 | ] 287 | } 288 | ] 289 | } --------------------------------------------------------------------------------