A word about HA -> HE integration

Now we need @SmartHomePrimer to test and see if the sensors and switches both work correctly.

2 Likes

Itā€™s a color tunable OSRAM. Just using for testing.

So youā€™re able to delete the child devices, then go right back and click initialize and the same child devices will regenerate?

@ymerj I did actuate the device to force a change. That wasnā€™t enough for the light, or the contact sensors to be regenerated. Itā€™s an odd thing Iā€™ve not seen before with child devices. Iā€™ll test some of the iterations and see how things progress with that. I didnā€™t try removing them and re-adding to HA yet.

Very please with the interest in this driver development.

Not quite. You need a device event on the HA side after initialize.

EDIT: for clarity, @ogiewon is right that as long as the ws connection was never closed you shouldn't even need the initialize step.

Are your switches polled by HA, @SmartHomePrimer? Maybe there is some lag before HA sees the update and publishes an event. Just a guess.

I didnā€™t even have to click Initialize after deleting a child device. Just needed to manually toggle the switch from the HA side.

1 Like

After you deleted the child device, you need to go to your wall switch and flip it so an event get generated (on HA) in order to recreate the child device. For the sensor, it should recreate the child device the next time it reports a change state to HA.

It is a event in HA that trigger the creation of a corresponding child device on HE.

Not tested with a switch yet. Just contact sensors and a bulb. Iā€™ll test some more. It was late (early :stuck_out_tongue_winking_eye:) so maybe something was flawed in my test.

Iā€™m testing with Home Assistant on a Mac (not HASS.IO). I do have HASS.IO working in Virtual Box, but not sure I can get the Zigbee stick connected that way. Need to test.

So far, it seems the only devices that are able to generate on HE are those that are directly paired on HA. The devices that were joined via HomeKit Controller on HA didnā€™t generate on the HE side.

Again, Iā€™ll need to test more. Wonā€™t be able to get to this until this evening though.

I'm not using or testing this integration but it still fascinates me. Oh how I wish all these options were there for me during my transition. Great job getting things started @ymerj and I had a feeling I would see @ogiewon lend his expertise on this one :wink:.

What devices are you referring to specifically? I skimmed the code and it looks like this may only be getting data from entities. If your HA devices do not have associated entities in HA they may not work. Just a guess, based on what I've read so far.

2 Likes

Thanks Stephan, Iā€™m very new to HA so there liable to be some of the finer points I donā€™t see.

There should be entities from the devices coming in via HomeKit Controller to HA, but again Iā€™m going to need to repeat my testing this evening to confirm I have not made a mistake somewhere.

@tomw Line 100 ( subdomain = response.event.data.attributes.device_class ) is causing a parse error in my log preventing status update. The command goes trough but no event are registered.

My bad...it should probably be:

subdomain = response.event.data.new_state.attributes.device_class

1 Like

yes that's it.

1 Like

@tomw Also seeing an issue with the Binary Sensor lookup code. Here is my latest version that you can merge in, as I see you already have a PR open. I also tried to suppress as many errors as possible.

/*
HA integration
*
* Description:
* Allow control of HA devices.
*
* Required Information:
* Home Asisstant IP and Port number
* Home Assistant long term Access Token
*
* Features List:
*
* Licensing:
* Copyright 2021 Yves Mercier.
* 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.
*
* Version Control:
*
* 2021-02-06 Dan Ogorchock      Added basic support for simple "Light" devices from Home Assistant using Hubitat Generic Component Dimmer driver
* 2021-02-06 tomw               Added handling for some binary_sensor subtypes based on device_class
* 2021-02-06 Dan Ogorchock      Bug Fixes 
*
* Thank you(s):
*/

import groovy.json.JsonSlurper
import groovy.json.JsonOutput

metadata {
    definition (name: "HomeAssistant Hub Parent", namespace: "ymerj", author: "Yves Mercier") {
        capability "Initialize"

//        command "createChild", [[ name: "entity", type: "STRING", description: "HomeAssistant Entity ID" ]]
//        command "removeChild", [[ name: "entity", type: "STRING", description: "HomeAssistant Entity ID" ]]
//        command "closeConnection"
    }

    preferences {
        input ("ip", "text", title: "IP", description: "HomeAssistant IP Address", required: true)
        input ("port", "text", title: "Port", description: "HomeAssistant Port Number", required: true, defaultValue: "8123")
        input ("token", "text", title: "Token", description: "HomeAssistant Access Token", required: true)
        input ("logEnable", "bool", title: "Enable debug logging", defaultValue: true)
        input ("txtEnable", "bool", title: "Enable description text logging", defaultValue: true)        
    }
}

def logsOff(){
    log.warn("debug logging disabled...")
    device.updateSetting("logEnable",[value:"false",type:"bool"])
}

def updated(){
    log.info("updated...")
    log.warn("debug logging is: ${logEnable == true}")
    log.warn("description logging is: ${txtEnable == true}")
    unschedule()
    if (logEnable) runIn(1800,logsOff)
    initialize()
}

def initialize() {
    log.info("initializing...")
    state.id = 2
    auth = '{"type":"auth","access_token":"' + "${token}" + '"}'
    evenements = '{"id":1,"type":"subscribe_events","event_type":"state_changed"}'
    try {
        interfaces.webSocket.connect("ws://${ip}:${port}/api/websocket")
        interfaces.webSocket.sendMessage("${auth}")
        interfaces.webSocket.sendMessage("${evenements}")
    } 
    catch(e) {
        log.error("initialize error: ${e.message}")
    }
}

def uninstalled() {
    closeConnection()
}

def webSocketStatus(String status){
    if ((status == "status: open") || (status == "status: closing")) log.info("websocket ${status}")
    else {
        log.warn("WebSocket ${status}, trying to reconnect")
        runIn(10, initialize)
    }
}

def parse(String description) {
    if (logEnable) log.debug("parsed: $description")
    def response = null;
    try{
        response = new groovy.json.JsonSlurper().parseText(description)
        if (response.type != "event") return
        
        def entity = response?.event?.data.entity_id
        def domain = entity?.tokenize(".")[0]
        def subdomain = response?.event?.data?.attributes?.device_class
        if (!subdomain) subdomain = response?.event?.data?.new_state?.attributes?.device_class
        def friendly = response?.event?.data?.new_state?.attributes?.friendly_name
        def etat = response?.event?.data?.new_state?.state
        
        if (logEnable) log.debug "parse: domain: ${domain}, subdomain: ${subdomain}, entity: ${entity}, etat: ${etat}, friendly: ${friendly}"
        
        switch (domain) {
            case "switch":
                onOff(domain, entity, friendly, etat)
                break
            case "light":
                def level = response?.event?.data?.new_state?.attributes?.brightness
                if (level) {
                    level = level.toInteger()
                }
                onOffDim(domain, entity, friendly, etat, level)
                break
            default:
                if (subdomain) sendChildEvent(domain, subdomain, entity, friendly, etat)
        }
        return
    }  
    catch(e) {
        log.error("Parsing error: ${e}")
        return
    }
}

def onOff(domain, entity, friendly, etat) {
    if ((etat == "on") || (etat == "off")) {
        def ch = createChild(domain, null, entity, friendly)
        ch.parse([[name: "switch", value: etat, descriptionText:"${ch.label} was turned ${etat}"]])
    }
}

def onOffDim(domain, entity, friendly, etat, level) {
    
    onOff(domain, entity, friendly, etat)
    
    if (level) {
        def ch = createChild(domain, null, entity, friendly)
        level = (level * 100 / 255)
        level = Math.round(level) 
        ch.parse([[name:"level", value: level, descriptionText:"${ch.label} level set to ${level}"]])
    }
}

def sendChildEvent(domain, subdomain, entity, friendly, etat)
{
    def ch = createChild(domain, subdomain, entity, friendly)
    def mapping
    switch(domain)
    {
        case "binary_sensor":
            mapping = translateBinarySensorTypes(subdomain)
            break
    }
    if (mapping) {
        def name =  mapping?.attributes?.name ?: subdomain
        def value = mapping?.attributes?.states?."${etat}"?: "${etat}"
        ch.parse([[name: name, value: value, descriptionText:"${ch.label} updated"]])    
    }
}

def createChild(domain, subdomain, entity, friendly)
{
    String thisId = device.id
    def ch = getChildDevice("${thisId}-${entity}")
    if (!ch)
    {
        def deviceType
        switch(domain)
        {
            case "switch":
                deviceType = "Generic Component Switch"
                break
            case "light":
                deviceType = "Generic Component Dimmer"
                break
            case "binary_sensor":
                deviceType = translateBinarySensorTypes(subdomain).type
                break
            
            default:
                return null
        }
        
        ch = addChildDevice("hubitat", deviceType, "${thisId}-${entity}", [name: "${entity}", label: "${friendly}", isComponent: false])
    }
    
    return ch
}

def translateBinarySensorTypes(device_class)
{
    def mapping =
        [
            door: [type: "Generic Component Contact Sensor", attributes: [name: "contact", states: [on: "closed", off: "open"]]],
            garage_door: [type: "Generic Component Contact Sensor", attributes: [name: "contact", states: [on: "closed", off: "open"]]],
            moisture: [type: "Generic Component Water Sensor", attributes: [name: "water", states: [on: "wet", off: "dry"]]],
            motion: [type: "Generic Component Motion Sensor", attributes: [name: "motion", states: [on: "active", off: "inactive"]]],
            opening: [type: "Generic Component Contact Sensor", attributes: [name: "contact", states: [on: "closed", off: "open"]]],
            presence: [type: "Generic Component Presence Sensor", attributes: [name: "presence", states: [on: "present", off: "not present"]]],
            window: [type: "Generic Component Contact Sensor", attributes: [name: "contact", states: [on: "closed", off: "open"]]]
        ]
    return mapping[device_class]
}

def removeChild(entity){
    String thisId = device.id
    def ch = getChildDevice("${thisId}-${entity}")
    if (ch) {deleteChildDevice("${thisId}-${entity}")}
}
    
def componentOn(ch){
    if (logEnable) log.info("received on request from ${ch.label}")
    state.id = state.id + 1
    entity = ch.name
    domain = entity.tokenize(".")[0]
    if (!ch.currentValue("level")) {
        messOn = JsonOutput.toJson([id: state.id, type: "call_service", domain: "${domain}", service: "turn_on", service_data: [entity_id: "${entity}"]])
    }
    else {
        messOn = JsonOutput.toJson([id: state.id, type: "call_service", domain: "${domain}", service: "turn_on", service_data: [entity_id: "${entity}", brightness_pct: "${ch.currentValue("level")}"]])        
    }
    if (logEnable) log.debug("messOn = ${messOn}")
    interfaces.webSocket.sendMessage("${messOn}")
}

def componentOff(ch){
    if (logEnable) log.info("received off request from ${ch.label}")
    state.id = state.id + 1
    entity = ch.name
    domain = entity.tokenize(".")[0]
    messOff = JsonOutput.toJson([id: state.id, type: "call_service", domain: "${domain}", service: "turn_off", service_data: [entity_id: "${entity}"]])
    if (logEnable) log.debug("messOff = ${messOff}")
    interfaces.webSocket.sendMessage("${messOff}")
}

def componentSetLevel(ch, level, transition=1){
    if (logEnable) log.info("received setLevel request from ${ch.label}")
    if (level > 100) level = 100
    if (level < 0) level = 0
    state.id = state.id + 1
    entity = ch.name
    domain = entity.tokenize(".")[0]
    messLevel = JsonOutput.toJson([id: state.id, type: "call_service", domain: "${domain}", service: "turn_on", service_data: [entity_id: "${entity}", brightness_pct: "${level}", transition: "${transition}"]])
    if (logEnable) log.debug("messLevel = ${messLevel}")
    interfaces.webSocket.sendMessage("${messLevel}")
}

def componentRefresh(ch){
    if (logEnable) log.info("received refresh request from ${ch.label}")
}

def closeConnection() {
    if (logEnable) log.debug("Closing connection...")
    interfaces.webSocket.sendMessage('{"id":2,"type":"unsubscribe_events","subscription":1}')
    interfaces.webSocket.close()
}
1 Like

I just stepped away for the day. Sorry to leave it in this state for you guys. Will you please merge in your fixes and the one in my PR @ogiewon?

Edit: it looks like @ymerj already merged my existing PR

Edit2: probably wasn't clear, but we still need @ogiewon's additional changes

1 Like

Just installed the driver to see how it would handle a bunch of devices and can report that so far it's holding up perfectly....only wish that HE would have incorporated the ability to collapse/expand the device list by now. It's found 92 devices so far so you can imagine that I have a reasonable scroll time going through the child devices. I can't imagine what this list would be like once more device types start getting added.

You all might want to consider adding a selective list rather than automatic child device creation. The last time I checked, I had over 500 entities in HA. Just an FYI that things might get unwieldly for users with a ton of stuff in HA.

Let me know if you need to test out any particular device types or scenarios and I'd be glad to help.

3 Likes

You also might want to consider a way to tally disable logging. This is my log for the last minute with both of the logging preferences disabled in the parent driver.

These are error logs. They are not affected by the logging preferences at the moment. Mainly because they should not occur. Which version do you have?

Version of your driver? I don't see it in the code, but it was whatever version you had on github about 20 minutes before my last post.

I assumed there were errors generated by unsupported device type in HA.

Ok. I change the file with the last bug fix from @ogiewon. Try it if you please to see if it fixes your errors.

2 Likes

Will do.

Please add in versions if that hasnā€™t been done. Gets pretty confusing as the PRs start rolling in.

2 Likes