[RELEASE] HubiThings Replica

Yeah, that's what I've figured after doing some research on the smartthings api. If I'm not mistaken, the original PAT is used to create a refresh token, valid for 30 days, and then the refresh token is used to create the tokens that are used in actual api calls. With that said, the scope of this last token has to be determined by something so I'm guessing it relates somehow to the scope of the original PAT. I'll try to do some more testing on this.

I think the magic may be happening on the authorization page:


and since there is only one checkbox for read/write opperation, there is no way to distinguish the two.

At one point the PAT was good for 50 years. Now it is only good for 24 hours.

And yes, once you build the OAuth the permissions move to the values you show, however those are completely controlled by SmartThings and there is not a way to have additional granularity that I am aware.

Thank you. Since the communication needs to be bi-directional I guess the only reason to keep them in the smartthings hub would be to be able to control them from two different apps. I need to re-think that idea.

Not really following but you don’t need a PAT for bi-directional communication. After you authorize the OAuth it does all the work keeping the tokens required refreshed for ST <-> HE communication.

You will only need a new PAT if you delete the OAuth and/or have problems.

The tokens are supplied via SSL and secure.

Hope this helps.

Yes, I understand that. What I'm trying to say is that since authorization scope is "Allow (execute, read, write) on all devices" then the HubiThings replica will always allow for bi-directional communication. If Hubitat portal/cloud is hacked and the hackers know the the hubitat ecosystem, they will gain access to all devices, those directly provisioned within the Hubitat hub as well as those coming from Hubithings Replica. While all the Hubitat processing is local, all the functionality is exposed through cloud. What I wanted to accomplish was to keep the locks operation in Smartthings but allow smartthings to send the status updates (Locked/Unlocked) to Hubitat. This was possible with various integrations back then when Smartthings allowed groovy scripts but it's a different world now. I understand that its a bit paranoid, and I'm not trying to diminish the great value of the HubiThings replica, just was wondering if there would be a way to use Hubitat in read-only mode for smartthings but it seems that its not and that is yet another thing that samsung broke.

Correct. At this time the application is read/write and does not allow for read-only solutions. You have the same issue with SmartThings in general. Everything in ST is exposed in the API by design, and even with limited local processing, the solution still requires cloud connectivity.

If you are really security conscious, you should not be using non-native SmartThings Luna drivers. You have no control over the developer’s intent, and the source code is closed and not inspected. This has been flagged several times in the ST forum. If you are looking for the biggest vector to be hacked, this is an easy win for anyone with malicious intent.

I have a Kwikset Halo Touch that is integrated in my ST Hub. I'm trying to mirror it over into HE via HubiThings, but in the "Select Hubitat Device Type" list, there isn't a good Replica or Virtual device to allow for door lock operations.

The only possible solution I could think of is to use a virtual switch with on/off linked to locked/unlocked.

Is there a better solution to this?

Locks can be funky, but there is an optional "Replica Lock" in HPM. YMMV depending on the ST lock solution. I was never really happy with the integration.

1 Like

Ah.....I didn't have that one checked in my origional HPM install. I was able to modify it and will give that a shot.

edit: success. That worked just fine for my application. Simple Lock and Unlock ability/satus updates along with battery updates

1 Like

Installed this to get around the 24hr samsung PAT issue.

Works great, thanQ :slight_smile:

1 Like

Just curious if you ever developed a solution to this? We have the same range hood!

(Solved) Hello. I just did an upgrade on my 2 x 6 zone konnected boards to a konnected pro now when i try to mirror my motion sensors to SmartThings they will show up ok but the icons are motion activated full time but if i edit the icon in SmartThings it show active and inactive. Any ideas? no matter what i try same issue. Everything seem to be ok with the older Konnected 6 zone panels.
I think I found the solution if this comes up with anyone else You can't just mirror the motion sensors they have to be custom.

1 Like

What's the secret to stop the info logging in logs?

app:3242026-01-11 02:46:56.452 PMinfo
HubiThings Replica refreshing all 'Store Room Tube' device capabilities
app:3242026-01-11 02:46:55.910 PMinfo
HubiThings Replica received 'Store Room Tube' status ● trigger:switch ➢ command:setSwitchValue:off
app:3242026-01-11 02:46:55.629 PMinfo
HubiThings Replica refreshing all 'Store Room Light' device capabilities

Screenshot 2026-01-11 151424

Check the main app too.

1 Like

Let's keep this between you and me Mr Bloodtick, looks like I failed the intelligence test. In my defense, in the app you have click Show Configuration, then click Advanced Configuration.

1 Like

You have a very interesting mind. So with Disable Info logging in devices I correctly don't get anything in logs. But with it enabled, not only do I get state changes in logs, but also every 20 minutes I get..

dev:5972026-01-12 10:34:49.991 AMinfo
Kitchen set level to 100%
dev:5972026-01-12 10:34:49.982 AMinfo
Kitchen was turned off 

For each device, regardless of state change. I jut want state changes shown in the logs.
This seems to work for the Replica Dimmer driver, but all of them need adjusting...

/**
*  Copyright 2023 Bloodtick
*
*  Licensed under the Apache License, Version 2.0 (the "License");
*/
public static String version() { return "1.3.3" }

import groovy.transform.Field
@Field volatile static Map<String,Long> g_mEventSendTime = [:]

metadata {
    definition(
        name: "Replica Dimmer",
        namespace: "replica",
        author: "bloodtick",
        importUrl: "https://raw.githubusercontent.com/bloodtick/Hubitat/main/hubiThingsReplica/devices/replicaDimmer.groovy"
    ) {
        capability "Actuator"
        capability "Configuration"
        capability "Switch"
        capability "SwitchLevel"
        capability "Refresh"

        attribute "healthStatus", "enum", ["offline", "online"]
    }

    preferences {
        input(name: "deviceInfoDisable", type: "bool", title: "Disable Info logging", defaultValue: false)
    }
}

def installed() { initialize() }
def updated()   { initialize() }

def initialize() {
    updateDataValue("triggers", groovy.json.JsonOutput.toJson(getReplicaTriggers()))
    updateDataValue("commands", groovy.json.JsonOutput.toJson(getReplicaCommands()))
}

def configure() {
    logInfo("${device.displayName} configured default rules")
    initialize()
    updateDataValue("rules", getReplicaRules())
    sendCommand("configure")
}

/* --------------------------------------------------------
   Replica commands
--------------------------------------------------------- */
static Map getReplicaCommands() {
    return ([
        "setLevelValue": [[name: "level*", type: "NUMBER"]],
        "setSwitchValue": [[name: "switch*", type: "ENUM"]],
        "setSwitchOff": [],
        "setSwitchOn": [],
        "setHealthStatusValue": [[name: "healthStatus*", type: "ENUM"]]
    ])
}

/* --------------------------------------------------------
   Level handling (FIXED)
--------------------------------------------------------- */
def setLevelValue(value) {
    Integer current = device.currentValue("level") as Integer
    Integer incoming = value as Integer

    if (current == incoming) return   // 🔇 suppress duplicate updates

    String descriptionText = "${device.displayName} set level to $incoming% ${getDelay()}"
    sendEvent(name: "level", value: incoming, unit: "%", descriptionText: descriptionText)
    logInfo(descriptionText)
}

/* --------------------------------------------------------
   Switch handling (FIXED)
--------------------------------------------------------- */
def setSwitchValue(value) {
    if (device.currentValue("switch") == value) return  // 🔇 suppress duplicates

    String descriptionText = "${device.displayName} was turned $value ${getDelay()}"
    sendEvent(name: "switch", value: value, descriptionText: descriptionText)
    logInfo(descriptionText)
}

def setSwitchOff() { setSwitchValue("off") }
def setSwitchOn()  { setSwitchValue("on")  }

/* --------------------------------------------------------
   Health
--------------------------------------------------------- */
def setHealthStatusValue(value) {
    if (device.currentValue("healthStatus") == value) return
    sendEvent(name: "healthStatus", value: value,
              descriptionText: "${device.displayName} healthStatus set to $value")
}

/* --------------------------------------------------------
   Delay tracking
--------------------------------------------------------- */
private String getDelay() {
    return (g_mEventSendTime[device.id] &&
            (now() - g_mEventSendTime[device.id] < 5000))
        ? "with roundtrip delay of ${now() - g_mEventSendTime[device.id]}ms"
        : ""
}

/* --------------------------------------------------------
   Triggers
--------------------------------------------------------- */
static Map getReplicaTriggers() {
    return ([ "off":[], "on":[], "setLevel":[[name:"level*",type:"NUMBER"],[name:"rate",type:"NUMBER",data:"rate"]], "refresh":[] ])
}

private def sendCommand(String name, def value=null, String unit=null, data=[:]) {
    Long now = g_mEventSendTime[device.id] = now()
    data.version = version()
    parent?.deviceTriggerHandler(device, [name:name, value:value, unit:unit, data:data, now:now])
}

def off()      { sendCommand("off") }
def on()       { sendCommand("on") }
def setLevel(level, rate=null) { sendCommand("setLevel", level, null, [rate: rate]) }
void refresh() { sendCommand("refresh") }

/* --------------------------------------------------------
   Rules (unchanged)
--------------------------------------------------------- */
static String getReplicaRules() {
    return """{"version":1,"components":[
        {"trigger":{"name":"off","label":"command: off()","type":"command"},"command":{"name":"off","type":"command","capability":"switch","label":"command: off()"},"type":"hubitatTrigger"},
        {"trigger":{"name":"on","label":"command: on()","type":"command"},"command":{"name":"on","type":"command","capability":"switch","label":"command: on()"},"type":"hubitatTrigger"},
        {"trigger":{"type":"attribute","properties":{"value":{"title":"SwitchState","type":"string"}},"required":["value"],"capability":"switch","attribute":"switch"},"command":{"name":"setSwitchValue","parameters":[{"name":"switch*","type":"ENUM"}]},"type":"smartTrigger"},
        {"trigger":{"type":"attribute","properties":{"value":{"type":"integer","minimum":0,"maximum":100}},"required":["value"],"capability":"switchLevel","attribute":"level"},"command":{"name":"setLevelValue","parameters":[{"name":"level*","type":"NUMBER"}]},"type":"smartTrigger"},
        {"trigger":{"type":"attribute","properties":{"value":{"title":"HealthState","type":"string"}},"required":["value"],"capability":"healthCheck","attribute":"healthStatus"},"command":{"name":"setHealthStatusValue","parameters":[{"name":"healthStatus*","type":"ENUM"}]},"type":"smartTrigger","mute":true}
    ]}"""
}

/* --------------------------------------------------------
   Logging
--------------------------------------------------------- */
private logInfo(msg)  { if (settings?.deviceInfoDisable != true) log.info msg }
private logWarn(msg)  { log.warn msg }
private logError(msg) { log.error msg }

Yeah, on each refresh it’ll run through normalization (if needed) and you’ll see that in the logs. The event itself won’t fire because Hubitat filters out unchanged values at a lower level, but you’ll still see the function pass in the logs. That’s why nothing new shows up under “Events.”

On the “all of them need adjusting” point: this project is basically in maintenance mode. If something major breaks, I’ll take a look, but I’m not planning to do broader tuning or enhancements right now. Sorry.

to note, another way todo what you might be looking for could be like this:

void sendEventX(Map x) {
    if(x?.value!=null  && !x?.eventDisable && (device.currentValue(x?.name).toString() != x?.value.toString() || x?.isStateChange)) {
        if(x?.descriptionText) { if(x?.logLevel=="warn") logWarn (x?.descriptionText); else logInfo (x?.descriptionText); }
        sendEvent(name: x?.name, value: x?.value, unit: x?.unit, descriptionText: x?.descriptionText, isStateChange: (x?.isStateChange ?: false))
    }
}

OK I'm going through them now, it's just discombobulating when checking logs to see devices change state when they haven't.
This Replica Switch driver has the exact same problem:

  • Replica re-sends state every ~20 minutes
  • setSwitchValue() always sends an event
  • Logging happens even when nothing changed
  • Auto-off can retrigger noise if not guarded

Below is a clean, drop-in replacement that:

:white_check_mark: Adds state-change suppression
:white_check_mark: Honors Disable Info logging
:white_check_mark: Logs ONLY real activity
:white_check_mark: Preserves auto-off behavior
:white_check_mark: Stops all 20-minute spam


:white_check_mark: What was changed

  • Added sendEventX() (same pattern as your Power Outlet fix)
  • Replaced all sendEvent() calls
  • Prevented duplicate switch + health events
  • Auto-off only schedules when a real ON happens
1 Like

You can save/add any driver and as long as the namespace is 'replica' it will find it and allow you to select in the UI. That is just for cases like this -- you want your own, maybe even speciality drivers and can publish them as a separate add-on in HPM.

1 Like