- 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:
charset=Latin1
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:
scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleSimple")
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 = [
TriggerBuilder.create()
.withId("Command_" + itemName)
.withTypeUID("core.ItemStateUpdateTrigger")
.withConfiguration(
Configuration({
"itemName": itemName
})).build()
]
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))
else:
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 = [
TriggerBuilder.create()
.withId("Command_" + itemName)
.withTypeUID("core.ItemStateUpdateTrigger")
.withConfiguration(
Configuration({
"itemName": itemName
})).build()
]
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"))
else:
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
automationManager.addRule(VolumeRule(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.