Now we need @SmartHomePrimer to test and see if the sensors and switches both work correctly.
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.
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 ) 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 .
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.
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
yes that's it.
@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()
}
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
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.
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.
Will do.
Please add in versions if that hasnāt been done. Gets pretty confusing as the PRs start rolling in.