Home Theater Direct (HTD) and OpenHAB

  • Platform information:
    • Hardware: x86_64/Intel NUC/8GB HTD GW-SL1 Smart Gateway
    • OS: Ubuntu 18.04LTS
    • Java Runtime Environment: 1.8.0_231
    • openHAB version: 2.4.0
  • Issue of the topic: How to get HTD working with OpenHAB

Here’s how to get the Home Theater Direct (HTD) whole-house Lync system to play with OpenHAB 2.4, and make the whole thing work with Google Home. The trick is to create an OpenHAB string item and bind it to a tcp channel to the HTD Gateway (GW-SL1) using the tcp binding.

First: how to get the connection between the gateway and OpenHAB working.

  • Install the tcp binding through the PaperUI or whatever means necessary
  • in /etc/openhab2/services/tcp.cfg, you need to change the character set to Latin1. Everything else should stay default. So the last line in tcp.cfg should read:

Now you need an items file that abuses a String item as a conduit to send messages to the HTD gateway. Replace the 192.168.x.x with whatever the IP address of your HTD GW-SL1 is:

String htd_link "link to HTD" { tcp=">[*:192.168.x.x:10006:]" }

Switch kitchen_audio_chromecast_audio "kitchen music" [ "Switchable" ]
Switch kitchen_audio_home_automation "kitchen home automation" [ "Switchable" ]
Dimmer kitchen_audio_volume "kitchen volume" [ "Lighting" ]

The purpose of the other items will become clearer later: they are switches and dimmers that can be manipulated from Google Home via myopenhab/cloud. You can later say for instance “switch on kitchen music” and it will trigger the HTD system to assign the source “music” to the zone “kitchen”. If you say “switch on kitchen home automation” it will assign the the source “home_automation” (the voice of OpenHAB) to the zone “kitchen”. And if you say “set kitchen volume to 100%” it will set the HTD volume for the kitchen zone all the way up.

Rather than having a proper binding, this is implemented as a set of rules. Here is the corresponding Jython code:


from org.slf4j import LoggerFactory
import time

log = LoggerFactory.getLogger("org.eclipse.smarthome.automation.htd");

# Maps the zone names used in openhab to HTD zone numbers.                                                                                                                                                         
# The names here do not have to  match what you have actually                                                                                                                                                      
# configured in the HTD gateway, but *must* match with your                                                                                                                                                        
# item naming scheme.                                                                                                                                                                                              

zone_to_number ={'kitchen':        1,
                 'music':          2,
                 'east_bath':      3,
                 'south_bedroom':  4,
                 'computer_room':  5,
                 'north_bedroom':  6,
                 'east_bedroom':   7,
                 'master_bath':    8,
                 'master_bedroom': 9,
                 'pingpong_room': 10,
                 'guest_room':    11,
                 'pool_patio':    12 }

# Maps the source names used in openhab to HTD src numbers.                                                                                                                                                        
# The source names here do not have to                                                                                                                                                                             
# match what you have actually configured in the HTD gateway,                                                                                                                                                      
# but *must* match with your item naming scheme                                                                                                                                                                    

# NOTE: one of the zones must be called 'silent', and                                                                                                                                                              
# refer to a HTD zone that has no audio input                                                                                                                                                                      

source_to_number = { 'kitchen_panel':     1,
                     'source_2':          2,
                     'source_3':          3,
                     'source_4':          4,
                     'source_5':          5,
                     'source_6':          6,
                     'source_7':          7,
                     'source_8':          8,
                     'source_9':          9,
                     'source_10':        10,
                     'silent':           11,
                     'source_12':        12,
                     'source_13':        13,
                     'source_14':        14,
                     'chromecast_audio': 15,
                     'source_16':        16,
                     'home_automation':  17,
                     'source_18':        18,
                     'intercom':         19}

# helper array for mapping volume to actual volume codes                                                                                                                                                           
# use table here:                                                                                                                                                                                                  
# https://www.universal-devices.com/networkresources/audio/HTD_Binary_Codes.txt                                                                                                                                    

volume_to_code = [0xCEE6,  # volume  0                                                                                                                                                                             
                  0xCEE6,  # volume  5                                                                                                                                                                             
                  0xCEE6,  # volume 10                                                                                                                                                                             
                  0xD8F0,  # volume 15                                                                                                                                                                             
                  0xD8F0,  # volume 20                                                                                                                                                                             
                  0xDDF5,  # volume 25                                                                                                                                                                             
                  0xE2FA,  # volume 30                                                                                                                                                                             
                  0xE7FF,  # volume 35                                                                                                                                                                             
                  0xEC04,  # volume 40                                                                                                                                                                             
                  0xF109,  # volume 45                                                                                                                                                                             
                  0xF60E]  # volume 50                                                                                                                                                                             

# rule that handles state changes to the "Switch" openhab items that                                                                                                                                               
# switch sources to zones. The items must follow an naming scheme, for zone                                                                                                                                        
# foozone to switch to barsrc, the corresponding item must be named                                                                                                                                                
#      foozone_audio_barsrc                                                                                                                                                                                        
# There must also be defined a string item "htd_link" that represents                                                                                                                                              
# the tcp channel to the HTD gateway                                                                                                                                                                               

class OnOffRule(SimpleRule):
    def __init__(self, zone, source):
        self.zone = zone
        self.source = source
        itemName = zone + "_audio_" + source
        self.triggers = [
                    .withId("Command_" + itemName)
                            "itemName": itemName
        if not source_to_number.has_key(source):
            raise Exception('bad source: {}!'.format(source))
        src = source_to_number[source]
        if not zone_to_number.has_key(zone):
            raise Exception('bad zone: {}!'.format(zone))
        zn = zone_to_number[zone]
        self.cmd = self.to_cmd(src, zn)
        self.silence_cmd = self.to_cmd(
            source_to_number['silent'], zn)

    def to_cmd(self, src, zn):
        # see above for link to code table                                                                                                                                                                         
        cmd = "0200%02x04%02x%02x" % (
            zn, (0x0F + src) if src < 13 else (0x63 + (src - 13)),
            zn - 1 + ((0x16 + src) if src < 13 else (0x6A + (src - 13))))
        return cmd
    def execute(self, module, input):
        if input["state"] == ON:
            events.sendCommand("htd_link", self.cmd.decode("hex"))
            log.info('setting zone {} to src {} with cmd: {}'.format(
                self.zone, self.source, self.cmd))
            events.sendCommand("htd_link", self.silence_cmd.decode("hex"))
            log.info('setting zone {} to silent with cmd: {}'.format(
                self.zone, self.silence_cmd))

# rule that handles volume updates. The openhab item for zone                                                                                                                                                      
# "foo" must be called foo_audio_volume                                                                                                                                                                            

class VolumeRule(SimpleRule):
    def __init__(self, zone):
        self.zone = zone
        itemName = zone + "_audio_volume"
        self.triggers = [
                    .withId("Command_" + itemName)
                            "itemName": itemName
        if not zone_to_number.has_key(zone):
            raise Exception('bad zone: {}!'.format(zone))
        self.zn = zone_to_number[zone]
        self.cmd_base = "0200%02x15" % self.zn

    def to_vol_cmd(self, volume):
        # volume is assumed to be in range of 0...100                                                                                                                                                              
        idx = max(min(int(volume // 10), 10), 0) #  index into table                                                                                                                                               
        code = volume_to_code[idx]
        vc = self.cmd_base + "%02x%02x" % (
            (code & 0xFF00) >> 8,  # take code from table                                                                                                                                                          
            ((code & 0xFF) + (self.zn - 1)) & 0xFF)  # need to add zone + wrap                                                                                                                                     
        return vc

    def execute(self, module, input):
        if type(input["state"]) is PercentType:
            v = input["state"].intValue()
            cmd = self.to_vol_cmd(v)
            log.info('setting zone {} to volume {} with cmd: {}'.format(self.zone, v, cmd))
            events.sendCommand("htd_link", cmd.decode("hex"))
            log.info('zone {} ignored invalid volume: {}'.format(self.zone, input["state"]))

# register the rules for all sources and zones that we want to                                                                                                                                                     
# connect with each other                                                                                                                                                                                          
zones_used = ['kitchen']
sources_used = ['chromecast_audio', 'home_automation']

for zone in zones_used:
    # create a separate rule for each zone and source                                                                                                                                                              
    for s in sources_used:
        automationManager.addRule(OnOffRule(zone, s))
    # create one volume rule per zone                                                                                                                                                                              

Note that you must have one zone called ‘silent’ that is mapped to an idle input source. That way you can say “switch of kitchen music” and it will reassign the HTD source from “music” to “silent”.

Now just a few sitemap entries for testing:

sitemap my_house label="Nolen" {
    Switch item=kitchen_audio_chromecast_audio label="kitchen audio chromecast"
    Switch item=kitchen_audio_home_automation label="kitchen audio home automation"
    Default item=kitchen_audio_volume label="kitchen audio volume"

Note that for the rules to work, the items must follow the naming convention: ZONE_audio_SOURCE for the switches, and ZONE_audio_volume for the dimmers. Your ZONE and SOURCE convention must match and be present in the dictionaries zone_to_number and source_to_number in the jython code.


Hello there! Thank you for posting this. I’ve started a project to add HTD Lync to my server. I am having trouble with it though. It seems to be running through the logic correctly - I’m getting the events logged and those look accurate. I’m just not seeing anything change with the system. I do have the correct IP set for the htd_link item. What I’m struggling with is how to verify it’s actually reaching the gateway. Wondering if you had a technique you used while working on this project.

I don’t recall having trouble with the tcp connection, but for debugging that part, I suggest using netstat -a to check that a connection has been established, and tcpdump to see if data is flowing between host and htd gateway.

I got this to work. Turns out the script/rule wasn’t running because I didn’t realize jython wasn’t installed. I’m running openhabian on a rp4. After installing jython, works like a charm!