Plugins/Soundtouch

From Domoticz
Jump to navigation Jump to search

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:


Soundtouch plugin devices

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.