Can you fix WEMO?

Smartthings and HA finds Wemo devices and controls them without internet.

Not sure how well it works (don't have any of these devices) but I asked ChatGPT to port the HA local code to Hubitat. Resultant code is at:

Thank you, will try that

Have you also tried the Wemo Connect community app? I have been using it for years.

Does the HA do more than just finding them and controlling them? Wemo Connect cannot provision devices or connect them to wifi, just finds and controls the Wemos that are already connected to your wifi.

I'm also going to try out thebearmay's port, because, why not! :smiley:

:slight_smile: DI generated code throws syntax error in line 137 but you right 6 years old app works.

Unfortunately i have 2nd LAN 192.168.2.0/24 also with few wemos.

In HA I was able to turn off auto-discovery and add wemos from both LANs using their IP addresses.

With Hubitat I do not know how to do this.

It appears I would need perhpas use /23 netmask but this will cause other problems in my config

Could try adding it as a subnet

http://<Hub IP Address>/hub/allowSubnets?<subNet IP List>

you mean like this?

http:///hub/allowSubnets?192.168.1.0/24,192.168.2.0/24

HA approach would be much easier, in configuration.yaml they simply add two wemos on diferent LANs:

wemo:
discovery: false
static:

  • 192.168.1.23
  • 192.168.2.17

Was looking thru app code but cant figure how to achieve similar.

How is that easier? Sure it’s different, but what makes it “much easier?”

The endpoint that @thebearmay suggested is entered directly into your web browser, it’s not added to the app code running on the hub.

Here’s another example that you might find a little easier to follow.

http://192.168.0.12/hub/allowSubnets?192.168.0.0,192.168.1.0

ETA: Perhaps you meant “much more familiar” rather than “much easier.”

change line 138 from:

        def m = loc =~ /https?:\/\/([^:\/]+)(?::(\d+))?\/setup\.xml/i

to:

        def m = loc =~ /(?i)https?:\/\/([^:\/]+)(?::(\d+))?\/setup\.xml/

I meant with HA was able do web search and found solution on my own, in minutes.

However I do not want to use HA, neither some bridge, just because few devices work on HA.

My goal is to use only Hubitat for home automation, with generous help from great community

@jb5

Would you be willing to try a modified version of @thebearmay's AI code? Through a lot of back and forth, my friend Claude found some issues and fixed them. However, got to the point where he thinks that my devices are rejecting the connections because the other Wemo app is subscribed to them. I cannot risk WAF by turning off the other app and trying to connect using this app. As your wemo setup is "new", would you be able to test this out?

/**
 * WeMo Local Control for Hubitat
 * v2.1 (consolidated bugfix release)
 *
 * Local-only WeMo integration inspired by pyWeMo/Home Assistant:
 * - SSDP discovery
 * - setup.xml parsing
 * - dynamic service discovery
 * - SOAP actions
 * - local UPnP event subscription via Hubitat HTTP server
 * - recovery/re-probing when WeMo ports change
 * - device-specific child drivers
 *
 * No Belkin cloud / Internet access is used.
 *
 * Fixed in v2.1 (vs. the original release):
 * - fetchSetup(): HubAction was built with method/path/headers/callback/timeout
 *   all crammed into one map, so the constructor's real `options` map came back
 *   null and `action.options.destinationAddress = ...` threw a NullPointerException
 *   on every single probe (manual IP or SSDP-triggered). destinationAddress,
 *   callback, and timeout now go into the constructor's options argument directly.
 * - ssdpResponse(): was calling parseLanMessage(response) on a value Hubitat
 *   already hands over as a parsed Map for UDP client callbacks, which has no
 *   matching method signature and threw immediately. Now reads response.payload
 *   directly.
 * - decodePayload(): was decoding the payload as Base64, but Hubitat sends LAN
 *   payloads hex-encoded. Now uses decodeHex().
 * - setupResponse(): `port` was reassigned one line before it was declared
 *   (order bug, would have failed as soon as the two bugs above were fixed).
 *   Declaration moved above its first use.
 * - setupResponse(): added a manufacturer check so a non-Belkin UPnP device that
 *   happens to answer the generic "upnp:rootdevice"/"ssdp:all" search targets
 *   doesn't get misidentified and added as a WeMo child device.
 * - fetchSetup()/setupResponse(): the original code tried `action.data = [ip:
 *   ip, port: port]` to stash context on the outgoing HubAction and read it
 *   back off the response. hubitat.device.HubAction has no such property, so
 *   this threw "No such property: data" on every probe. Removed. ip/port are
 *   now read from the device's own declared <URLBase> in the setup XML body
 *   (falling back to the response's x-hubitat-source-ip header for ip, and
 *   49153 for port), which is also more correct than guessing since it's the
 *   port the device itself claims, not just whichever port in the scan range
 *   happened to answer.
 * - discover(): the manual-IP fallback fired every IP x every port in the
 *   49152-49159 range as one synchronous burst of sendHubCommand calls (e.g.
 *   11 IPs x 8 ports = 88 at once). That trips Hubitat's own "N pending hub
 *   commands, consider disabling app/device" throttle and can crowd out real
 *   SSDP replies arriving in the same window. Manual probes are now queued in
 *   state and drained one at a time, 200ms apart, via
 *   drainManualProbeQueue(). Port range is unchanged (still scans the full
 *   49152-49159 range per IP, just spread out over time) so port-drift
 *   recovery still works.
 *
 * Known open issue (not a code bug, still unresolved as of this release):
 * on at least one hub, discovery (both SSDP and manual-IP) found zero
 * devices even after all of the above fixes, on a completely fresh app
 * install. Ping confirmed the target WeMo devices were up and reachable,
 * and they were simultaneously being polled successfully by a separate,
 * older WeMo integration (WeMo Connect) running on the same hub. Manual
 * probes against known-correct ip:port pairs got "Connection refused"
 * even though the devices were live. Leading theory: some older Belkin
 * WeMo devices' embedded web server only accepts one concurrent TCP
 * client, and another app already holding a persistent connection to a
 * device can block this app from connecting to it (and possibly from
 * getting an SSDP reply at all). If you hit "0 devices found" after
 * confirming your devices are powered on and reachable, check whether
 * another WeMo integration (WeMo Connect, an Alexa/Belkin app, etc.) is
 * also actively connected to the same devices, and try testing with
 * that other integration paused, or with a device that isn't managed by
 * anything else yet.
 *
 * v2.1 bugfixes: Neerav Modi, debugged with Claude (Anthropic).
 * Original app: OpenAI (ChatGPT).
 */

definition(
    name: "WeMo Local Control v2",
    namespace: "local.wemo.v2",
    author: "Neerav Modi / OpenAI",
    description: "Fuller local-only WeMo integration: discovery, SOAP, subscriptions and recovery.",
    category: "Integrations"
)

preferences {
    page(name: "mainPage")
}

def mainPage() {
    dynamicPage(name: "mainPage", title: "WeMo Local Control v2", install: true, uninstall: true) {
        section("Local-only") {
            paragraph "All communication is directly between Hubitat and your WeMo devices. No Belkin cloud services are used."
            input "discoverOnStartup", "bool", title: "Discover at startup", defaultValue: true
            input "pollMinutes", "number", title: "Fallback poll interval (minutes)", defaultValue: 2
            input "debugLogging", "bool", title: "Debug logging", defaultValue: false
            input "manualIps", "text", title: "Manual WeMo IPs (comma separated)", required: false
        }
        section("Actions") {
            input "discoverNow", "button", title: "Discover WeMo Devices"
            input "refreshAll", "button", title: "Refresh All Devices"
            input "resubscribeAll", "button", title: "Re-subscribe to Events"
        }
        section("Status") {
            paragraph "Known devices: ${(state.devices ?: [:]).size()}"
            paragraph "Hub callback port: ${getCallbackPort() ?: 'not yet assigned'}"
        }
    }
}

def installed() { initialize() }

def updated() {
    unsubscribe()
    unschedule()
    initialize()
}

def initialize() {
    state.devices = state.devices ?: [:]
    if (discoverOnStartup != false) runIn(2, discover)
    if (pollMinutes) runEvery1Minute("pollChildren")
    runIn(10, subscribeAll)
}

def appButtonHandler(btn) {
    switch(btn) {
        case "discoverNow": discover(); break
        case "refreshAll": refreshAll(); break
        case "resubscribeAll": subscribeAll(); break
    }
}

def pollChildren() {
    def n = pollMinutes ?: 2
    def minute = ((now() / 60000L) as Long) as Integer
    if (minute % n != 0) return
    getChildDevices()?.each { d ->
        try { d.refresh() } catch (e) { logDebug("poll ${d.deviceNetworkId}: ${e.message}") }
    }
}

def refreshAll() {
    getChildDevices()?.each { d -> try { d.refresh() } catch (ignored) {} }
}

def subscribeAll() {
    getChildDevices()?.each { d ->
        try { d.subscribeEvents() } catch (e) { logDebug("subscribe ${d.deviceNetworkId}: ${e.message}") }
    }
}

def discover() {
    logDebug("SSDP discovery")
    def targets = [
        "urn:Belkin:device:**",
        "urn:Belkin:service:basicevent:1",
        "upnp:rootdevice",
        "ssdp:all"
    ]
    targets.each { st ->
        def msg = """M-SEARCH * HTTP/1.1\r
HOST: 239.255.255.250:1900\r
MAN: "ssdp:discover"\r
MX: 3\r
ST: ${st}\r
\r
"""
        try {
            def action = new hubitat.device.HubAction(
                msg,
                hubitat.device.Protocol.LAN,
                [
                    type: hubitat.device.HubAction.Type.LAN_TYPE_UDPCLIENT,
                    destinationAddress: "239.255.255.250:1900",
                    callback: "ssdpResponse",
                    timeout: 5,
                    parseWarning: false
                ]
            )
            sendHubCommand(action)
        } catch (e) {
            log.warn "SSDP send failed: ${e.message}"
        }
    }

    // Manual IP fallback. Probe the normal WeMo port range, but staggered --
    // firing all IPs x all ports as one burst of sendHubCommand calls trips
    // Hubitat's own outbound-command throttle ("N pending hub commands,
    // consider disabling app/device") and can crowd out real SSDP replies
    // arriving in the same window. One probe every 200ms is gentle enough to
    // avoid that while still finishing a full sweep in well under a minute.
    def queue = []
    (manualIps ?: "").split(",").collect { it.trim() }.findAll { it }.each { ip ->
        (49152..49159).each { port -> queue << [ip: ip, port: port] }
    }
    if (queue) {
        state.manualProbeQueue = queue
        runInMillis(200, "drainManualProbeQueue")
    }
}

def drainManualProbeQueue() {
    def queue = state.manualProbeQueue
    if (!queue) return
    def next = queue.remove(0)
    state.manualProbeQueue = queue
    fetchSetup(next.ip as String, next.port as Integer)
    if (queue) runInMillis(200, "drainManualProbeQueue")
}

def ssdpResponse(response) {
    try {
        // Hubitat hands UDP client callbacks an already-parsed Map
        // (mac/ip/port/payload, payload hex-encoded) -- do NOT run this
        // through parseLanMessage(), which expects a raw description string
        // and has no signature matching a Map argument.
        def text = decodePayload(response?.payload) ?: ""
        def loc = getHeader(text, "LOCATION")
        if (!loc) return
        def m = loc =~ /(?i)https?:\/\/([^:\/]+)(?::(\d+))?(\/\S*)/
        if (m.find()) {
            def ip = m.group(1)
            def port = (m.group(2) ?: "49153").toInteger()
            def path = m.group(3) ?: "/setup.xml"
            fetchSetup(ip, port, path)
        }
    } catch (e) {
        logDebug("SSDP response parse: ${e.message}")
    }
}

private void fetchSetup(String ip, Integer port, String path = "/setup.xml") {
    try {
        def action = new hubitat.device.HubAction(
            [
                method: "GET",
                path: path,
                headers: ["HOST": "${ip}:${port}", "Connection": "close"],
                body: null
            ],
            null,
            [
                callback: "setupResponse",
                timeout: 5,
                parseWarning: false,
                destinationAddress: "${ip}:${port}"
            ]
        )
        sendHubCommand(action)
    } catch (e) {
        logDebug("setup.xml ${ip}:${port}: ${e.message}")
    }
}

def setupResponse(response) {
    try {
        String body = response?.body ?: ""
        if (!body.contains("<root")) return

        // hubitat.device.HubAction has no settable 'data' property, so context
        // (ip/port) cannot be stashed on the outgoing action and read back off
        // the response -- attempting that throws "No such property: data".
        // Every real UPnP device description declares its own base URL, so use
        // that as the authoritative source for both ip and port; fall back to
        // the response's source-IP header, then to a default port.
        String ip = null
        Integer port = null
        def ub = body =~ /<URLBase>https?:\/\/([^:\/]+)(?::(\d+))?/
        if (ub.find()) {
            ip = ub.group(1)
            if (ub.group(2)) port = ub.group(2).toInteger()
        }
        if (!ip) ip = response?.headers?."x-hubitat-source-ip"
        if (!ip) return  // no way to identify which device this came from

        def root = new XmlParser(false, false).parseText(body)
        def dev = root.device
        def friendly = dev.friendlyName?.text() ?: "WeMo"
        def udn = dev.UDN?.text()
        def type = dev.deviceType?.text() ?: ""
        def model = dev.modelName?.text() ?: ""
        def serial = dev.serialNumber?.text() ?: ""
        def manufacturer = dev.manufacturer?.text() ?: ""

        // Only "urn:Belkin:device:**" and "urn:Belkin:service:basicevent:1"
        // are WeMo-specific search targets. "upnp:rootdevice" and "ssdp:all"
        // are generic and will get answers from any UPnP device on the LAN
        // (routers, NAS boxes, other smart-home hubs, etc). Filter those out
        // here instead of relying on setup path naming, since real WeMo
        // devices don't all use the same setup filename.
        if (!manufacturer.toLowerCase().contains("belkin")) {
            logDebug("Ignoring non-Belkin UPnP device at ${ip} (manufacturer='${manufacturer}', friendlyName='${friendly}', type='${type}')")
            return
        }

        def services = []
        dev.serviceList?.service?.each { s ->
            services << [
                serviceType: s.serviceType?.text() ?: "",
                serviceId: s.serviceId?.text() ?: "",
                controlURL: s.controlURL?.text() ?: "",
                eventSubURL: s.eventSubURL?.text() ?: "",
                SCPDURL: s.SCPDURL?.text() ?: ""
            ]
        }

        if (!port) port = 49153

        def basic = services.find { it.serviceType.contains("basicevent") }
        if (basic?.controlURL) {
            def pm = basic.controlURL =~ /:(\d+)\//
            if (pm.find()) {
                // use discovered control URL port if present
                def p = pm.group(1).toInteger()
                if (p) port = p
            }
        }

        def dni = "wemo-${udn ? udn.replaceAll(/[^A-Za-z0-9_-]/, '_') : "${ip}-${port}"}"
        def existing = getChildDevice(dni)

        Map data = [
            ip: ip,
            port: port.toString(),
            udn: udn ?: "",
            model: model ?: "",
            deviceType: type ?: "",
            serial: serial ?: "",
            servicesJson: groovy.json.JsonOutput.toJson(services)
        ]

        if (!existing) {
            String driver = chooseDriver(type, model, services)
            existing = addChildDevice("local.wemo.v2", driver, dni, [
                name: friendly,
                label: friendly,
                isComponent: false,
                data: data
            ])
            log.info "Added ${friendly} (${driver}) at ${ip}:${port}"
        } else {
            data.each { k,v -> existing.updateDataValue(k, v.toString()) }
            existing.setLabel(friendly)
        }

        data.each { k,v -> try { existing.updateDataValue(k, v.toString()) } catch (ignored) {} }
        state.devices[dni] = [name:friendly, ip:ip, port:port, udn:udn, model:model, type:type]
        existing.refresh()
        runIn(2, { try { existing.subscribeEvents() } catch (ignored) {} })
    } catch (e) {
        logDebug("setup response: ${e.message}")
    }
}

private String chooseDriver(String type, String model, List services) {
    String t = (type ?: "").toLowerCase()
    String m = (model ?: "").toLowerCase()
    if (t.contains("insight") || m.contains("insight") || services.any { it.serviceType?.toLowerCase()?.contains("insight") }) {
        return "WeMo Local Insight"
    }
    if (t.contains("lightswitch") || t.contains("dimmer") || m.contains("dimmer") || m.contains("lightswitch")) {
        return "WeMo Local Dimmer"
    }
    if (t.contains("motion") || t.contains("sensor")) {
        return "WeMo Local Sensor"
    }
    return "WeMo Local Switch"
}

private String getCallbackPort() {
    try {
        return location.hub?.localIP ? "Hubitat hub HTTP listener" : null
    } catch (ignored) { return null }
}

private String getHeader(String text, String name) {
    def line = text.readLines().find { it.toUpperCase().startsWith(name.toUpperCase() + ":") }
    line ? line.substring(line.indexOf(":")+1).trim() : null
}

private String decodePayload(String payload) {
    if (!payload) return null
    try { return new String(payload.decodeHex(), "UTF-8") } catch (ignored) { return payload }
}

private void logDebug(String s) { if (debugLogging) log.debug s }

There is an "Known open issue" listed in the code. TL;DR: unsubscribe through Wemo Connect and/or disable the Wemo Connect app. Then try discovering through this app.

The knew code is better how?

isn’t it just copied pasted from old app?

It is not a copy/paste of the thebearmay's code. For full details, there is a changelog for version 2.1 in the code I provided. It fixes or improves discovery, decoding responses, ignores non-Wemo devices that respond, and a bunch of things Claude considered "bugs" or necessary improvements to the app and its core functionality.

I would like get 2nd hub for tests, but need time to acquire one.

I moved some of devices over the weekend from old smartthings and still have many to move, before winter kicks, so reluctant as well. Will let you know, please understand.

Other thing Samsung probably bricked some of my zigbee devices, by pushing flawed drivers.

No worries. I was just trying to get an app working that wasn't working for you.

I'm subscribed to this thread. Whenever you're ready and post here in a few weeks/months, I'll get a notification.