SleepIQ (Sleep Number)

I just had a few minutes to try it out and it seems to work with a few minor tweaks.

before all headers: lines in the app code (there are 5) put:
requestContentType: 'application/json',
contentType: 'application/json',

And also in the app code, change sleep(100) to Sleep(100)

The rest worked as is. It could probably use some other cleanup (it mentions Smarthings in the user visible text, etc) but nothing that seemed to hamper functionality.

1 Like

@rvrolyk can you tell me what are the headers in the app? I'm not a programmer by any means but trying to learn some. Thanks

@leeonestop

Here you go (you can compare it to the original to see where @rvrolyk's suggested changes are) :

Modified app:

/**
 *  Sleep Number Manager
 *
 *  Copyright 2019 Tim Parsons
 *
 *  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.
 *
 */
definition(
    name: "Sleep Number Manager",
    namespace: "ClassicTim1",
    author: "Classic_Tim",
    description: "Control your Sleep Number bed vai SmartThings. You can use it to raise, lower, and adjsut the pressure on each side of the bed seperately if it's split, or all together if it's not. If you want to do each side seperately you must use two devices for each side",
    category: "Convenience",
    iconUrl: "https://raw.githubusercontent.com/ClassicTim1/SleepNumberManager/master/icons/logo.jpg",
    iconX2Url: "https://raw.githubusercontent.com/ClassicTim1/SleepNumberManager/master/icons/logo.jpg",
    iconX3Url: "https://raw.githubusercontent.com/ClassicTim1/SleepNumberManager/master/icons/logo.jpg"
)


preferences {
  page name: "rootPage"
  page name: "findDevicePage"
  page name: "selectDevicePage"
  page name: "createDevicePage"
}

def rootPage() {
  log.trace "rootPage()"
  
  def devices = getChildDevices()
  
  dynamicPage(name: "rootPage", install: true, uninstall: true) {
    section("Settings") {
      input("login", "text", title: "Username", description: "Your SleepIQ username")
      input("password", "password", title: "Password", description: "Your SleepIQ password")
    }
    section("Devices") {
      if (devices.size() > 0) {
        devices.each { device ->
          paragraph title: device.label, "${device.currentBedId} / ${device.currentSide}"
        }
      }
      href "findDevicePage", title: "Create New Device", description: null
    }
      if(devices.size() > 0){
          section(""){
              paragraph title: "", "To remove a device remove it from the Things tab in SmartThings"
          }
      }
  }
}

def findDevicePage() {
  log.trace "findDevicePage()"

  def responseData = getBedData()
  log.debug "Response Data: $responseData"
  
  dynamicPage(name: "findDevicePage") {
    if (responseData.beds.size() > 0) {
      responseData.beds.each { bed ->
        section("Bed: ${bed.bedId}") {
          href "selectDevicePage", title: "Right Side", description: "Right side of the bed", params: [bedId: bed.bedId, side: "Right"]
          href "selectDevicePage", title: "Left Side", description: "Left side of the bed", params: [bedId: bed.bedId, side: "Left"]
        }
      }
    } else {
      section {
        paragraph "No Beds Found"
      }
    }
  }
}

def selectDevicePage(params) {
  log.trace "selectDevicePage()"
  
  settings.newDeviceName = null
  
  dynamicPage(name: "selectDevicePage") {
    section {
      paragraph "Bed ID: ${params.bedId}"
      paragraph "Side: ${params.side}"
      input "newDeviceName", "text", title: "Device Name", description: "Name this device", defaultValue: ""
    }
    section {
      href "createDevicePage", title: "Create Device", description: null, params: [bedId: params.bedId, side: params.side]
    }
  }
}

def createDevicePage(params) {
  log.trace "createDevicePage()"

  def deviceId = "sleepiq.${params.bedId}.${params.side}"
  def device = addChildDevice("sleepNumberBed", "Sleep Number Bed", deviceId, null, [label: settings.newDeviceName])
  device.setBedId(params.bedId)
  device.setSide(params.side)
  settings.newDeviceName = null
  rootPage()
  /*-dynamicPage(name: "selectDevicePage") {
    section {
      paragraph "Name: ${device.name}"
      paragraph "Label: ${device.label}"
      paragraph "Bed ID: ${device.curentBedId}"
      paragraph ": ${device.current}"
      paragraph "Presence: ${device.currentPresnce}"
    }
    section {
      href "rootPage", title: "Back to Device List", description: null
    }
  }-*/
}


def installed() {
  log.trace "installed()"
  initialize()
}

def updated() {
  log.trace "updated()"
  unsubscribe()
  unschedule()
  initialize()
}

def initialize() {
  log.trace "initialize()"
  getBedData()
}

def getBedData() {
  log.trace "getBedData()"
    
  state.requestData = null
  updateFamilyStatus()
  while(state.requestData == null) { Sleep(100) }
  def requestData = state.requestData
  state.requestData = null
  processBedData(requestData)
  return requestData
}

def processBedData(responseData) {
  if (!responseData || responseData.size() == 0) {
    return
  }
  for(def device : getChildDevices()) {
    for(def bed : responseData.beds) {
      if (device.currentBedId == bed.bedId) {
      def bedSide = bed.leftSide
      	if(device.currentSide == "Right")
        	bedSide = bed.rightSide
        def foundationStatus = updateFoundationStatus(device.currentBedId, device.currentSide)
        String onOff = "off"
        if(foundationStatus.fsCurrentPositionPresetRight != null && ((device.currentSide == "Right" && foundationStatus.fsCurrentPositionPresetRight != "Flat") || (device.currentSide == "Left" && foundationStatus.fsCurrentPositionPresetLeft != "Flat"))){
       		onOff = "on"
        }
        device.updateData(onOff, bedSide.sleepNumber, bedSide.isInBed)
        break;
      }
    }
  }
}





private def ApiHost() { "prod-api.sleepiq.sleepnumber.com" }

private def ApiUriBase() { "https://prod-api.sleepiq.sleepnumber.com" }

private def ApiUserAgent() { "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36" }

private def updateFamilyStatus(alreadyLoggedIn = false) {
  log.trace "[SleepNumberManager] Updating Family Status"

  if (needsLogin()) {
      login()
  }

  try {
    def statusParams = [
      uri: ApiUriBase() + '/rest/bed/familyStatus?_k=' + state.session?.key,
      requestContentType: 'application/json',
      contentType: 'application/json',
      headers: [
        'Content-Type': 'application/json;charset=UTF-8',
        'Host': ApiHost(),
        'User-Agent': ApiUserAgent(),
        'Cookie': state.session?.cookies,
        'DNT': '1',
      ],
    ]
    httpGet(statusParams) { response -> 
      if (response.status == 200) {
        state.requestData = response.data
      } else {
          log.error "[SleepNumberManager] Error updating family status - Request was unsuccessful: ($response.status) $response.data"
        state.session = null
        state.requestData = [:]
      }
    }
  } catch(Exception e) {
      log.error "[SleepNumberManager] Error updating family status -  Error ($e)"
  }
}

private def updateFoundationStatus(String bedId, String currentSide) {
  log.trace "[SleepNumberManager] Updating Foundation Status for: " + currentSide

  if (needsLogin()) {
      login()
  }

  try {
    def statusParams = [
      uri: ApiUriBase() + '/rest/bed/'+bedId+'/foundation/status?_k=' + state.session?.key,
      requestContentType: 'application/json',
      contentType: 'application/json',
      headers: [
        'Content-Type': 'application/json;charset=UTF-8',
        'Host': ApiHost(),
        'User-Agent': ApiUserAgent(),
        'Cookie': state.session?.cookies,
        'DNT': '1',
      ],
    ]
    httpGet(statusParams) { response -> 
      if (response.status == 200) {
        return response.data
      } else {
        log.error "[SleepNumberManager] Error updating foundation status - Request was unsuccessful: ($response.status) $response.data"
      }
    }
  } catch(Exception e) {
      log.error "[SleepNumberManager] Error updating foundation status - Error ($e)"
  }
}

private def login() {
  log.trace "[SleepNumberManager] Logging in..."
  state.session = null
  state.requestData = [:]
  try {
    def loginParams = [
      uri: ApiUriBase() + '/rest/login',
      requestContentType: 'application/json',
      contentType: 'application/json',
      headers: [
        'Content-Type': 'application/json;charset=UTF-8',
        'Host': ApiHost(),
        'User-Agent': ApiUserAgent(),
        'DNT': '1',
      ],
      body: '{"login":"' + settings.login + '","password":"' + settings.password + '"}='
    ]
    httpPut(loginParams) { response ->
      if (response.status == 200) {
        log.trace "[SleepNumberManager] Login was successful"
        state.session = [:]
        state.session.key = response.data.key
        state.session.cookies = ''
        response.getHeaders('Set-Cookie').each {
          state.session.cookies = state.session.cookies + it.value.split(';')[0] + ';'
        }
  		getBedData()
      } else {
        log.trace "[SleepNumberManager] Login failed: ($response.status) $response.data"
        state.session = null
        state.requestData = [:]
      }
    }
  } catch(Exception e) {
    log.error "[SleepNumberManager] Login failed: Error ($e)"
    state.session = null
    state.requestData = [:]
  }
}

private def raiseBed(String bedId, String side){
  log.trace "[SleepNumberManager] Raising bed side "+side+"..."
  put('/rest/bed/'+bedId+'/foundation/preset?_k=', "{'preset':1,'side':"+side+",'speed':0}")
}

private def lowerBed(String bedId, String side){
  log.trace "[SleepNumberManager] Lowering bed side "+side+"..."
  put('/rest/bed/'+bedId+'/foundation/preset?_k=', "{'preset':4,'side':"+side+",'speed':0}")
}

private def setNumber(String bedId, String side, number){
  log.trace "[SleepNumberManager] Setting sleep number side "+side+" to "+number+"..."
  put('/rest/bed/'+bedId+'/sleepNumber?_k=', "{'bed': "+bedId+", 'side': "+side+", 'sleepNumber': "+number+"}")
}

private def put(String uri, String body){
  if(needsLogin()){
  	login()
  }
  uri = uri + state.session?.key
  
  try {
    def statusParams = [
      uri: ApiUriBase() + uri,
        requestContentType: 'application/json',
        contentType: 'application/json',
        headers: [
        'Content-Type': 'application/json;charset=UTF-8',
        'Host': ApiHost(),
        'User-Agent': ApiUserAgent(),
        'Cookie': state.session?.cookies,
        'DNT': '1',
      ],
      body: body,
    ]
    httpPut(statusParams) { response -> 
      if (response.status == 200) {
  		getBedData()
        return true
      } else {
        log.error "[SleepNumberManager] Put Request failed: "+uri+" : "+body+" : ($response.status) $response.data"
        state.session = null
        state.requestData = [:]
        return false
      }
    }
  } catch(Exception e) {
      log.error "[SleepNumberManager] Put Request failed: "+uri+" : "+body+" : Error ($e)"
  }
}

private def needsLogin(){
	if(!state.session || !state.session?.key)
    	return true
        
    try {
        def statusParams = [
          uri: ApiUriBase() + '/rest/bed/familyStatus?_k=' + state.session?.key,
          requestContentType: 'application/json',
          contentType: 'application/json',
          headers: [
            'Content-Type': 'application/json;charset=UTF-8',
            'Host': ApiHost(),
            'User-Agent': ApiUserAgent(),
            'Cookie': state.session?.cookies,
            'DNT': '1',
          ],
        ]
        httpGet(statusParams) { response -> 
          if (response.status == 200) {
            state.requestData = response.data
          } else {
            return true;
          }
        }
  } catch(Exception e) {
      return true;
  }
        
    return false
}

Modified driver:

/**
 *  Sleep Number
 *
 *  Copyright 2019 Tim Parsons
 *
 *  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.
 */
metadata {
	definition (name: "Sleep Number Bed", namespace: "sleepNumberBed", author: "Classic_Tim", cstHandler: true) {
		capability "Switch Level"
        capability "Switch"
        capability "Presence Sensor"
        
        attribute "bedId", "String"
        attribute "side", "String"
               
        command "setBedId", ["string"]
        command "setSide", ["string"]
	}


	simulator {
		// TODO: define status and reply messages here
	}

	tiles(scale:2){
        standardTile("switch", "device.switch", width: 4, height: 4, canChangeIcon: false) {
            state "on", label:'RAISED', action:"off", icon:"https://raw.githubusercontent.com/ClassicTim1/SleepNumberManager/master/icons/raisedBed-icn3.png", backgroundColor:"#79b821"
            state "off", label:'FLAT', action:"on", icon:"st.Bedroom.bedroom2", backgroundColor:"#ffffff"
        }
        
        controlTile("levelSliderControl", "device.level", "slider", height: 4, width: 2) {
   			state "level", action:"switch level.setLevel"
		}
		valueTile("Side", "device.side", width: 3, height: 1){
        	state "default", label: '${currentValue} Side'
        }   
		valueTile("Presence", "device.PresenceState", width: 3, height: 1){
        	state "default", label: '${currentValue}'
        } 
   //     valueTile("level", "device.level",height:2, width:2, inactiveLabel: false, decoration: "flat") {
	//		state "level", label: 'Sleep Number: ${currentValue}'
	//	} 
    }
}

def updateData(String state, Integer sleepNumber, boolean present){
	sendEvent(name: "switch", value: state)
	sendEvent(name: "level", value: sleepNumber)
    if(present)
		sendEvent(name: "PresenceState", value: "Present")
    else
		sendEvent(name: "PresenceState", value: "Not Present")
}

// parse events into attributes
def parse(String description) {
	log.debug "Parsing '${description}'"
	// TODO: handle 'level' attribute

}

def setLevel(level) {
	sendEvent(name: "level", value: level)
    String side = "L"
    if(device.latestState('side').stringValue == "Right"){
    	side = "R"
    }
    parent.setNumber(device.latestState('bedId').stringValue, side, Math.round(level))
}


def setBedId(val){
	sendEvent(name: "bedId", value: val)
}

def setSide(val){
	sendEvent(name: "side", value: val)
}

def on(){
    String side = "L"
    if(device.latestState('side').stringValue == "Right"){
    	side = "R"
    }
	sendEvent(name: "switch", value: "on")
    parent.raiseBed(device.latestState('bedId').stringValue, side)
}

def off(){
    String side = "L"
    if(device.latestState('side').stringValue == "Right"){
    	side = "R"
    }
	sendEvent(name: "switch", value: "off")
    parent.lowerBed(device.latestState('bedId').stringValue, side)
}
3 Likes

Thanks @aaiyar, you beat me to it :smiley:

@leeonestop, you can read more about making HTTP requests here. I don't know why Smartthings documents the requestContentType and contentType parameters but doesn't require them while Hubitat does but basically all it's doing is telling the HTTP library what sort of content is being sent so it can handle encoding as needed.

1 Like

Thanks to you both. I see now where it even has where it says headers. It where I couldn't see the forest because of the trees.

1 Like

Thanks it's working like it does in ST. One thing that notice on both Hubitat and ST is when I turn on or off a side to set the head elevation it toggles the other side of the bed. So if my side is on and hers is off when I turn my side off to go back to flat it turns her side on. It's probably something simple in the logic. It toggles both if I press it again. Almost like it's sending the on off command to the bed instead of the side. The bed is a split top just fyi

1 Like

A little off topic, but does anyone have experience with both Tempurpedic and sleep number? Which would you choose and why?

Assuming you mean Tempurpedic Memory Foam vs Sleep Number. I've had both and the Tempurpedic was more comfortable at first but eventually had a dent where I normally slept after a few years. I'm going on about 7 years on the sleep number and it's still fine. I like being able to adjust the firmness if my back is bothering me. You can customize things about it that you just can't do with the temperpedic. The foam topper is easy to replace also just unzip and throw in whatever you want. I put a 4" cooling gel foam in there and it's been great.

The sleep number beds are pretty expensive though so that's something to keep in mind.

1 Like

i'm getting errors when trying to setup a bed (one side adds no problem the 2nd throws out errors out like crazy (doesn't matter which side I start with) Any help would be great i'm using the Modified app and drivers from @aaiyar posted a couple posts above

Edited out error to clean up my post, My guess is my error are from not having an flexbase

I didn't write this and it works for me so all I can do is guess... what kind of bed frame/foundation do you have? Is it possible you don't have a split (or half split) frame and maybe the code doesn't handle that?

@rvrolyk yeah we don't have an adjustable base, I found Tim's app and Driver for without a base (and with a little hacking got it ported to Hubitat thanks to this port) and got it setup with no errors, just out of curiosity does yours show presence? i'm thinking that feature is part of the base and not the mattress as it's not showing up

Does you bed come with the presence sensors etc. You can look on line to see your model?

1 Like

yes, mine shows presence but mine is the adjustable split base. I don't know if the non-adjustable bases have this (or if it's a feature of the mattress). I assumed it was a pressure pad in the mattress but if it's the base this would explain why you aren't seeing it (though the API is just "familyStatus" vs. "foundation/status" so not too sure how you'd tell).

@scubamikejax904

Like the others, my bed has an adjustable base. However, I am certain that presence is a feature of the mattress and not the base.

If your bed doesn't have an adjustable base, you are better off using the other (older) SleepIQ integration that is also posted in this thread. If you can't find it, let me know, and I will PM the files to you.

Here you go - try this app and driver:

Thanks @aaiyar as I still use the old one and it still work great for me.

1 Like

Came across some API info I thought you might be interested in. https://raw.githubusercontent.com/DeeeeLAN/homebridge-sleepiq/master/API.js
This was written for homebridge but gives some insight into other features that could be added such as foot actuation side table lights, and massage.

1 Like

Interesting, thanks for sharing @lewis.heidrick. I don't recall seeing massage or side table lights when we bought ours :slight_smile: I don't see an option to toggle the foot heaters which is the one thing I'd like to have... if you come across that let us know.

How are the heaters connected to the bed? do they plug into the pump under the bed or are they hard wired?

It looks like something he is working on to get the heaters working..

https://github.com/DeeeeLAN/homebridge-sleepiq

Issues/Future Work

  • Foundation settings (If you have a flexfit foundation, please reach out to help, I need data from all the different bases):
    • Verify different configurations (split head/split foot, split head/single foot, single head/single foot)
    • Set up outlets for all bases that have them
    • Set up foot heater for flexfit 3
    • ?
  • Allow for different refresh times based on the time of day
  • Verify everything still works will twin/full beds (If anybody has one, contact me)
  • Fix support for multiple beds (Does anybody have multiple beds?)
  • File a ticket or submit a pull request if you find any problems!

I think they plug into the pump but the wires disappear into the mattress directly so I haven't chased where they end up.

Good to see he's hoping to add it; I'll reach out and see if more info is still needed.