Plugins/Soundtouch
Bose soundtouch devices and be controlled directly using the network connection. For this, a python library is available called libsoundtouch (https://github.com/CharlesBlonde/libsoundtouch)
This plugin uses this library to control the device.
Installation
For this plugin, you will need:
- Libsoundtouch library - if you use "pip install libsoundtouch", the 2.7 python module will be installed. As Domoticz runs under python 3, this will not work. Use "python3 -m pip install libsoundtouch"
- - zeroconf library - if not installed, use "python3 -m pip install zeroconf"
- - websocket library - if not installed, use "python3 -m pip install websocket"
- install the plugin (see section plugin)
Usage
After installation of the plugin, a hardware device called "Soundtouch plugin" should be available in domoticz. There are the following parameters:
Parameter | Value | Purpose |
---|---|---|
Soundtouch host | IP/DNS name | The IP or DNS name of the soundtouch device |
Interval | seconds | Used to query the device periodically to update the volume/station in domiticz. Note that this is only needed if the device is controlled directly |
Automatic presets | True/False | On startup if this option is True, the presets are read from the device and the control device is updated with preset station names. Disable if you want to name it manually. |
Debug | True/False | Default, false. If set to true, a logfile is generated in the plugin folder and the domoticz log is written to with additional information |
There are two devices added on startup:
Control - Used to change the preset
Volume - Control the volume.
If the interval is set, any change on the device will be reflected in these controls periodically.
Plugin
Create a folder in the domoticz plugin folder, Soundtouch. Eg. /home/pi/domoticz/plugins/Soundtouch. Create a file, python.py in this folder with this content:
# Soundtouch plugin
#
# Author: Gerrit Hulleman
#
"""
<plugin key="SoundTouch" name="Soundtouch plugin" author="Gerrit Hulleman" version="1.0.1" wikilink="http://www.domoticz.com/wiki/plugins/plugin.html" externallink="">
<description>
<h2>Soundtouch plugin</h2><br/>
Uses the libsoundtouch (https://github.com/CharlesBlonde/libsoundtouch) to communicate with bose soundtouch devices.
<h3>Features</h3>
<ul style="list-style-type:square">
<li>Turn off / switch preset stations</li>
<li>Control audio</li>
</ul>
<h3>Configuration</h3>
Soundtouch host: the IP/DNS name of the soundtouch device
Interval: active reread of device status. Note: only needed if the device op operated directly
Automatic presets: use preset in device to update control.
Debug: additional debug information to domoticz log and logfile.
</description>
<params>
<param field="Address" label="Soundtouch host" width="200px" required="true" default="192.168.178.24"/>
<param field="Mode1" label="Interval" width="200px" required="true" default="10"/>
<param field="Mode2" label="Automatic presets" width="75px">
<options>
<option label="True" value="1" default="true"/>
<option label="False" value="0" />
</options>
</param>
<param field="Mode6" label="Debug" width="75px">
<options>
<option label="True" value="Debug"/>
<option label="False" value="Normal" default="true" />
</options>
</param>
</params>
</plugin>
"""
import Domoticz
import sys
sys.path.append('/home/pi/.local/lib/python3.7/site-packages/')
from libsoundtouch import soundtouch_device
from libsoundtouch.utils import Source, Type
class BasePlugin:
enabled = False
debugMode = False
soundTouchHost = ''
deviceName = ''
heartbeat = 10
automaticPreset = False
presetMap = {} #Contains location / preset id on startup
def __init__(self):
return
def onStart(self):
self.logMessage("Loading parameters")
self.soundTouchHost = Parameters["Address"]
self.heartbeat = Parameters["Mode1"]
if Parameters["Mode2"] != "0":
self.automaticPreset = True
if Parameters["Mode6"] != "Debug":
Domoticz.Debugging(0)
self.debugMode = False
else:
self.debugMode = True
if (self.heartbeat != 0):
Domoticz.Heartbeat(int(self.heartbeat))
if (len(Devices) == 0):
# devices not created, possible first run or deleted.
Options = {"LevelActions": "||||",
"LevelNames": "Off|Preset 1|Preset 2|Preset 3|Preset 4",
"LevelOffHidden": "false",
"SelectorStyle": "1"}
Domoticz.Device(Name="control", Unit=1, TypeName="Selector Switch", Options=Options, Image=8).Create() # Image 8 = speaker icon
Domoticz.Device(Name="volume", Unit=2, TypeName="Switch", Switchtype=7, Image=8).Create() # Switchtype 7 = dimmer, Image 8 = speaker icon
self.logMessage("Soundtouch devices created");
extDevice = soundtouch_device(self.soundTouchHost)
presets = extDevice.presets()
presetOptions = "Off"
presetActions = ""
for preset in presets:
presetOptions += '|'
presetActions += '|'
presetOptions += preset.name
# save location. If the channel is change on the device, easier lookup on heartbeat
presetIdSwitch = int(preset.preset_id)*10; # 1 = 10, 2 = 20 etc.
self.presetMap[preset.location] = presetIdSwitch
self.logDebug("Channel: "+preset.name+ " id: "+str(preset.preset_id)+ " selector: "+str(presetIdSwitch))
if (self.automaticPreset):
self.logMessage("Preset update - auto : " + presetOptions)
Options = {"LevelActions": presetActions,
"LevelNames": presetOptions,
"LevelOffHidden": "false" }
# nValue/sValue is mandatory.
Devices[1].Update(nValue=Devices[1].nValue, sValue=Devices[1].sValue, Options=Options)
self.logMessage("Plugin operational")
def onStop(self):
self.logDebug("onStop called")
def onConnect(self, Connection, Status, Description):
self.logDebug("onConnect called")
def onMessage(self, Connection, Data):
self.logDebug("onMessage called")
def onCommand(self, Unit, Command, Level, Hue):
self.logDebug("onCommand called for Unit " + str(Unit) + ": Command '" + str(Command) + "', Level: " + str(Level))
#(Bose 20) onCommand called for Unit 1: Parameter 'Set Level', Level: 10
if (Unit == 1):
# control device
if (Command == "Set Level"):
extDevice = soundtouch_device(self.soundTouchHost)
if Level == 0:
extDevice.power_off()
presetId = int((Level / 10) - 1)
if (presetId >= 0
and presetId <= 6):
presets = extDevice.presets()
extDevice.select_preset(presets[presetId])
Devices[1].Update(2, str(Level)) # switch is not updated from domoticz itself.
if (Command == "Off"):
extDevice = soundtouch_device(self.soundTouchHost)
extDevice.power_off()
Devices[1].Update(2, str(Level)) # switch is not updated from domoticz itself.
#(Bose 20) onCommand called for Unit 2: Command 'Set Level', Level: 17
if (Unit == 2):
# volume control
extDevice = soundtouch_device(self.soundTouchHost)
if (Command == "Off"):
extDevice.set_volume(0)
Devices[2].Update(0, str(Level)) # switch is not updated from domoticz itself.
else:
extDevice.set_volume(Level)
Devices[2].Update(2, str(Level)) # switch is not updated from domoticz itself.
def onNotification(self, Name, Subject, Text, Status, Priority, Sound, ImageFile):
self.logDebug("Notification: " + Name + "," + Subject + "," + Text + "," + Status + "," + str(Priority) + "," + Sound + "," + ImageFile)
def onDisconnect(self, Connection):
self.logDebug("Disconnect")
return
def onHeartbeat(self):
self.logDebug("onHeartbeat called")
extDevice = soundtouch_device(self.soundTouchHost)
try:
# check/update station
status = extDevice.status()
# print(status.source) # TUNEIN, STANDBY, BLUETOOTH, AUX
if (status.source == "STANDBY"):
self.logDebug("on standby")
Devices[1].Update(nValue = 0, sValue = "00")
self.logDebug("Device updated")
else:
location = status.content_item.location
if (location in self.presetMap):
# found -> update device to reflect channel
#self.logDebug("Found channel currently on " + str(self.presetMap[location]))
Devices[1].Update(2, sValue = str(self.presetMap[location]))
# check/update volume
actualVolume = extDevice.volume().actual
self.logDebug("Set volume: " + str(actualVolume))
Devices[2].Update(2, str(actualVolume))
self.logDebug("Set volume done")
except:
self.logMessage("Unexpected error:" + sys.exc_info()[0])
self.logDebug("Heartbeat done, return")
def logMessage(self, Message):
if self.debugMode:
f= open("plugins/Soundtouch/log.txt","a+")
f.write(Message+'\r\n')
Domoticz.Log(Message)
def logDebug(self, Message):
if self.debugMode:
self.logMessage(Message)
global _plugin
_plugin = BasePlugin()
def onStart():
global _plugin
_plugin.onStart()
def onStop():
global _plugin
_plugin.onStop()
def onConnect(Connection, Status, Description):
global _plugin
_plugin.onConnect(Connection, Status, Description)
def onMessage(Connection, Data):
global _plugin
_plugin.onMessage(Connection, Data)
def onCommand(Unit, Command, Level, Hue):
global _plugin
_plugin.onCommand(Unit, Command, Level, Hue)
def onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile):
global _plugin
_plugin.onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile)
def onDisconnect(Connection):
global _plugin
_plugin.onDisconnect(Connection)
def onHeartbeat():
global _plugin
_plugin.onHeartbeat()
# Generic helper functions
def DumpConfigToLog():
for x in Parameters:
if Parameters[x] != "":
Domoticz.Log( "'" + x + "':'" + str(Parameters[x]) + "'")
Domoticz.Log("Device count: " + str(len(Devices)))
for x in Devices:
Domoticz.Log("Device: " + str(x) + " - " + str(Devices[x]))
Domoticz.Log("Device ID: '" + str(Devices[x].ID) + "'")
Domoticz.Log("Device UnitID: '" + str(Devices[x].Unit) + "'")
Domoticz.Log("Device DeviceID: '" + str(Devices[x].DeviceID) + "'")
Domoticz.Log("Device Name: '" + Devices[x].Name + "'")
Domoticz.Log("Device nValue: " + str(Devices[x].nValue))
Domoticz.Log("Device sValue: '" + Devices[x].sValue + "'")
Domoticz.Log("Device LastLevel: " + str(Devices[x].LastLevel))
return
Errors
Invalid syntax
File "bose.py", line 7, in <module> from libsoundtouch import soundtouch_device File "/usr/local/lib/python2.7/dist-packages/libsoundtouch/__init__.py", line 9, in <module> from zeroconf import Zeroconf, ServiceBrowser File "/usr/local/lib/python2.7/dist-packages/zeroconf.py", line 175 def current_time_millis() -> float: ^
SyntaxError: invalid syntax
For those who got this error: check the python version (terminal, python -V). If version 2.7.x, zeroconf might be a high version (terminal, pip show zeroconf). Version 0.20 and higher does not support python 2.7. If you want to use python 2.7, uninstall zerconf (pip uninstall zeroconf) and install version 0.19.1 (pip install zeroconf==0.19.1)
Error: ...... hardware (##) thread seems to have ended unexpectedly
Unknown. The heartbeat is finished with updating the controls and without error according to the log (added in v1.0.1). But still Domoticz reports this error.