ESPHome + Hubitat

I'm trying to add Temperature & Humidity sensors to ESPHome Multi Switch & Contact Sensor.
Values are displayed in the parent's current state, but they are not updated. what am i doing wrong?

/**
 *  MIT License
 *  Copyright 2022 Jonathan Bradshaw (jb@nrgup.net)
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in all
 *  copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 *  SOFTWARE.
 */
metadata {
    definition(
        name: 'ESPHome Multi Switch - Contact Sensor - TH ',
        namespace: 'esphome',
        author: 'Jonathan Bradshaw',
        singleThreaded: true,
        importUrl: '') {

        capability 'Refresh'
        capability 'Initialize'
        capability 'SignalStrength'
        capability 'Sensor'
        capability 'RelativeHumidityMeasurement'
        capability 'TemperatureMeasurement'

        // attribute populated by ESPHome API Library automatically
        attribute 'networkStatus', 'enum', [ 'connecting', 'online', 'offline' ]
    }

    preferences {
        input name: 'ipAddress',    // required setting for API library
                type: 'text',
                title: 'Device IP Address',
                required: true

        input name: 'password',     // optional setting for API library
                type: 'text',
                title: 'Device Password <i>(if required)</i>',
                required: false

        input name: 'logEnable',    // if enabled the library will log debug details
                type: 'bool',
                title: 'Enable Debug Logging',
                required: false,
                defaultValue: false

        input name: 'logTextEnable',
              type: 'bool',
              title: 'Enable descriptionText logging',
              required: false,
              defaultValue: true
    }
}

import com.hubitat.app.DeviceWrapper
import com.hubitat.app.ChildDeviceWrapper

public void initialize() {
    // API library command to open socket to device, it will automatically reconnect if needed
    openSocket()

    if (logEnable) {
        runIn(1800, 'logsOff')
    }
}

public void installed() {
    log.info "${device} driver installed"
}

public void logsOff() {
    espHomeSubscribeLogs(LOG_LEVEL_INFO, false) // disable device logging
    device.updateSetting('logEnable', false)
    log.info "${device} debug logging disabled"
}

// driver commands
public void componentOn(DeviceWrapper dw) {
    String key = dw.getDeviceNetworkId().minus("${device.id}-")
    if (dw.currentValue('switch') != 'on') {
        if (logTextEnable) { log.info "${device} on" }
        espHomeSwitchCommand(key: key as Long, state: true)
    }
}

public void componentOff(DeviceWrapper dw) {
    String key = dw.getDeviceNetworkId().minus("${device.id}-")
    if (dw.currentValue('switch') != 'off') {
        if (logTextEnable) { log.info "${device} off" }
        espHomeSwitchCommand(key: key as Long, state: false)
    }
}

public void refresh() {
    log.info "${device} refresh"
    state.clear()
    state.requireRefresh = true
    espHomeDeviceInfoRequest()
}

public void updated() {
    log.info "${device} driver configuration updated"
    initialize()
}

public void uninstalled() {
    closeSocket('driver uninstalled') // make sure the socket is closed when uninstalling
    log.info "${device} driver uninstalled"
}

// the parse method is invoked by the API library when messages are received
public void parse(Map message) {
    if (logEnable) { log.debug "ESPHome received: ${message}" }

    switch (message.type) {
        case 'device':
            // Device information
            break

        case 'entity':
            // Discover all binary switches and create child devices for each
            if (message.platform == 'switch' && !message.disabledByDefault && message.entityCategory == 'none') {
                String dni = "${device.id}-${message.key}"
                ChildDeviceWrapper dw = getChildDevice(dni) ?:
                    addChildDevice(
                        'hubitat',
                        'Generic Component Switch',
                        dni
                    )
                dw.name = message.objectId
                dw.label = message.name
            }

            // Discover all binary sensors and create child devices for each
            if (message.platform == 'binary' && !message.disabledByDefault && message.entityCategory == 'none') {
                String dni = "${device.id}-${message.key}"
                ChildDeviceWrapper dw = getChildDevice(dni) ?:
                    addChildDevice(
                        'hubitat',
                        'Generic Component Contact Sensor',
                        dni
                    )
                dw.name = message.objectId
                dw.label = message.name
            }

            if (message.platform == 'sensor') {
                switch (message.deviceClass) {
                    case 'signal_strength':
                        state['signalStrength'] = message.key
                        break
                    case 'humidity':
                        // This will populate the cover dropdown with all the entities
                        // discovered and the entity key which is required when sending commands
                        state.sensors = (state.sensors ?: [:]) + [ (message.key): message ]
                        if (!settings.humidity) {
                            device.updateSetting('humidity', message.key)
                        }
                        break
                    case 'temperature':
                        // This will populate the cover dropdown with all the entities
                        // discovered and the entity key which is required when sending commands
                        state.sensors = (state.sensors ?: [:]) + [ (message.key): message ]
                        if (!settings.temperature) {
                            device.updateSetting('temperature', message.key)
                        }
                        break
                }
                return
            }
            break

        case 'state':
            // Check if the entity key matches the message entity key received to update device state
            if (settings.temperature as Long == message.key && message.hasState) {
                String value = message.state
                if (device.currentValue('temperature') != value) {
                    sendEvent([
                        name: 'temperature',
                        value: value,
                        descriptionText: "Temperature is ${value}"
                    ])
                }
                return
            }

            if (settings.humidity as Long == message.key && message.hasState) {
                String value = message.state
                if (device.currentValue('humidity') != value) {
                    sendEvent([
                        name: 'humidity',
                        value: value,
                        unit: '%',
                        descriptionText: "Humidity is ${value}"
                    ])
                }
                return
            }
            
            // Signal Strength
            if (state.signalStrength as Long == message.key && message.hasState) {
                Integer rssi = Math.round(message.state as Float)
                String unit = 'dBm'
                if (device.currentValue('rssi') != rssi) {
                    descriptionText = "${device} rssi is ${rssi}"
                    sendEvent(name: 'rssi', value: rssi, unit: unit, descriptionText: descriptionText)
                    if (logTextEnable) { log.info descriptionText }
                }
                return
            }

            // Receives entity state updates to send to child device
            if (message.platform == 'switch') {
                String dni = "${device.id}-${message.key}"
                String type = message.isDigital ? 'digital' : 'physical'
                String value = message.state ? 'on' : 'off'
                getChildDevice(dni)?.parse([
                    [ name: 'switch', value: value, type: type, descriptionText: "switch is ${value}" ]
                ])
                return
            }

            // Receives entity state updates to send to child device
            if (message.platform == 'binary' && message.hasState) {
                String dni = "${device.id}-${message.key}"
                String type = message.isDigital ? 'digital' : 'physical'
                String value = message.state ? 'closed' : 'open'
                getChildDevice(dni)?.parse([
                    [ name: 'contact', value: value, type: type, descriptionText: "contact is ${value}" ]
                ])
                return
            }
            break
    }
}

// Put this line at the end of the driver to include the ESPHome API library helper
#include esphome.espHomeApiHelper

Solved the TH values update problem. But now I can't get values rounded to 1 decimal.

filters:
    - round: 1 # will round to 1 decimal place

round filter is not recognized in the sensor (bme280)

I tried lambda, but the most I could get was an integer

filters:
      - lambda: return round(x);

this doesn't work

filters:
      - lambda: return round(x, 1);

Partially solved the round problem: round function is recognized updating esphome over v.2023.11.

With filters: - round: 1 sometimes I still get values with multi decimals.

Do you know how to truncate to 1 decimal in esphome or in the driver?

FYI, after a day of trial and error, to get a round value you have to modify the string value in the driver:

String value = message.state
// mod
valuernd = String.format(Locale.US, "%.1f", Float.valueOf(value));

Has anyone solved the problem of reconnection of devices after a hub update?

Many thanks to @jonathanb for the awesome work on ESPHome for hubitat. I've started off with his GarageDoor driver and build a driver for those of us who use ratGDO :smiley:

I have also opened a PR against Jonathan's repo to add it there, but until he merges it, you can find the code here.

Things I did:

  • added most supported/exposed devices from ratGDO (except buttons for sync, opening time, closing time, things that are not really needed for automations) - and yes, position ranges from 0 (closed) to 100 (open) and updates in real time as the garage door travels :fist_right:

  • maintained his selector UI approach and made them populate automatically (but feel free to change them)

  • added the Door Control and Contact Sensor capabilities to satisfy HomeKit's requirements - the device shows up as a controllable Garage Door in HomeKit.

//todo:

  • introduce child devices for the sake of HomeKit - currently the device only shows up as a Garage Door, since HomeKit is not able to convert the device to multiple capabilities - there is no way currently to turn the light on/off from HomeKit, or lock/unlock the remotes, although you can trigger off things off of the motion sensor and the contact (door open/closed) sensor.

To get this working:
Step 1. install the ESPHome firmware on your ratDGO device using their ESPHome Installer in Chrome (for Mac, you need to install some USB drivers, check the bottom of that page).
Step 2. Install the ratGDO as per wiring instructions on page at step 1
Step 3. Allocate a static IP using your router - this is important so that the IP does not change, ever.
Step 4. Install Jonathan's ESPHome library (this is available via HPM)
Step 5. Install the ratGDO driver (manually for now in the Drivers code section, from my repo, hoping Jonathan adds it to his repo and the list of drivers in the HPM repo
Step 6. Create a virtual device of the ESPHome ratGDO type
Step 7. Enter the IP address you allocated at step 3 and click Save Preferences
Step 8. Refresh the page, all entities should have filled themselves up, feel free to make changes
Step 9. there is no step 9

2 Likes

updated the code to support child devices - this allows HomeKit to control the garage door opener light and remote locks, among other things.

1 Like

This works nicely, thanks. I'd suggest changing the light information to make the function clearer. 'Light On', 'Light Off' in the commands, 'light' instead of 'switch' in the states. And I'd request 'restart' be added. I've restarted a couple of times from the device web page, not sure if I've really needed to.

Also, not sure if you would be 'the guy', but it would be nice to be able to use encryption if the effort to implement is reasonable. Not because I care much, but because Home Assistant uses it by default.

Thanks again

Not sure I follow - are you talking about the names listed in the UI? I started with Hubitat a few days ago so I don't yet know all the semantics of how Hubitat is diff from SmartThings. Can you point me to what in code you'd like changed? Or open a PR, perhaps to the original repo.

Is there a restart() method? what does it do? is it called after a hub reboot? or restart the ratGDO device? Sorry, I'm confused.

And don't think I'm the guy for encryption, that'd be @jonathanb if he has time, or anyone else - I don't yet speak ESPHome, I just implemented the application layer on top of that comms layer, but if no one else, I can give it a shot.

I think I know what you mean about the restart, add the capability to restart the ratGDO device from the device itself, i.e. expose a restart() method so it can be restarted from rules, etc.

I tried to keep the GDO layer clean of any underlying features, restart is not a feature of the GDO itself, but rather the host contraption that allows access to the GDO, but sure, I will add the restart() method. How about sync()? toggle()? Would that be usefull on a garage door? LOL

Let me see if I get this right.

you want the command buttons in the device UI to read "Light On" instead of "On" - unfortunately, that's the name the Switch capability defines and the switch attribute is part of it too - to do what you want, I'd have to move away from the Switch capability and define a custom attribute light and two custom commands lightOn and lightOff - this means the device would no longer show as a switch - although the child device light would - note that the child device for light also uses On, Off, and switch as attribute. That's the standard.

Just updated to your driver - Wow!!! This is absolutely awesome!

Thank you so much for your great work on this. :smiley:

2 Likes

I am really torn between the entity inputs where you link the attributes to the ESPHome entities and hiding them since they are meant for generic devices, but this is a very particular one, so.. anyone has opinions on this?

Toggle is just like using the physical remote. I would not find it useful. Nor the sync.

That is what I was thinking. It works if you do all the actions noted, the light is still a switch (like the learn). One negative effect is to still have 'on' and 'off' buttons that don't do anything. The way to get around all that is to make the driver a parent/child with no duplication between the 2. That is more work than I would ask you to do.

So, after all that, adding a restart and keeping the light as is would be good for me.

Added restart, sync, toggle - give me a few minutes while I figure out getting rid of the preference inputs - I believe they are not needed.

2 Likes

The reason I don't want to do that is that having many devices (named differently) is worse than having one device with multiple capabilities. HomeKit is straight up dumb and can only support one actionable capability and sub-sensors - the garage door shows as a door with a contact and a motion sensor attached - but the light and lock don't show up. Alexa on the other hand can respond to "Alexa, turn on Garage Door", "Alexa, lock Garage Door", as well as "Alexa, open Garage Door" - this saves me the effort to remember and say distinct names for all those devices - the only reason to use child devices is really if you want to control them from HomeKit/Siri - but see, I have to say "Siri, lock the Garage Door Lock", "Siri, turn on the Garage Door Light", and "Siri, open the Garage Door" - not as cute as Alexa there...

2 Likes

Ok, updated the driver - my repo has the latest code, the original repo has merged the first version of the code, still needs to merge the latest changes, thank you @jonathanb :heart:

Please hit Refresh after changing and saving the driver code. Hoping all works fine, as it transitions to fully automated setup, no entities to choose. Also provides restart(), sync(), and toggle()

2 Likes

I notice in the web interface that there is also a stop button. In the device, there is a button - are those the same?

I tried the button function with 1 and also with 2, but it didn’t seem to do anything. Is there something else that this does?

1 Like

are you talking about the button in the middle of open/close buttons under "Door"?

I have not noticed that button, I can look into it... :slight_smile:

1 Like

Yup, that’s the one. I just tested it and it stops the door when pressed, regardless of the direction its going. Could be useful. :slight_smile:

You seem to have some options in your web interface that I don’t have - nice! (Paired Devices and Learn). More recent firmware maybe? I have 30f75c (I think…)

1 Like