/**
* Sensibo Integration for Hubitat
*
* Copyright 2021 Paul Hutton
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* Date Comments
* 2021-02-15 Forked from Bryan Li's port from ST
* 2021-02-28 Resolved old namespace issue (thanks benmek)
* 2024-03-27 Significant updates, support thermostat capabilities
*/
//file:noinspection GroovySillyAssignment
//file:noinspection GrDeprecatedAPIUsage
//file:noinspection GroovyDoubleNegation
//file:noinspection GroovyUnusedAssignment
//file:noinspection unused
//file:noinspection SpellCheckingInspection
//file:noinspection GroovyFallthrough
//file:noinspection GrMethodMayBeStatic
//file:noinspection GroovyAssignabilityCheck
//file:noinspection UnnecessaryQualifiedReference
import groovy.json.*
import groovy.transform.Field
import groovy.transform.CompileStatic
definition(
name: "Sensibo Integration",
namespace: "velowulf",
author: "Paul Hutton",
description: "Connect your Sensibo Pod to Hubitat.",
category: "Green Living",
iconUrl: "", // empty string - not implemented in Hubitat
iconX2Url: "", // empty string - not implemented in Hubitat
iconX3Url: "", // empty string - not implemented in Hubitat
singleInstance: true)
preferences{
page(name: "SelectAPIKey", title: "API Key", content: "setAPIKey", nextPage: "deviceList", install: false, uninstall: true)
page(name: "deviceList", title: "Sensibo", content:"SensiboPodList", install:true, uninstall: true)
page(name: "timePage")
page(name: "timePageEvent")
page((sNM):sPDPC)
}
// Initial variable declarations
static String getServerUrl(){ return 'https://home.sensibo.com' }
static String getChildNamespace(){ return 'velowulf' }
static String getChildTypeName(){ return 'SensiboPod' }
String getApiKey(){ return (String)settings.apiKey }
//static Long getPollRateMillis(){ return 45000L }
//static Long getCapabilitiesRateMillis(){ return 600000L }
static String version(){ return 'Hubitat' }
@Field static final String devVersionFLD = '2.0.0.0'
@Field static final String sNULL = (String)null
@Field static final String sSPACE = ' '
@Field static final String sLINEBR = '
'
@Field static final String sTRUE = 'true'
@Field static final String sFALSE = 'false'
@Field static final String sCLRRED = 'red'
@Field static final String sCLRGRY = 'gray'
@Field static final String sCLRORG = 'orange'
@Field static final String sAPPJSON = 'application/json'
@Field static final String sPDPC = 'pageDumpPCache'
@Field static final String sBLK=''
@Field static final Integer iZ=0
@Field static final Integer i1=1
@Field static final String sNM='name'
@Field static final String sTIT='title'
// Capture APIkey and logging preference
def setAPIKey(){
logTrace( "setAPIKey()")
String keystring= getApiKey() ?: ''
dynamicPage(name: "SelectAPIKey", title: "Enter your API Key", uninstall: true){
section("API Key"){
paragraph "Please enter your API Key provided by Sensibo \n\nAvailable at: \nhttps://home.sensibo.com/me/api"
input(name: "apiKey", title:"", type: "text", required:true, multiple:false, description: "", defaultValue: keystring)
}
section("Logging"){
paragraph "Application Logging Level"
input "logInfo", "bool", title: "Show Info Logs?", required: false, defaultValue: true
input "logWarn", "bool", title: "Show Warning Logs?", required: false, defaultValue: true
input "logError", "bool", title: "Show Error Logs?", required: false, defaultValue: true
input "logDebug", "bool", title: "Show Debug Logs?", description: "Only leave on when required", required: false, defaultValue: false
input "logTrace", "bool", title: "Show Detailed Logs?", description: "Only Enabled when asked by the developer", required: false, defaultValue: false
}
if((Boolean)settings.logTrace){
section('Debug'){
href sPDPC,(sTIT):'Dump Caches',description:sBLK
}
}
}
}
def SensiboPodList()
{
logTrace( "SensiboPodList()")
//logDebug( "apiKey: "+getApiKey() )
Map stats= getSensiboPodList()
//logDebug( "device list: $stats")
dynamicPage(name: "deviceList", title: "Select Your Sensibo Pod", uninstall: true){
section(""){
paragraph "Tap below to see the list of Sensibo Pods available in your Sensibo account and select the ones you want to connect to Hubitat."
input(name: "SelectedSensiboPods", title:"Pods", type: "enum", required:true, multiple:true, description: "Tap to choose", options: stats)
}
section("Refresh"){
input(name:"refreshinminutes", title: "Refresh rates in minutes", type: "enum", required:false, multiple: false, options: ["1", "5", "10","15","30"])
}
/*
section("Receive Pod sensors infos"){
input "boolnotifevery", "bool",submitOnChange: true, required: false, title: "Receive temperature, humidity and battery level notification every hour?"
href(name: "toTimePageEvent", page: "timePageEvent", title:"Only during a certain time", require: false)
}
section("Alert on sensors (threshold)"){
input "sendPushNotif", "bool",submitOnChange: true, required: false, title: "Receive alert on Sensibo Pod sensors based on threshold?"
}
if(sendPushNotif){
section("Select the temperature threshold",hideable: true){
input "minTemperature", "decimal", title: "Min Temperature",required:false
input "maxTemperature", "decimal", title: "Max Temperature",required:false
}
section("Select the humidity threshold",hideable: true){
input "minHumidity", "decimal", title: "Min Humidity level",required:false
input "maxHumidity", "decimal", title: "Max Humidity level",required:false
}
section("How frequently?"){
input(name:"days", title: "Only on certain days of the week", type: "enum", required:false, multiple: true, options: ["Monday", "Tuesday", "Wednesday","Thursday","Friday","Saturday","Sunday"])
}
section(""){
href(name: "toTimePage", page: "timePage", title:"Only during a certain time", require: false)
}
}
*/
}
}
/*
// page def must include a parameter for the params map!
def timePage(){
dynamicPage(name: "timePage", uninstall: false, install: false, title: "Only during a certain time"){
section(""){
input(name: "startTime", title: "Starting at : ", required:false, multiple: false, type:"time",)
input(name: "endTime", title: "Ending at : ", required:false, multiple: false, type:"time")
}
}
}
// page def must include a parameter for the params map!
def timePageEvent(){
dynamicPage(name: "timePageEvent", uninstall: false, install: false, title: "Only during a certain time"){
section(""){
input(name: "startTimeEvent", title: "Starting at : ", required:false, multiple: false, type:"time",)
input(name: "endTimeEvent", title: "Ending at : ", required:false, multiple: false, type:"time")
}
}
}
*/
// Function that runs when the app is first installed
def installed(){
logTrace( "Installed() called with settings: ${settings}")
initialize()
/*def d= getChildDevices() // Can't find that this actually does anything - probably remove (d is returned by the initiatize function and is then overwritten by this definition
if(boolnotifevery){
//runEvery1Hour("hournotification")
schedule("0 0 * * * ?", "hournotification")
}
logDebug( "Configured health checkInterval when installed()")
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60)*/
//subscribe(d,"temperatureUnit",eTempUnitHandler)
/*if(sendPushNotif){
subscribe(d, "temperature", eTemperatureHandler)
subscribe(d, "humidity", eHumidityHandler)
}*/ // Can't find the sendPushNotif variable anywhere - probably remove code
}
// Function that runs when the app is updated
def updated(){
logTrace( "Updated() called with settings: ${settings}")
unschedule() // Remove any scheduled tasks. If method is called without parameters, all schedules will be removed.
unsubscribe() // Unsubscribe from events sent from a device or all event subscriptions.
//state.lastTemperaturePush= null
//state.lastHumidityPush= null
initialize()
/*def d= getChildDevices() // Can't find that this actually does anything - probably remove (d is returned by the initiatize function and is then overwritten by this definition
if(boolnotifevery){
//runEvery1Hour("hournotification")
schedule("0 0 * * * ?", "hournotification")
}
logDebug( "Configured health checkInterval when installed()")
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60)*/
//subscribe(d,"temperatureUnit",eTempUnitHandler)
/*if(sendPushNotif){
subscribe(d, "temperature", eTemperatureHandler)
subscribe(d, "humidity", eHumidityHandler)
}*/ // Can't find the sendPushNotif variable anywhere - probably remove code
}
// Function that runs when the app is removed (uninstalled)
def uninstalled(){
// remove child devices
logTrace "Uninstalled called with settings: ${settings}"
}
def initialize(){
logTrace( "initialize() called")
//logTrace( "key "+ getApiKey())
List devices= ((List)SelectedSensiboPods).collect{ dni ->
logDebug("Initializing " + dni)
def d
d= getChildDevice(dni) // grab child devices with the current collection entry
if(!d){ // the return is empty so no device exists in which case add it
Map.Entry name= getSensiboPodList().find( {key,value -> key == dni })
logDebug( "Pod : ${name.value} - Hub : ${(String)((List)location.hubs)[iZ].name} - Type : " + getChildTypeName() + " - Namespace : " + getChildNamespace())
d= addChildDevice(getChildNamespace(), getChildTypeName(), dni, ((List)location.hubs)[iZ].id, [
"label" : "${name.value} Pod",
"name" : "${name.value} Pod"
]
)
logTrace( "created ${d.displayName} with id $dni")
}else{ // Tell the user that the device already exists
logTrace( "found ${d.displayName} with id $dni already exists")
}
return d
}
logTrace( "found / created ${devices.size()} Sensibo Pod")
List delete
// Delete any that are no longer selected
if(!(List)SelectedSensiboPods){
logDebug( "Removing all Sensibo devices")
delete= getChildDevices()
}else{
delete= getChildDevices().findAll { !((List)SelectedSensiboPods).contains(it.deviceNetworkId) }
}
logTrace( "deleting ${delete.size()} Sensibo")
delete.each { deleteChildDevice(it.deviceNetworkId) }
resetSchedule()
//if(advLogsActive()){ runIn(1800, "logsOff") }
if(advLogsActive()){ runIn(28800, "logsOff") }
asyncReq('finishRefresh',true) // devices refresh
}
Boolean advLogsActive(){ return ((Boolean)settings.logDebug || (Boolean)settings.logTrace) }
void logsOff(){
app.updateSetting("logDebug",[value:sFALSE,type:"bool"])
app.updateSetting("logTrace",[value:sFALSE,type:"bool"])
log.debug "Disabling debug logs"
}
void resetSchedule(){
logTrace("resetSchedule()")
//Set up schedule for refreshing devices based on app preferences
String rfmins=(String)settings.refreshinminutes
logTrace( "refresh() called with rate of " + rfmins + " minutes")
if(rfmins == "1")
runEvery1Minute("refreshDevices")
else if(rfmins == "5")
runEvery5Minutes("refreshDevices")
else if(rfmins == "10")
runEvery10Minutes("refreshDevices")
else if(rfmins == "15")
runEvery15Minutes("refreshDevices")
else if(rfmins == "30")
runEvery30Minutes("refreshDevices")
else
runEvery10Minutes("refreshDevices")
}
Map restParams(String path, Map queryMap, Boolean intr=true, String jsonBody=sNULL, Boolean headers=false){
Map res
res=[
uri: "${getServerUrl()}",
path: path,
query: [apiKey: getApiKey(), type:"json"] + (intr ? [integration: version()] : [:]) + queryMap
]
if(headers) res= res + [headers: ["Content-Type": sAPPJSON]]
else res= res + [requestContentType: sAPPJSON]
if(jsonBody) res= res + [body: jsonBody]
res
}
private Long wnow(){ return (Long)now() }
void asyncReq(String callBack, Boolean doPod=false){
logTrace( "asyncReq")
Map deviceListParams= restParams('/api/v2/users/me/pods', [fields:"*"],true)
logDebug( "HTTP REQUEST ASYNC asyncReq")
try {
asynchttpGet('ahttpReqResp', deviceListParams, [command: callBack, doPod: doPod])
}catch(e){
logError("async exception",e)
}
}
@Field static final Integer i200=200
@Field static final Integer i300=300
@Field static final Integer i400=400
void ahttpReqResp(resp, Map callbackData){
logTrace( "attpReqResp")
String callBackC= (String)callbackData?.command
Boolean doPod= (Boolean)callbackData?.doPod
//logWarn( "callback: $callbackData")
Integer rCode; rCode=(Integer)resp.status
String erMsg; erMsg=''
if(resp.hasError()){
erMsg= " Response Status: ${resp.status} error Message: ${resp.getErrorMessage()}".toString()
logWarn(erMsg)
}
Boolean respOk=(rCode>=i200 && rCode=i300 && rCode res= (List