Hunter Douglas API

Yeah I'm actually surprised he provided the shade information. It's awesome that they did.

1 Like

Hi, I'm a brand new Hubitat user and integrating with Hunter Douglas Powerview was my first use case. So first up - thank you! Your integration has completely sold me on the solution and the community. In the spirit of community, I have a v1 Hunter Douglas hub and had to make some tweaks to hunter-douglas-powerview.groovy to get things working with it. I don't have a Hunter Douglas v2 hub or repeaters to test with so I tried to make the changes in the least invasive way I could. What's the best way for me to contribute those changes, or better yet have you look them over and make sure I didn't break your working code?

If you post your version here I can compare it to my version as well as test it with my v2 hub to make sure it still works.

/**
 *  Hunter Douglas PowerView
 *
 *  Copyright 2017 Chris Lang
 *
 *  Ported to Hubitat by Brian Ujvary
 *
 *  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.
 *
 *  Change Log:
 *    05/10/2020 v1.0 - Initial release
 *
 */
definition(
    name: "Hunter Douglas PowerView",
    namespace: "hdpowerview",
    author: "Chris Lang",
    description: "Provides control of Hunter Douglas shades, scenes and repeaters via the PowerView hub.",
    category: "Convenience",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
    iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
    singleInstance: true,
    importUrl: "https://raw.githubusercontent.com/bujvary/hubitat/master/apps/hunter-douglas-powerview.groovy"
)


preferences {
    section("Title") {
        page(name: "mainPage")
        page(name: "devicesPage")
        page(name: "roomsPage")
    }
}


/*
 * Firmware Version GLOBAL
 */
gFirmware = [revision: 0, subRevision: 0, build: 0]
getFirmwareInfo()

/*
 * Pages
 */
def mainPage() {
    def setupComplete = !!atomicState?.shades
    def pageProperties = [
        name: "mainPage",
        title: "",
        install: setupComplete,
        uninstall: atomicState?.installed
    ]

		
    return dynamicPage(pageProperties) {
        section("PowerView Hub") {
            input("powerviewIPAddress", "text", title: "IP Address", defaultValue: "", description: "(ie. 192.168.1.10)", required: true, submitOnChange: true)
        }
        if (settings?.powerviewIPAddress) {
            section("Devices & Scenes") {
                def description = (atomicState?.deviceData) ? "Click to modify" : "Click to configure";
                href "devicesPage", title: "Manage Devices", description: description, state: "complete"
                atomicState?.loadingDevices = false

                input("disablePoll", "bool", title: "Disable periodic polling of devices", required: false, defaultValue: false)
                input("logEnable", "bool", title: "Enable debug logging", required: false, defaultValue: true)
            }
        }
    }
}

def devicesPage() {
    def pageProperties = [
        name: "devicesPage",
        title: "Manage Devices"
    ]

    if (logEnable) log.debug "atomicState?.loadingDevices = ${atomicState?.loadingDevices}"
    if (!atomicState?.loadingDevices) {
        atomicState?.loadingDevices = true
    }

    if (logEnable) log.debug "atomicState?.deviceData = ${atomicState?.deviceData}"
    if (!atomicState?.deviceData?.shades || !atomicState?.deviceData?.scenes || !atomicState?.deviceData?.rooms || (!atomicState?.deviceData?.repeaters && gFirmware.revision > 1) ) {
        pageProperties["refreshInterval"] = 1
        return dynamicPage(pageProperties) {
            section("Discovering Devices...") {
                paragraph "Please wait..."
            }
        }
    }

    return dynamicPage(pageProperties) {
        section("Rooms") {
            href "roomsPage", title: "Manage Rooms", description: "Click to configure open/close scenes for each room", state: "complete"
        }
        section("Shades") {
            input("syncShades", "bool", title: "Automatically sync all shades", required: false, defaultValue: true, submitOnChange: true)
            if (settings?.syncShades == true || settings?.syncShades == null) {
                def shadesDesc = atomicState?.deviceData?.shades.values().join(", ")
                paragraph "The following shades will be added as devices: ${shadesDesc}"
                atomicState?.shades = atomicState?.deviceData?.shades
            } else {
                def shadesList = getDiscoveredShadeList()
                input(name: "shades", title: "Shades", type: "enum", required: false, multiple: true, submitOnChange: true, options: shadesList)
                atomicState?.shades = getSelectedShades(settings?.shades)
                if (logEnable) log.debug "shades: ${settings?.shades}"
            }
        }
        section("Scenes") {
            input("syncScenes", "bool", title: "Automatically sync all scenes", required: false, defaultValue: true, submitOnChange: true)
            if (settings?.syncScenes == true || settings?.syncScenes == null) {
                def scenesDesc = atomicState?.deviceData?.scenes.values().join(", ")
                paragraph "The following scenes will be added as devices: ${scenesDesc}"
                atomicState?.scenes = atomicState?.deviceData?.scenes
            } else {
                def scenesList = getDiscoveredSceneList()
                input(name: "scenes", title: "Scenes", type: "enum", required: false, multiple: true, submitOnChange: true, options: scenesList)
                atomicState?.scenes = getSelectedScenes(settings?.scenes)
                if (logEnable) log.debug "scenes: ${settings?.scenes}"
            }
        }
        if (gFirmware.revision > 1) {
	        section("Repeaters") {
	            input("syncRepeaters", "bool", title: "Automatically sync all repeaters", required: false, defaultValue: true, submitOnChange: true)
	            if (settings?.syncRepeaters == true || settings?.syncRepeaters == null) {
	                def repeatersDesc = atomicState?.deviceData?.repeaters.values().join(", ")
	                paragraph "The following repeaters will be added as devices: ${repeatersDesc}"
	                atomicState?.repeaters = atomicState?.deviceData?.repeaters
	            } else {
	                def repeatersList = getDiscoveredRepeaterList()
	                input(name: "repeaters", title: "Repeaters", type: "enum", required: false, multiple: true, submitOnChange: true, options: repeatersList)
	                atomicState?.repeaters = getSelectedRepeaters(settings?.repeaters)
	                if (logEnable) log.debug "repeaters: ${settings?.repeaters}"
	            }
	        }
      	}
      	section("Hub Firmware Version"){
      		paragraph "${gFirmware.revision}.${gFirmware.subRevision}.${gFirmware.build}"
      	}
    }
}

def roomsPage() {
    def pageProperties = [
        name: "roomsPage",
        title: "Manage Rooms"
    ]

    dynamicPage(pageProperties) {
        section {
            paragraph("Configure scenes to open or close the blinds in each room. A virtual device will be created for each room so configured.")
        }
        def rooms = [:]
        atomicState?.deviceData.rooms.collect { id, name ->
            section(name) {
                def openSetting = "room" + id + "Open"
                def closeSetting = "room" + id + "Close"
                def description
                if (settings[openSetting] && settings[closeSetting]) {
                    description = "Blinds in this room will open and close via the configured scenes."
                } else if (settings[openSetting]) {
                    description = "Blinds in this room will open via the configured scene, but not close."
                } else if (settings[closeSetting]) {
                    description = "Blinds in this room will close via the configured scene, but not open."
                } else {
                    description = "No virtual device will be created for this room because neither open nor close scenes are configured."
                }
                paragraph(description)

                // TODO limit to scenes for this room or multi-room scenes
                def scenesList = getDiscoveredSceneList()
                input(name: openSetting, title: "Open", type: "enum", required: false, multiple: false, submitOnChange: true, options: scenesList)
                input(name: closeSetting, title: "Close", type: "enum", required: false, multiple: false, submitOnChange: true, options: scenesList)

                rooms[id] = [
                    name: name,
                    openScene: settings[openSetting],
                    closeScene: settings[closeSetting],
                ]
            }
        }
        atomicState?.rooms = rooms
        if (logEnable) log.debug "atomicState?.rooms = ${atomicState?.rooms}"
    }
}

/*
 * Service Manager lifecycle
 */
def installed() {
    if (logEnable) log.debug "Installed with settings: ${settings}"

    initialize()
}

def updated() {
    if (logEnable) log.debug "Updated with settings: ${settings}"

    initialize()
}

def uninstalled() {
    removeDevices()
    unsubscribe()
    unschedule()
}

def initialize() {
    atomicState?.installed = true
    unsubscribe()
    addDevices()

    unschedule()
    pollDevices(true)
    runEvery5Minutes("pollDevices")
    
    if (logEnable) runIn(900, logsOff)
}

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

def addDevices() {
    if (logEnable) log.debug "In addDevices()"
    if (atomicState?.rooms) {
        atomicState?.rooms?.collect { id, room ->
            if (logEnable) log.debug "checking room ${id}"
            if (room.openScene || room.closeScene) {
                def dni = roomIdToDni(id)
                def child = getChildDevice(dni)
                if (!child) {
                    child = addChildDevice("hdpowerview", "Hunter Douglas PowerView Room", dni, null, [label: getRoomLabel(room.name)])
                    if (logEnable) log.debug "Created child '${child}' with dni ${dni}"
                }
            }
        }
    }
    if (atomicState?.shades) {
        atomicState?.shades?.collect { id, name ->
            def dni = shadeIdToDni(id)
            def child = getChildDevice(dni)
            if (!child) {
                child = addChildDevice("hdpowerview", "Hunter Douglas PowerView Shade", dni, null, [label: name])
                if (logEnable) log.debug "Created child '${child}' with dni ${dni}"
            }
        }
    }
    if (atomicState?.scenes) {
        atomicState?.scenes?.collect { id, name ->
            def dni = sceneIdToDni(id)
            def child = getChildDevice(dni)
            if (!child) {
                child = addChildDevice("hdpowerview", "Hunter Douglas PowerView Scene", dni, null, [label: name])
                if (logEnable) log.debug "Created child '${child}' with dni ${dni}"
            }
        }
    }
    if (atomicState?.repeaters) {
        atomicState?.repeaters?.collect { id, name ->
            def dni = repeaterIdToDni(id)
            def child = getChildDevice(dni)
            if (!child) {
                child = addChildDevice("hdpowerview", "Hunter Douglas PowerView Repeater", dni, null, [label: name])
                if (logEnable) log.debug "Created child '${child}' with dni ${dni}"
            }
        }
    }
}

def removeDevices() {
    if (logEnable) log.debug "In removeDevices()"

    try {
        getChildDevices()?.each {
            try {
                if (logEnable) log.debug "Deleting device ${it.deviceNetworkId}"
                deleteChildDevice(it.deviceNetworkId)
            } catch (e) {
                if (logEnable) log.debug "Error deleting ${it.deviceNetworkId}: ${e}"
            }
        }
    } catch (err) {
        if (logEnable) log.debug "Either no children exist or error finding child devices for some reason: ${err}"
    }
}

def pollDevices(firstPoll = false) {
    def now = now()
    def updateBattery = false
    def runDelay = 1

    if (!firstPoll && disablePoll) {
        if (logEnable) log.debug "pollDevices: skipping polling because polling is disabled"
        return
    }

    // Update battery status no more than once an hour
    if (!atomicState?.lastBatteryUpdate || (atomicState?.lastBatteryUpdate - now) > (60 * 60 * 1000)) {
        updateBattery = true
        atomicState?.lastBatteryUpdate = now
    }

    if (logEnable) log.debug "pollDevices: updateBattery = ${updateBattery}"

    getShadeDevices().eachWithIndex { device, index ->
        if (device != null) {
            def shadeId = dniToShadeId(device.deviceNetworkId)
            
            if (logEnable) log.debug "Running pollShadeDelayed() with runDelay = ${runDelay} for shade ${shadeId} (index = ${index})"
            
            runIn(runDelay, "pollShadeDelayed", [overwrite: false, data: [shadeId: shadeId, updateBattery: updateBattery]])
            runDelay += 5
        } else {
            if (logEnable) log.debug "Got null shade device, index ${index}"
        }
    }
		
		
    if (gFirmware.revision > 1) {
	    getRepeaterDevices().eachWithIndex { device, index ->
	        if (device != null) {
	            def repeaterId = dniToRepeaterId(device.deviceNetworkId)
	            
	            if (logEnable) log.debug "Running pollRepeaterDelayed() with runDelay = ${runDelay} for repeater ${repeaterId}"
	            
	            runIn(runDelay, "pollRepeaterDelayed", [overwrite: false, data: [repeaterId: repeaterId]])
	            runDelay += 5
	        } else {
	            if (logEnable) log.debug "Got null repeater device, index ${index}"
	        }
	    }
  	}
}

def pollShadeDelayed(data) {
    if (logEnable) log.debug "pollShadeDelayed: data: ${data}"
    pollShadeId(data.shadeId, data.updateBattery)
}


def pollRepeaterDelayed(data) {
    if (logEnable) log.debug "pollRepeaterDelayed: data: ${data}"
    pollRepeaterId(data.repeaterId)
}


/*
 * Device management
 */
def getDevices() {
    getRooms()
    getShades()
    getScenes()
    if (gFirmware.revision > 1) {
    	getRepeaters()
    }
}

def getRoomLabel(roomName) {
    return "${roomName} Blinds"
}

def getRoomDniPrefix() {
    return "PowerView-Room-"
}

def getSceneDniPrefix() {
    return "PowerView-Scene-"
}

def getShadeDniPrefix() {
    return "PowerView-Shade-"
}


def getRepeaterDniPrefix() {
    return "PowerView-Repeater-"
}


def roomIdToDni(id) {
    return "${getRoomDniPrefix()}${id}"
}

def dniToRoomId(dni) {
    def prefix = getRoomDniPrefix()
    return dni.startsWith(prefix) ? dni.replace(prefix, "") : null
}

def sceneIdToDni(id) {
    return "${getSceneDniPrefix()}${id}"
}

def dniToSceneId(dni) {
    def prefix = getSceneDniPrefix()
    return dni.startsWith(prefix) ? dni.replace(prefix, "") : null
}

def shadeIdToDni(id) {
    return "${getShadeDniPrefix()}${id}"
}

def dniToShadeId(dni) {
    def prefix = getShadeDniPrefix()
    return dni.startsWith(prefix) ? dni.replace(prefix, "") : null
}

def repeaterIdToDni(id) {
    return "${getRepeaterDniPrefix()}${id}"
}

def dniToRepeaterId(dni) {
    def prefix = getRepeaterDniPrefix()
    return dni.startsWith(prefix) ? dni.replace(prefix, "") : null
}

def getSceneDevices() {
    return atomicState?.scenes?.keySet().collect {
        getChildDevice(sceneIdToDni(it))
    }
}

def getShadeDevice(shadeId) {
    return getChildDevice(shadeIdToDni(shadeId))
}

def getShadeDevices() {
    return atomicState?.shades?.keySet().collect {
        getChildDevice(shadeIdToDni(it))
    }
}

def getRepeaterDevice(repeaterId) {
    return getChildDevice(repeaterIdToDni(repeaterId))
}

def getRepeaterDevices() {
    return atomicState?.repeaters?.keySet().collect {
        getChildDevice(repeaterIdToDni(it))
    }
}

// data can contain 'shades', 'scenes', "rooms' and/or 'repeaters' -- only deviceData for specified device types is updated
def updateDeviceDataState(data) {
    def deviceData = atomicState?.deviceData ?: [:]

    if (data?.rooms) {
        deviceData["rooms"] = data?.rooms
    }
    if (data?.scenes) {
        deviceData["scenes"] = data?.scenes
    }
    if (data?.shades) {
        deviceData["shades"] = data?.shades
    }    
    if (data?.repeaters) {
        deviceData["repeaters"] = data?.repeaters
    }

    atomicState?.deviceData = deviceData
    if (logEnable) log.debug "updateDeviceData: atomicState.deviceData: ${atomicState?.deviceData}"
}

def getSelectedShades(Collection selectedShadeIDs) {
    return getSelectedDevices(atomicState?.deviceData?.shades, selectedShadeIDs)
}

def getSelectedScenes(Collection selectedSceneIDs) {
    return getSelectedDevices(atomicState?.deviceData?.scenes, selectedSceneIDs)
}


def getSelectedRepeaters(Collection selectedRepeaterIDs) {
    return getSelectedDevices(atomicState?.deviceData?.repeaters, selectedRepeaterIDs)
}


def getSelectedDevices(Map devices, Collection selectedDeviceIDs) {
    if (!selectedDeviceIDs) {
        return [:]
    }
    return devices?.findAll {
        selectedDeviceIDs.contains(it.key)
    }
}

def getDiscoveredShadeList() {
    def ret = [:]
    atomicState?.deviceData?.shades.each { shade ->
            ret[shade.key] = shade.value
    }
    return ret
}

def getDiscoveredSceneList() {
    def ret = [:]
    atomicState?.deviceData?.scenes.each { scene ->
            ret[scene.key] = scene.value
    }
    return ret
}

def getDiscoveredRepeaterList() {
    def ret = [:]
    atomicState?.deviceData?.repeaters.each { repeater ->
            ret[repeater.key] = repeater.value
    }
    return ret
}

/*
 * PowerView API
 */

// FIRMWARE VERSION
def getFirmwareInfo(){
	callPowerView("fwversion", firmwareVersionCallback)
}

void firmwareVersionCallback(hubitat.device.HubResponse hubResponse) {
    if (logEnable) log.debug "Entered firmwareVersionCallback()..."
    if (logEnable) log.debug "json: ${hubResponse.json}"
    
    gFirmware.revision = hubResponse.json.firmware.mainProcessor.revision
    gFirmware.subRevision = hubResponse.json.firmware.mainProcessor.subRevision
    gFirmware.build = hubResponse.json.firmware.mainProcessor.build

}

// ROOMS

def getRooms() {
    callPowerView("rooms", roomsCallback)
}

def openRoom(roomDevice) {
    if (logEnable) log.debug "openRoom: roomDevice = ${roomDevice}"

    def roomId = dniToRoomId(roomDevice.deviceNetworkId)
    def sceneId = atomicState?.rooms[roomId]?.openScene
    if (sceneId) {
        triggerScene(sceneId)
    } else {
        log.info "no open scene configured for room ${roomId}"
    }
}

def closeRoom(roomDevice) {
    if (logEnable) log.debug "closeRoom: roomDevice = ${roomDevice}"

    def roomId = dniToRoomId(roomDevice.deviceNetworkId)
    def sceneId = atomicState?.rooms[roomId]?.closeScene
    if (sceneId) {
        triggerScene(sceneId)
    } else {
        log.info "no close scene configured for room ${roomId}"
    }
}

void roomsCallback(hubitat.device.HubResponse hubResponse) {
    if (logEnable) log.debug "Entered roomsCallback()..."
    if (logEnable) log.debug "json: ${hubResponse.json}"

    def rooms = [:]
    hubResponse.json.roomData.each { room ->
        def name = new String(room.name.decodeBase64())
        rooms[room.id] = name
        if (logEnable) log.debug "room: ID = ${room.id}, name = ${name}"
    }

    updateDeviceDataState([rooms: rooms])
}

// SCENES

def getScenes() {
    callPowerView("scenes", scenesCallback)
}

def triggerSceneFromDevice(sceneDevice) {
    def sceneId = dniToSceneId(sceneDevice.deviceNetworkId)
    triggerScene(sceneId)
}

def triggerScene(sceneId) {
    callPowerView("scenes?sceneId=${sceneId}", triggerSceneCallback)
}

void scenesCallback(hubitat.device.HubResponse hubResponse) {
    if (logEnable) log.debug "Entered scenesCallback()..."
    if (logEnable) log.debug "json: ${hubResponse.json}"

    def scenes = [:]
    hubResponse.json.sceneData.each {scene ->
        def name = new String(scene.name.decodeBase64())
        scenes[scene.id] = name
        if (logEnable) log.debug "scene: ID = ${scene.id}, name = ${name}"
    }

    updateDeviceDataState([scenes: scenes])
}

def triggerSceneCallback(hubitat.device.HubResponse hubResponse) {
    if (logEnable) log.debug "Entered triggerScenesCallback()..."

    if (hubResponse.status != 200) {
        log.warn("got unexpected response: status=${hubResponse.status} body=${hubResponse.body}")
    } else {
        runIn(15, pollDevices)
    }
}

// SHADES 

def getShades() {
    callPowerView("shades", shadesCallback)
}

def pollShade(shadeDevice, updateBatteryStatus = false) {
    if (logEnable) log.debug "pollShade: shadeDevice = ${shadeDevice}"
    def shadeId = dniToShadeId(shadeDevice.deviceNetworkId)
    pollShadeId(shadeId)
}

def pollShadeId(shadeId, updateBatteryStatus = false) {
    if (logEnable) log.debug "pollShadeId: shadeId = ${shadeId}"

    def query = [:]
    if (updateBatteryStatus)
        query = [updateBatteryLevel: "true"]
    else
        query = [refresh: "true"]

    callPowerView("shades/${shadeId}", shadePollCallback, query)
}

def calibrateShade(shadeDevice) {
    if (logEnable) log.debug "calibrateShade: shadeDevice = ${shadeDevice}"
    moveShade(shadeDevice, [motion: "calibrate"])
}

def jogShade(shadeDevice) {
    if (logEnable) log.debug "jogShade: shadeDevice = ${shadeDevice}"
    moveShade(shadeDevice, [motion: "jog"])
}

def setPosition(shadeDevice, positions) {
    if (logEnable) log.debug "setPosition: shadeDevice = ${shadeDevice}, positions = ${positions}"

    def shadePositions = [:]
    def positionNumber = 1

    if (positions?.containsKey("bottomPosition")) {
        shadePositions["posKind${positionNumber}"] = 1
        shadePositions["position${positionNumber}"] = (int)(positions.bottomPosition * 65535 / 100)
        positionNumber += 1
    }

    if (positions?.containsKey("topPosition")) {
        shadePositions["posKind${positionNumber}"] = 2
        shadePositions["position${positionNumber}"] = (int)(positions.topPosition * 65535 / 100)
    }

    if (positions?.containsKey("position")) {
        shadePositions["posKind${positionNumber}"] = 1
        shadePositions["position${positionNumber}"] = (int)(positions.position * 65535 / 100)
    }

    moveShade(shadeDevice, [positions: shadePositions])
}

def moveShade(shadeDevice, movementInfo) {
    def shadeId = dniToShadeId(shadeDevice.deviceNetworkId)

    def body = [:]
    body["shade"] = movementInfo

    def json = new groovy.json.JsonBuilder(body)
    callPowerView("shades/${shadeId}", setPositionCallback, null, "PUT", json.toString())
}

void shadePollCallback(hubitat.device.HubResponse hubResponse) {
    if (logEnable) log.debug "Entered shadePollCallback()..."
    if (logEnable) log.debug "json: ${hubResponse.json}"

    def shade = hubResponse.json.shade
    def childDevice = getShadeDevice(shade.id)

    if (logEnable) log.debug "shadePollCallback for shade id ${shade.id}, calling device ${childDevice}"
    childDevice.handleEvent(shade)
}

void setPositionCallback(hubitat.device.HubResponse hubResponse) {
    if (logEnable) log.debug "Entered setPositionCallback()..."
    if (logEnable) log.debug "json: ${hubResponse.json}"

    def shade = hubResponse.json.shade
    def childDevice = getShadeDevice(shade.id)

    if (logEnable) log.debug "setPositionCallback for shadeId ${shade.id}, calling device ${childDevice}"
    childDevice.handleEvent(shade)
}


void shadesCallback(hubitat.device.HubResponse hubResponse) {
    if (logEnable) log.debug "Entered shadesCallback()..."
    if (logEnable) log.debug "json: ${hubResponse.json}"

    def shades = [:]
    hubResponse.json.shadeData.each { shade ->
        def name = shade.name ? new String(shade.name.decodeBase64()) : "Shade ID ${shade.id}"
        shades[shade.id] = name
        if (logEnable) log.debug "shade: ID = ${shade.id}, name = ${name}"
    }

    updateDeviceDataState([shades: shades])
}

// REPEATERS
def getRepeaters() {
    callPowerView("repeaters", repeatersCallback)
}

void repeatersCallback(hubitat.device.HubResponse hubResponse) {
    if (logEnable) log.debug "Entered repeatersCallback()..."
    if (logEnable) log.debug "json: ${hubResponse.json}"

    def repeaters = [:]
    hubResponse.json.repeaterData.each { repeater ->
        def name = repeater.name ? new String(repeater.name.decodeBase64()) : "Repeater ID ${repeater.id}"
        repeaters[repeater.id] = name
        if (logEnable) log.debug "repeater: ID = ${repeater.id}, name = ${name}"
    }

    updateDeviceDataState([repeaters: repeaters])
}

def pollRepeater(repeaterDevice) {
    if (logEnable) log.debug "pollRepeater: repeaterDevice = ${repeaterDevice}"

    def repeaterId = dniToRepeaterId(repeaterDevice.deviceNetworkId)
    pollRepeaterId(repeaterId)
}

def pollRepeaterId(repeaterId) {
    if (logEnable) log.debug "pollRepeaterId: repeaterId = ${repeaterId}"

    callPowerView("repeaters/${repeaterId}", repeaterPollCallback)
}

def setRepeaterPrefs(repeaterDevice, prefs) {
    def repeaterId = dniToRepeaterId(repeaterDevice.deviceNetworkId)

    def body = [:]
    body["repeater"] = prefs

    def json = new groovy.json.JsonBuilder(body)
    callPowerView("repeaters/${repeaterId}", setRepeaterPrefsCallback, null, "PUT", json.toString())
}

void setRepeaterPrefsCallback(hubitat.device.HubResponse hubResponse) {
    if (logEnable) log.debug "Entered setRepeaterPrefsCallback()..."
    if (logEnable) log.debug "json: ${hubResponse.json}"

    def repeater = hubResponse.json.repeater
    def childDevice = getRepeaterDevice(repeater.id)

    if (logEnable) log.debug "setRepeaterPrefs Callback for repeaterId ${repeater.id}, calling device ${childDevice}"
    childDevice.handleEvent(repeater)
}

void repeaterPollCallback(hubitat.device.HubResponse hubResponse) {
    if (logEnable) log.debug "Entered repeaterPollCallback()..."
    if (logEnable) log.debug "json: ${hubResponse.json}"

    def repeater = hubResponse.json.repeater
    def childDevice = getRepeaterDevice(repeater.id)

    if (logEnable) log.debug "repeaterPollCallback for repeater id ${repeater.id}, calling device ${childDevice}"
    childDevice.handleEvent(repeater)
}

// CORE API

def callPowerView(String path, callback, Map query = null, String method = "GET", String body = null) {
    def host = "${settings?.powerviewIPAddress}:80"
    def fullPath = "/api/${path}"

    if (logEnable) log.debug "callPowerView: url = 'http://${host}${fullPath}', method = '${method}', body = '${body}', query = ${query}, callback = ${callback}"

    def headers = [
        "HOST": host,
        'Content-Type': 'application/json'
    ]

    def hubAction = new hubitat.device.HubAction(
        method: method,
        path: fullPath,
        headers: headers,
        query: query,
        body: body,
        null,
        [callback: callback]
    )

    if (logEnable) log.debug "Sending HubAction: ${hubAction}"

    sendHubCommand(hubAction)
}

If you run the following in your browser what do you get back:

http://[powerview hub ip]/api/repeaters

And run this too please and provide the response:

http://[powerview hub ip]/api/fwversion

@bujvary: I have a driver I have been using for a while. It was a port from someone else's Smartthings driver. All I did is port over the Scenecollection (which i just realized I named ShadeCollection). It does support tilting open the blinds.

https://github.com/matt1097/powerview
Feel free to compare to yours.

I have a v1 hub with Silhouette blinds.

http://[powerview hub ip]/api/repeaters
via Chrome => ERR_EMPTY_RESPONSE
via curl from the terminal => curl: (52) Empty reply from server

http://[powerview hub ip]/api/fwversion
via Chrome => {"firmware":{"mainProcessor":{"name":"PowerView Hub","revision":1,"subRevision":1,"build":857}}}

I hope that helps! It would be great to get the repeaters in, there's also a distinct possibility that I've included my repeaters in some abnormal way. It would be great to verify with another install.

Hey there, i just got power view blinds installed yesterday and found your app/drivers. I'm having a slight issue getting it up and running. I installed the drivers and the app, i put in the ip address of my hub and click configure it says discovering devices please wait and this goes on for, well i left it 10 mins and nothing changed. the logs show this

app:11252020-06-18 11:03:21.979 debugatomicState?.deviceData = [rooms:[17973:Kitchen, 45453:Default Room], shades:[50062:Blind 4, 51264:Right Rear, 65359:Blind 3, 23777:Bifold Doors], scenes:[60073:Middle Position]]
app:11252020-06-18 11:03:21.971 debugatomicState?.loadingDevices = true
app:11252020-06-18 11:03:20.968 debugatomicState?.deviceData = [rooms:[17973:Kitchen, 45453:Default Room], shades:[50062:Blind 4, 51264:Right Rear, 65359:Blind 3, 23777:Bifold Doors], scenes:[60073:Middle Position]]
app:11252020-06-18 11:03:20.960 debugatomicState?.loadingDevices = true
app:11252020-06-18 11:03:19.971 debugatomicState?.deviceData = [rooms:[17973:Kitchen, 45453:Default Room], shades:[50062:Blind 4, 51264:Right Rear, 65359:Blind 3, 23777:Bifold Doors], scenes:[60073:Middle Position]]
app:11252020-06-18 11:03:19.962 debugatomicState?.loadingDevices = true
app:11252020-06-18 11:03:18.979 debugatomicState?.deviceData = [rooms:[17973:Kitchen, 45453:Default Room], shades:[50062:Blind 4, 51264:Right Rear, 65359:Blind 3, 23777:Bifold Doors], scenes:[60073:Middle Position]]
app:11252020-06-18 11:03:18.967 debugatomicState?.loadingDevices = true
app:11252020-06-18 11:03:18.115 debugupdateDeviceData: atomicState.deviceData: [rooms:[17973:Kitchen, 45453:Default Room], shades:[50062:Blind 4, 51264:Right Rear, 65359:Blind 3, 23777:Bifold Doors], scenes:[60073:Middle Position]]
app:11252020-06-18 11:03:18.107 debugjson: [repeaterIds:, repeaterData:]
app:11252020-06-18 11:03:18.106 debugEntered repeatersCallback()...
app:11252020-06-18 11:03:18.084 debugupdateDeviceData: atomicState.deviceData: [rooms:[17973:Kitchen, 45453:Default Room], shades:[50062:Blind 4, 51264:Right Rear, 65359:Blind 3, 23777:Bifold Doors], scenes:[60073:Middle Position]]
app:11252020-06-18 11:03:18.079 debugscene: ID = 60073, name = Middle Position
app:11252020-06-18 11:03:18.075 debugjson: [sceneIds:[60073], sceneData:[[iconId:95, networkNumber:27, colorId:14, hkAssist:false, name:TWlkZGxlIFBvc2l0aW9u, id:60073, roomId:17973, order:0]]]
app:11252020-06-18 11:03:18.073 debugEntered scenesCallback()...
app:11252020-06-18 11:03:18.053 debugupdateDeviceData: atomicState.deviceData: [rooms:[17973:Kitchen, 45453:Default Room], shades:[50062:Blind 4, 51264:Right Rear, 65359:Blind 3, 23777:Bifold Doors]]
app:11252020-06-18 11:03:18.050 debugshade: ID = 65359, name = Blind 3
app:11252020-06-18 11:03:18.048 debugshade: ID = 50062, name = Blind 4
app:11252020-06-18 11:03:18.047 debugshade: ID = 23777, name = Bifold Doors
app:11252020-06-18 11:03:18.046 debugshade: ID = 51264, name = Right Rear
app:11252020-06-18 11:03:18.043 debugjson: [shadeIds:[51264, 23777, 50062, 65359], shadeData:[[capabilities:3, batteryStrength:160, batteryStatus:3, signalStrength:4, groupId:45539, name:UmlnaHQgUmVhcg==, positions:[position1:0, position2:12272, posKind2:3, posKind1:1], id:51264, type:54, firmware:[subRevision:8, build:1944, revision:1], roomId:17973], [capabilities:3, batteryStrength:184, batteryStatus:3, signalStrength:4, groupId:45539, name:Qmlmb2xkIERvb3Jz, positions:[position1:0, position2:32767, posKind2:3, posKind1:1], id:23777, type:54, firmware:[subRevision:8, build:1944, revision:1], roomId:17973], [capabilities:0, batteryStrength:162, batteryStatus:3, groupId:12043, name:QmxpbmQgNA==, id:50062, type:4, firmware:[subRevision:8, build:1944, revision:1], roomId:17973], [capabilities:0, batteryStrength:171, batteryStatus:3, groupId:12043, name:QmxpbmQgMw==, positions:[position1:65535, posKind1:1], id:65359, type:4, firmware:[subRevision:8, build:1944, revision:1], roomId:17973]]]
app:11252020-06-18 11:03:18.036 debugEntered shadesCallback()...
app:11252020-06-18 11:03:18.034 debugupdateDeviceData: atomicState.deviceData: [rooms:[17973:Kitchen, 45453:Default Room]]
app:11252020-06-18 11:03:18.020 debugroom: ID = 17973, name = Kitchen
app:11252020-06-18 11:03:18.018 debugroom: ID = 45453, name = Default Room
app:11252020-06-18 11:03:18.011 debugjson: [roomIds:[45453, 17973], roomData:[[iconId:168, colorId:15, name:RGVmYXVsdCBSb29t, id:45453, type:2, order:0], [iconId:16, colorId:3, name:S2l0Y2hlbg==, id:17973, type:0, order:1]]]
app:11252020-06-18 11:03:18.007 debugEntered roomsCallback()...
app:11252020-06-18 11:03:17.905 debugatomicState?.deviceData = null
app:11252020-06-18 11:03:17.900 debugSending HubAction: GET /api/repeaters HTTP/1.1
Accept: /
User-Agent: Linux UPnP/1.0 Hubitat
HOST: xxx.xxx.xx.187:80
Content-Type: application/json
Content-Length: 0
app:11252020-06-18 11:03:17.898 debugcallPowerView: url = 'http://xxx.xxx.xx.187:80/api/repeaters', method = 'GET', body = 'null', query = null, callback = repeatersCallback
app:11252020-06-18 11:03:17.893 debugSending HubAction: GET /api/scenes HTTP/1.1
Accept: /
User-Agent: Linux UPnP/1.0 Hubitat
HOST: xxx.xxx.xx.187:80
Content-Type: application/json
Content-Length: 0
app:11252020-06-18 11:03:17.883 debugcallPowerView: url = 'http://xxx.xxx.xx.187:80/api/scenes', method = 'GET', body = 'null', query = null, callback = scenesCallback
app:11252020-06-18 11:03:17.877 debugSending HubAction: GET /api/shades HTTP/1.1
Accept: /
User-Agent: Linux UPnP/1.0 Hubitat
HOST: xxx.xxx.xx.187:80
Content-Type: application/json
Content-Length: 0
app:11252020-06-18 11:03:17.875 debugcallPowerView: url = 'http://xxx.xxx.xx.187:80/api/shades', method = 'GET', body = 'null', query = null, callback = shadesCallback
app:11252020-06-18 11:03:17.872 debugSending HubAction: GET /api/rooms HTTP/1.1
Accept: /
User-Agent: Linux UPnP/1.0 Hubitat
HOST: xxx.xxx.xx.187:80
Content-Type: application/json
Content-Length: 0
app:11252020-06-18 11:03:17.843 debugcallPowerView: url = 'http://xxx.xxx.xx.187:80/api/rooms', method = 'GET', body = 'null', query = null, callback = roomsCallback
app:11252020-06-18 11:03:17.831 debugatomicState?.loadingDevices = false

have i done something wrong? i have a V2 hub by the way

{"firmware":{"mainProcessor":{"name":"PV Hub2.0","revision":2,"subRevision":0,"build":1024},"radio":{"revision":2,"subRevision":0,"build":2610}}}

Cheers
Edd

Hi Edd, I'm not the creator of this app, but I have spent some time with the logging lately. From the snippet you sent, it looks like you're not getting into the repeatersCallback function. That's the problem that I was having and I assumed it was just a v1 vs v2 hub issue. What do you get if you hit this URL from a browser?

http://[powerview hub ip]/api/repeaters

Hi,
I Get the following

{"repeaterIds":[],"repeaterData":[]}

Thanks for checking that. I guess the good news is that you get a response at least. The obvious next question is "do you have any repeaters in your Hunter Douglas set up?" If you don't have any repeaters that would explain what you're getting back. In any case, the problem is that you're getting an empty array back, thus the repeaters list is never getting filled and the main setup loop runs until it has filled in at least one shade, one scene, one room, and one repeater (see line 85 of hunter-douglas-powerview.groovy). If you look at the code that I sent in this thread yesterday and search for "gFirmware", you can see the places that I addressed not having the repeaters api in the Hunter Douglas hub v1. It would be a complete hack, but you could change all instances of "gFirmware.revision > 1" to "gFirmware.revision > 2". You'll lose the ability to know about any repeaters, but it should get you going for now.

i do not have any repeaters in my set up just 4 blinds two of which have an issue that the installer is working with luxaflex on and the hub.

Thanks for your help i'll try to play with the code tomorrow

Thanks for the information. I'll need to modify the code to account for the fact that there may not be rooms, scenes or repeaters.

Brilliant, if you need me to test anything then let me know. i'm running @losomode s code now which while it didn't complete the discovery has added the blinds and i have control. sort of......

cheers

Alright, I made the code changes to the PowerView app that will allow device discovery to complete even if shades, scenes, repeaters or rooms don't exist. I added error handling in the device discovery callbacks to make sure the HTTP status is 200. If it isn't then an error will be logged with all the response information. Finally I added code to get the firmware version and display it on the main page.

I tested everything the best I could considering I have shades, scenes, repeaters and rooms configured in my PowerView hub.

The code has been checked into my Github. I also updated the packageManifest so if you used the Package Manager to install the package you'll be able to update it from there.

Let me know if you run into any problems.

Just to let you know, I have removed everything and reinstalled it using the new code and so far it has worked. I'm waiting for some issues to be ironed out by the installer and i should be up and running in a few weeks. so I'll let you know how I get on then.

thanks for the quick work with the code.

One other thing, I noticed in the device driver for shades some ToDo comments for shade types I have vertical blinds which appear as shadeType 54. let me know if there is any way I can help with some info if you want, I'm no coder but I can cut and paste :smile:

I'm working on the shade driver to incorporate the various shade types but using the "capabilities" value that's being returned by the hub and not the "shadeType" value. This was based information that I received from a developer at Hunter Douglas. Apparently the shadeType value is difficult to work with because of the various shade types they have added and removed over the years so they introduced the "capabilities" value which groups the shade types together according to their capabilities (top-down, top-down bottom-up, etc).

Can you give me the output from this URL so I can verify the capability value of your vertical blinds:

http://[powerview hub ip]/api/shades

Here's the output from the Url. bear in mind that all 4 of my blinds are vertical but 2 of them are reporting to the hub as "roman" (shadeType : 4) at the moment (this is what the installer is trying to sort out)

{"shadeIds":[51264,23777,50062,65359],"shadeData":[{"id":51264,"type":54,"capabilities":3,"batteryStatus":0,"batteryStrength":0,"roomId":17973,"firmware":{"revision":1,"subRevision":8,"build":1944},"name":"UmlnaHQgUmVhcg==","groupId":45539,"positions":{"posKind2":3,"position2":12272,"posKind1":1,"position1":0},"signalStrength":4},{"id":23777,"type":54,"capabilities":3,"batteryStatus":0,"batteryStrength":0,"roomId":17973,"firmware":{"revision":1,"subRevision":8,"build":1944},"name":"Qmlmb2xkIERvb3Jz","groupId":45539,"positions":{"posKind1":1,"position1":65495,"posKind2":3,"position2":58360},"signalStrength":4},{"id":50062,"type":4,"capabilities":0,"batteryStatus":0,"batteryStrength":0,"roomId":17973,"firmware":{"revision":1,"subRevision":8,"build":1944},"name":"QmxpbmQgNA==","groupId":12043,"positions":{"posKind1":3,"position1":200},"motor":{"revision":50,"subRevision":52,"build":11825}},{"id":65359,"type":4,"capabilities":0,"batteryStatus":0,"batteryStrength":0,"roomId":17973,"firmware":{"revision":1,"subRevision":8,"build":1944},"name":"QmxpbmQgMw==","positions":{"posKind1":1,"position1":65534},"groupId":12043,"motor":{"revision":50,"subRevision":52,"build":11825}}]}

Thanks for the data.

Hello - Can you point me to the API for hunter douglas shades for Hubitat? thanks so much! Anthony