the X1s is on sale for $169.99 on Amazon.
Yup, I broke down and ordered one. Gets here tomorrow.
I am not a dealer. I do it all myself. It's possible and actually about as complex as using hubitat.
If you wanted to only control one room like a theater with a bunch of av gear you can do it without the annual fee.
I simplified the setup and driver code a bit for anyone that doesn't want to edit the code themselves...You just have to setup the buttons in the app like below. I'm currently using 40 buttons and it works great. AFAIK there is no limit on how many buttons you can add.
Enter your button name + number
Enter http://my hubitat IP address:39501/ for the URL
Set PUT as the request method
Leave Content Type and Additional Headers blank
And then just enter the button # in the body.
You'll have to do this for every button so it's a good idea to have the url copied so you can just paste it in each time
/*
Sofabaton X1S
Copyright 2025 Hubitat Inc. All Rights Reserved
2025-03-22 maxwell
-initial publication in github repo
2025-03-28 Gassgs
-Made Button Count a preferences & changed parse to just send the number from the body as the button press
*simple example driver for Sofabaton X1S remote, allows mapping X1S remote buttons to Hubitat button events
*driver configuration
-set a static DHCP reservation for the XS1 hub
-use that reserved IP in this drivers preference setting
*mobile app configuration on the X1S side for this specific driver instance:
-click add devices in devices tab, select Wi-Fi
-click link at bottom "Create a virtual device for IP control"
-enter http://my hubs IP address:39501/
-set PUT as the request method, for the Content Type leave blank, for Headers leave blank, for the Body, enter just the button number.
*/
metadata {
definition (name: "Sofabaton X1S", namespace: "hubitat", author: "Mike Maxwell") {
capability "Actuator"
capability "PushableButton"
preferences {
input name:"ip", type:"text", title: "X1S IP Address"
input name:"buttonCount", type: "number",title:"Button Count", description: "Set button count to suit your needs", defaultValue: 10
input name:"logEnable", type: "bool", title: "Enable debug logging", defaultValue: false
input name:"txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
}
}
}
void logsOff(){
log.warn "debug logging disabled..."
device.updateSetting("logEnable",[value:"false",type:"bool"])
}
void updated(){
log.info "updated..."
log.warn "debug logging is: ${logEnable == true}"
log.warn "description logging is: ${txtEnable == true}"
if (logEnable) runIn(1800,logsOff)
if (ip) {
device.deviceNetworkId = ipToHex(IP)
sendEvent(name:"numberOfButtons",value:settings.buttonCount)
}
}
void parse(String description) {
msg = parseLanMessage(description)
def data = msg.body
if (txtEnable) log.info "$device.label Button $data Pushed"
sendEvent(name:"pushed", value:data,isStateChange: true)
}
void push(data) {
if (txtEnable) log.info "$device.label Button $data Pushed"
sendEvent(name:"pushed", value:data,isStateChange: true)
}
String ipToHex(IP) {
List<String> quad = ip.split(/\./)
String hexIP = ""
quad.each {
hexIP+= Integer.toHexString(it.toInteger()).padLeft(2,"0").toUpperCase()
}
return hexIP
}
This addition is very nice!
Using this integration, I am setting-up a synchronization between SofaBaton and Hubitat which works quite well.
Summary
In the SofaBaton app, I have setup a button per activity which I named: on. I also setup an âOnâ and an âOffâ command.:
(I did have to select a connection type for some reason, but âapplication/jsonâ works)
This ended-up looking this:
And then in the device, you can see those buttons:
Then, this next step allows those buttons to be used as a âSelect Sourceâ for Hubitat:
-
From the âHubitatâ device, select âEditâ on the top right
-
Select âSource configurationâ
-
Select âI need to switch inputâ
-
Select âDirect input switchingâ
-
Select all the activitiesâ âOnâ commands - Since it doesnât move away from
In the next step, I setup the âOnâ and âOffâ buttons for the âHubitatâ device:
- From the âHubitatâ device, select âEditâ on the top-right
- Select âPower Settingsâ
- Select âTurn off when not in useâ
- Select âSeparate Power on & Power offâ
- From the âTurn on sequenceâ screen, select â+â, then âAdd a commandâ, then select âOnâ (If you test it, in Hubitat, the button press screen will show the âOnâ command if that is what was setup):
- Then from the âTurn off sequenceâ screen, select â+â, then âAdd a commandâ, then select âOffâ.
- After selecting âCompleteâ, the on/off commands will automatically be sent when choosing to turn on/off the hub.
Once these are all setup, all that is needed is to setup your activities to use the âHubitatâ device. You will be prompted for a source during the setup, which should be the activity name.
Then from Hubitat, this button can be used in rules to trigger whatever automation your heart desires - but essentially, the hub will know that this was the last action sent to it.
In this type of setup, the driver could ideally be modified to be able to accept 3 button types, one that has the on/off state, another that has the last activity state, and a third that could be used to receive other commands. Iâm not sure how this would be setup though⌠maybe with Additional Headers?
Iâve made a small change to the driver to recognize âOnâ or âOffâ buttons:
/*
Sofabaton X1S
Copyright 2025 Hubitat Inc. All Rights Reserved
2025-03-22 maxwell
-initial publication in github repo
2025-03-28 Gassgs
-Made Button Count a preferences & changed parse to just send the number from the body as the button press
2025-03-30 SViel
-Added recognition of on/off button
-Added the option to store the name of an activity if included in the button press
*simple example driver for Sofabaton X1S remote, allows mapping X1S remote buttons to Hubitat button events
*driver configuration
-set a static DHCP reservation for the XS1 hub
-use that reserved IP in this drivers preference setting
*mobile app configuration on the X1S side for this specific driver instance:
-click add devices in devices tab, select Wi-Fi
-click link at bottom "Create a virtual device for IP control"
-enter http://my hubs IP address:39501/
-set PUT as the request method, for the Content Type leave blank, for Headers leave blank, for the Body, enter just the button number.
*/
metadata {
definition (name: "Sofabaton X1S", namespace: "hubitat", author: "Mike Maxwell") {
capability "Actuator"
capability "PushableButton"
capability "Switch"
preferences {
input name:"ip", type:"text", title: "X1S IP Address"
input name:"buttonCount", type: "number",title:"Button Count", description: "Set button count to suit your needs", defaultValue: 10
input name:"logEnable", type: "bool", title: "Enable debug logging", defaultValue: false
input name:"txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
}
attribute "Activity","String"
}
}
void logsOff(){
log.warn "debug logging disabled..."
device.updateSetting("logEnable",[value:"false",type:"bool"])
}
void updated(){
log.info "updated..."
log.warn "debug logging is: ${logEnable == true}"
log.warn "description logging is: ${txtEnable == true}"
if (logEnable) runIn(1800,logsOff)
if (ip) {
device.deviceNetworkId = ipToHex(IP)
sendEvent(name:"numberOfButtons",value:settings.buttonCount)
}
}
void parse(String description) {
msg = parseLanMessage(description)
if (logEnable) log.debug "String is: $msg"
if (logEnable) log.debug "String Header is: $msg.header"
if (logEnable) log.debug "String Body is: $msg.body"
if (txtEnable) log.info "$device.label Button $msg.body Pushed"
def data = msg.body
//Recognize if the button name was "On" or "Off" and set the swtich instead of the button.
if (data.equalsIgnoreCase("on")){
sendEvent(name:"switch", value:"on")
} else if (data.equalsIgnoreCase("off")){
sendEvent(name:"switch", value:"off")
} else {
sendEvent(name:"pushed", value:data,isStateChange: true)
sendEvent(name: "Activity", value: "$data", isStateChange: true)
}
}
void push(data) {
if (txtEnable) log.info "$device.label Button $data Pushed"
sendEvent(name:"pushed", value:data,isStateChange: true)
}
String ipToHex(IP) {
List<String> quad = ip.split(/\./)
String hexIP = ""
quad.each {
hexIP+= Integer.toHexString(it.toInteger()).padLeft(2,"0").toUpperCase()
}
return hexIP
}
EDIT: I just did a pull request to add the latest changes from @gassgs and myself to the driver in GitHub. Iâve never done this before, so followed ChatGPT directions. @mike.maxwell, hopefully, I didnât screw-up anything⌠please let me know if I did thoughâŚ
Also, there is very likely a much better way to do this than what I did, so anyone can feel free to fix it up!!!
I just ordered one as well. There was a 10% coupon, which lowered the priced from $170 down to $153. I doubt that remote is going to get much cheaper in the near term. Supposed to arrive on Wednesday.
Wondering how this remote and hub compares to the Harmony Elite and Hub which I use with your driver currently.
This community can be a PITA at times, always recommending tech that makes my money evaporate - a salesman's dream I am
I find that it works very similarly. Iâm currently attempting to synchronize both together though Hubitat, so that I might use either interchangeably.
Will see if it works!
Just a quick update - It required a few RM rules to synchronize both ways (between SofaBaton and Harmony), but it works!
I had to create a rule for the on/off, and one for the activities to be synchronized from SofaBaton to Harmony.
Then I created a rule per Harmony activity, plus power off to ensure that the SofaBaton would start those same activities (using their API).
Now I can control my devices with either the SofaBaton or the Harmony. Not sure if I will use this for long, but happy that it works!
Yep, I am wondering the same. I saw recently that Logitech announced the end support for early generation Harmony remotes. Seems like theyâre signaling the beginning of the end. Hopefully the Harmony Hub based remotes are supported for a few more years, as I have 4 of those working great in my house.
Still Happy with my X1s after making the leap.
Well... no it cannot learn bluetooth!
This remote is becoming more useless than my Harmony 650, so it will probably go back...
I have contacted sofabaton and they pretty much gave up helping me, and I have emailed "support" at Formovie....
I am going to give them a few days to respond to see if they have any helpful ideas.
The Android app will not allow me to save an "invalid" JSON string in the Body field, and when I do put in a valid string like {"command":"red"}
as soon as I save it the app turns the string into "{\"parse\":\"red\"}"
(including those outside quotes) that is a pain to clean up for the switch logic in the driver. It also becomes "invalid" again if I have to go back into the app to edit the string and I have to clean up all the garbage the app inserted. But as I said earlier, I got it working.
But after stewing for a couple of days about how messy my additional code in the driver was I came back to the app to try to figure out a better way, at least for Android.
Leaving aside all the colorful language I used during the trial and error process, it turned out to be a relatively minor change. Instead of a single word in the body I use a key-value pair like command:red
in the header and leave the body blank. The app does not mess with the formatting of the header and I can simply use a second map in the driver to grab my command.
void parse(String description) {
Map msg = parseLanMessage(description)
Map header = msg.headers
switch (header.command) {
case "red" :
sendButtonEvent("pushed", 1, "physical")
break
case "green" :
sendButtonEvent("pushed", 2, "physical")
break
case "yellow" :
sendButtonEvent("pushed", 3, "physical")
break
case "blue" :
sendButtonEvent("pushed", 4, "physical")
break
default :
log.debug "unknown command:${header.command}"
}
}
I hope this can be of some use for any other Android users out there. I'm totally going to steal the configurable number of buttons that @gassgs came up with!