/**
* Soma Connect Driver for - Shades 2
*
* To get the mac address for your devices type this command into a browser
* replace the IP address with the IP address of your Soma Connect
* --- http://192.168.1.?:3000/list_devices ---
*
* Copyright 2021 Gassgs/ Gary Gassmann
*
*
* Based on the Hubitat community driver httpGetSwitch
* https://raw.githubusercontent.com/hubitat/HubitatPublic/master/examples/drivers/httpGetSwitch.groovy
*
* 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.
*
* Change History:
*
* V1.0.0 6-09-2021 first run
* V1.1.0 6-14-2021 improvments & added morning position option
* V1.2.0 6-17-2021 Seperate Tilt and Shade drivers
* V1.3.0 8-14-2021 Fixed opening/closing bug with no position change.
* V1.4.0 9-01-2021 Improvements for Soma firmware 2.2.9 stop level correction
* V1.5.0 9-09-2021 Improvements for windowShade attribute changes
* V1.6.0 9-21-2021 Changed Morning Position implementation and added info logging
* V1.7.0 10-5-2021 Changed Battery check to once per day at 4:00am (refresh command also checks battery level)
* V1.8.0 1-20-2023 Added support for Connect usb U1 device + bug fixes and improvements
*/
def driverVer() { return "1.8" }
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
metadata {
definition (name: "Soma Connect Shades 2", namespace: "Gassgs", author: "Gary G", importUrl: "https://raw.githubusercontent.com/Gassgs/Hubitat-Apps-and-Drivers/master/Drivers/Soma%20Connect/Soma%20Shades%202.groovy") {
capability "WindowShade"
capability "Switch"
capability "Switch Level"
capability "Change Level"
capability "Actuator"
capability "Refresh"
capability "Sensor"
capability "Battery"
command "setMorningPosition",[[name:"Position", type: "NUMBER",description: "Set Morning Position"]]
}
}
preferences {
input name: "usb", type: "bool", title: "Enable for U1 usb device", defaultValue: false
input name: "connectIp",type: "text", title: "Soma Connect or U1 device IP Address", required: true
input name: "mac",type: "text", title: "Mac address of Shade 2 device", required: true
input name: "timeout",type: "number", title: "Time it takes to fully open from closed", required: true, defaultValue: 10
input name: "logInfoEnable", type: "bool", title: "Enable info text logging", defaultValue: true
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
}
def logsOff() {
log.warn "debug logging disabled..."
device.updateSetting("logEnable", [value: "false", type: "bool"])
}
def updated() {
log.info "updated..."
log.warn "debug logging is: ${logEnable == true}"
state.DriverVersion=driverVer()
refresh()
if (logEnable) runIn(1800, logsOff)
schedule('0 0 4 * * ?',getBattery)
}
def open() {
if (logEnable) log.debug "Sending Open Command to [${settings.mac}]"
if (logInfoEnable) log.info "$device.label Sending Open Command"
state.position = 100
currentLevel = device.currentValue("level")
posChange = 100 - currentLevel
if (usb){
cmd = ":3000/open_shade?mac="
}else{
cmd = ":3000/open_shade/"
}
try {
httpGet("http://" + connectIp + cmd + mac) { resp ->
def json = (resp.data)
def msg = (resp.data as String)
if (usb){
def dataUsb = new JsonSlurper().parseText(msg)
json = dataUsb
}
if (logEnable) log.debug "${json}"
if (json.result == "error") {
if (logEnable) log.debug "Command -ERROR- from SOMA Connect- $json.msg"
}
if (json.result == "success") {
if (logEnable) log.debug "Command Success Response from SOMA Connect"
sendEvent(name: "windowShade", value: "opening", isStateChange: true)
if (posChange > 75){
timeout = timeout * 1 as Integer
}
else if (posChange > 50 && posChange <= 75){
timeout = timeout * 0.75 as Integer
}
else if (posChange > 25 && posChange <= 50){
timeout = timeout * 0.50 as Integer
}
else if (posChange <= 25){
timeout = timeout * 0.25 as Integer
}
runIn(timeout,getPosition)
}
}
} catch (Exception e) {
log.warn "Call to on failed: ${e.message}"
}
}
def close() {
if (logEnable) log.debug "Sending Close Command to [${settings.mac}]"
if (logInfoEnable) log.info "$device.label Sending Close Command"
state.position = 0
posChange = device.currentValue("level")
if (usb){
cmd = ":3000/close_shade?mac="
}else{
cmd = ":3000/close_shade/"
}
try {
httpGet("http://" + connectIp + cmd + mac) { resp ->
def json = (resp.data)
def msg = (resp.data as String)
if (usb){
def dataUsb = new JsonSlurper().parseText(msg)
json = dataUsb
}
if (logEnable) log.debug "${json}"
if (json.result == "error") {
if (logEnable) log.debug "Command -ERROR- from SOMA Connect- $json.msg"
}
if (json.result == "success") {
if (logEnable) log.debug "Command Success Response from SOMA Connect"
sendEvent(name: "windowShade", value: "closing", isStateChange: true)
if (posChange > 75){
timeout = timeout * 1 as Integer
}
else if (posChange > 50 && posChange <= 75){
timeout = timeout * 0.75 as Integer
}
else if (posChange > 25 && posChange <= 50){
timeout = timeout * 0.50 as Integer
}
else if (posChange <= 25){
timeout = timeout * 0.25 as Integer
}
runIn(timeout,getPosition)
}
}
} catch (Exception e) {
log.warn "Call to on failed: ${e.message}"
}
}
def on() {
open()
}
def off() {
close()
}
def stopPositionChange() {
if (logEnable) log.debug "Sending Stop Command to [${settings.mac}]"
if (logInfoEnable) log.info "$device.label Sending Stop Command"
if (usb){
cmd = ":3000/stop_shade?mac="
}else{
cmd = ":3000/stop_shade/"
}
try {
httpGet("http://" + connectIp + cmd + mac) { resp ->
def json = (resp.data)
def msg = (resp.data as String)
if (usb){
def dataUsb = new JsonSlurper().parseText(msg)
json = dataUsb
}
if (logEnable) log.debug "${json}"
if (json.result == "error") {
if (logEnable) log.debug "Command -ERROR- from SOMA Connect- $json.msg"
}
if (json.result == "success") {
if (logEnable) log.debug "Command Success Response from SOMA Connect"
sendEvent(name: "windowShade", value: "stopped", isStateChange: true)
runIn(1,getStopPosition)
}
}
} catch (Exception e) {
log.warn "Call to on failed: ${e.message}"
}
}
def stopLevelChange() {
stopPositionChange()
}
def setPosition(value) {
if (logEnable) log.debug "Sending Set Position Command to [${settings.mac}]"
if (logInfoEnable) log.info "$device.label Sending Set Position Command $value %"
state.position = value
currentLevel = device.currentValue("level")
if (value > currentLevel){
posChange = value - currentLevel
}else{
posChange = currentLevel - value
}
value = value.toInteger()
def position = 100 - value
if (usb){
cmd = ":3000/set_shade_position?mac="
posCmd = "&pos="
}else{
cmd = ":3000/set_shade_position/"
posCmd = "/"
}
try {
httpGet("http://" + connectIp + cmd + mac + posCmd + position) { resp ->
def json = (resp.data)
def msg = (resp.data as String)
if (usb){
def dataUsb = new JsonSlurper().parseText(msg)
json = dataUsb
}
if (logEnable) log.debug "${json}"
if (json.result == "error") {
if (logEnable) log.debug "Command -ERROR- from SOMA Connect- $json.msg"
}
if (json.result == "success") {
if (logEnable) log.debug "Command Success Response from SOMA Connect"
if (value == device.currentValue("level")){
if (logEnable) log.debug "No change needed"
}
else if (value > device.currentValue("level")){
sendEvent(name: "windowShade", value: "opening", isStateChange: true)
if (posChange > 75){
timeout = timeout * 1 as Integer
}
else if (posChange > 50 && posChange <= 75){
timeout = timeout * 0.75 as Integer
}
else if (posChange > 25 && posChange <= 50){
timeout = timeout * 0.50 as Integer
}
else if (posChange <= 25){
timeout = timeout * 0.25 as Integer
}
runIn(timeout,getPosition)
}
else{
sendEvent(name: "windowShade", value: "closing", isStateChange: true)
if (posChange > 75){
timeout = timeout * 1 as Integer
}
else if (posChange > 50 && posChange <= 75){
timeout = timeout * 0.75 as Integer
}
else if (posChange > 25 && posChange <= 50){
timeout = timeout * 0.50 as Integer
}
else if (posChange <= 25){
timeout = timeout * 0.25 as Integer
}
runIn(timeout,getPosition)
}
}
}
} catch (Exception e) {
log.warn "Call to on failed: ${e.message}"
}
}
def setLevel(value) {
setPosition(value)
}
def startPositionChange(direction) {
if (direction == "open") {
open()
} else {
close()
}
}
def startLevelChange(direction) {
if (direction == "up") {
open()
} else {
close()
}
}
def setMorningPosition(value) {
if (usb){
log.warn "$device.label - Morning Position command not supported on U1 usb device"
sendEvent(name:"windowShade",value:"command not supported")
runIn(3,refresh)
}
else{
if (logEnable) log.debug "Sending Set Moring Position Command to [${settings.mac}]"
if (logInfoEnable) log.info "$device.label Sending Set Morning Position Command $value %"
state.position = value
currentLevel = device.currentValue("level")
if (value > currentLevel){
posChange = value - currentLevel
}else{
posChange = currentLevel - value
}
value = value.toInteger()
def newPosition = 100 - value
try {
httpGet("http://" + connectIp + ":3000/set_shade_position/" + mac + "/"+ newPosition + "?morning_mode=1") { resp ->
def json = (resp.data)
if (logEnable) log.debug "${json}"
if (json.result == "error") {
if (logEnable) log.debug "Command -ERROR- from SOMA Connect- $json.msg"
}
if (json.result == "success") {
if (logEnable) log.debug "Command Success Response from SOMA Connect"
if (value > device.currentValue("level")){
sendEvent(name: "windowShade", value: "opening",isStateChange: true)
if (posChange > 75){
timeout = timeout * 5 as Integer
}
else if (posChange > 50 && posChange <= 75){
timeout = (timeout * 0.75) * 5 as Integer
}
else if (posChange > 25 && posChange <= 50){
timeout = (timeout * 0.50) * 5 as Integer
}
else if (posChange <= 25){
timeout = (timeout * 0.25) * 5 as Integer
}
runIn(timeout,getPosition)
}
else{
sendEvent(name: "windowShade", value: "closing", isStateChange: true)
if (posChange > 75){
timeout = timeout * 5 as Integer
}
else if (posChange > 50 && posChange <= 75){
timeout = (timeout * 0.75) * 5 as Integer
}
else if (posChange > 25 && posChange <= 50){
timeout = (timeout * 0.50) * 5 as Integer
}
else if (posChange <= 25){
timeout = (timeout * 0.25) * 5 as Integer
}
runIn(timeout,getPosition)
}
}
}
} catch (Exception e) {
log.warn "Call to on failed: ${e.message}"
}
}
}
def getPosition() {
if (logEnable) log.debug "Checking Shade Position"
shadeValue = state.position
if (usb){
cmd = ":3000/get_shade_state?mac="
}else{
cmd = ":3000/get_shade_state/"
}
try {
httpGet("http://" + connectIp + cmd + mac) { resp ->
def json = (resp.data)
def msg = (resp.data as String)
if (usb){
def dataUsb = new JsonSlurper().parseText(msg)
json = dataUsb
}
if (logEnable) log.debug "${json}"
def shadePos = 100 - json.position
sendEvent(name: "position", value: shadeValue)
sendEvent(name: "level", value: shadeValue)
if (logEnable) log.debug "Shade Position set to ${shadePos}"
if (shadePos >= 95){
sendEvent(name: "windowShade", value: "open",isStateChange: true)
sendEvent(name: "switch", value: "on", isStateChange: true)
} else if (shadePos == 0) {
sendEvent(name: "windowShade", value: "closed",isStateChange: true)
sendEvent(name: "switch", value: "off", isStateChange: true)
} else {
sendEvent(name: "windowShade", value: "partially open",isStateChange: true)
sendEvent(name: "switch", value: "on", isStateChange: true)
}
}
} catch (Exception e) {
log.warn "Call to on failed: ${e.message}"
}
}
def getStopPosition() {
if (logEnable) log.debug "Checking Shade Position"
unschedule(getPosition)
unschedule(refresh)
if (usb){
cmd = ":3000/get_shade_state?mac="
}else{
cmd = ":3000/get_shade_state/"
}
try {
httpGet("http://" + connectIp + cmd + mac) { resp ->
def json = (resp.data)
def msg = (resp.data as String)
if (usb){
def dataUsb = new JsonSlurper().parseText(msg)
json = dataUsb
}
if (logEnable) log.debug "${json}"
def shadePos = 100 - json.position
sendEvent(name: "position", value: shadePos)
sendEvent(name: "level", value: shadePos)
if (logEnable) log.debug "Shade Position set to ${shadePos}"
if (shadePos >= 95){
sendEvent(name: "windowShade", value: "open",isStateChange: true)
sendEvent(name: "switch", value: "on", isStateChange: true)
} else if (shadePos == 0) {
sendEvent(name: "windowShade", value: "closed",isStateChange: true)
sendEvent(name: "switch", value: "off", isStateChange: true)
} else {
sendEvent(name: "windowShade", value: "partially open",isStateChange: true)
sendEvent(name: "switch", value: "on", isStateChange: true)
}
}
} catch (Exception e) {
log.warn "Call to on failed: ${e.message}"
}
}
def getBattery() {
if (logEnable) log.debug "Checking Battery Level"
if (usb){
cmd = ":3000/get_battery_level?mac="
}else{
cmd = ":3000/get_battery_level/"
}
try {
httpGet("http://" + connectIp + cmd + mac) { resp ->
def json = (resp.data)
def msg = (resp.data as String)
if (usb){
def dataUsb = new JsonSlurper().parseText(msg)
json = dataUsb
}
if (logEnable) log.debug "${json}"
def batteryPercent = json.battery_percentage
sendEvent(name: "battery", value: batteryPercent)
if (logEnable) log.debug "Battery level set to ${batteryPercent}"
if (logInfoEnable) log.info "$device.label Battery level is ${batteryPercent}"
}
} catch (Exception e) {
log.warn "Call to on failed: ${e.message}"
}
}
def refresh() {
getBattery()
getStopPosition()
}
def installed() {
log.info "installed..."
log.warn "debug logging is: ${logEnable == true}"
state.DriverVersion=driverVer()
refresh()
if (logEnable) runIn(1800, logsOff)
}