Skip to content

Commit

Permalink
[ 1.0.5 ] - 2023/11/01
Browse files Browse the repository at this point in the history
  *  Updated code to handle device websocket connection errors (e.g. power loss, socket connection errors, etc).  This was causing devices to not respond once the websocket connection was re-established.
  • Loading branch information
thlucas1 committed Nov 2, 2023
1 parent c103636 commit c6349f2
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 30 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ config/*
# Local project files to ignore.
/custom_components/soundtouchplus/developer_notes.txt
/.github/workflows/lint.yml
/New_Release_Instructions.txt
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
## Change Log

All notable changes to this project are listed here.
All notable changes to this project are listed here.

Change are listed in reverse chronological order (newest to oldest).
Change are listed in reverse chronological order (newest to oldest).

<span class="changelog">

###### [ 1.0.5 ] - 2023/11/01

* Updated code to handle device websocket connection errors (e.g. power loss, socket connection errors, etc). This was causing devices to not respond once the websocket connection was re-established.

###### [ 1.0.4 ] - 2023/10/31

* Updated code to handle devices that do not support websocket notifications. In this case, HA will poll the device every 10 seconds for status updates.
Expand Down
4 changes: 2 additions & 2 deletions custom_components/soundtouchplus/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"websocket-client>=1.6.4",
"urllib3",
"smartinspectPython>=3.0.27",
"bosesoundtouchapi>=1.0.3"
"bosesoundtouchapi>=1.0.4"
],
"version": "1.0.4",
"version": "1.0.5",
"zeroconf": [ "_soundtouch._tcp.local." ]
}
98 changes: 73 additions & 25 deletions custom_components/soundtouchplus/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ class SoundTouchMediaPlayer(MediaPlayerEntity):
"""
Representation of a Bose SoundTouch device.
"""
# we will (by default) set polling to false, as the SoundTouch device should be
# sending us updates as they happen if it supports websockets. if not, then we
# will reset this flag in the __init__ method.
should_poll = False

def __init__(self, initParms:EntityInitParms) -> None:
"""
Expand Down Expand Up @@ -109,15 +105,37 @@ def __init__(self, initParms:EntityInitParms) -> None:
# entity screens, and used to build the Entity ID that's used in automations etc.
self._attr_name = self._client.Device.DeviceName

# we will (by default) set polling to false, as the SoundTouch device should be
# sending us updates as they happen if it supports websocket notificationss.
# if not, then we will reset this flag in the __init__ method.
self._attr_should_poll = False

# if websockets are not supported, then we need to enable device polling.
if self._socket is None:
_logsi.LogVerbose("'%s': should_poll is being enabled, as the device does not support websockets" % (self.name))
self.should_poll = True
_logsi.LogVerbose("'%s': _attr_should_poll is being enabled, as the device does not support websockets" % (self.name))
self._attr_should_poll = True

_logsi.LogObject(SILevel.Verbose, "'%s': initialized" % (self.name), self._client)
return


# @property
# def should_poll(self) -> bool:
# """Return True if entity has to be polled for state.

# False if entity pushes its state to HA.
# """
# return self._attr_should_poll

# @should_poll.setter
# def should_poll(self, value:bool):
# """
# Sets the _attr_should_poll property value.
# """
# if isinstance(value, bool):
# self._attr_should_poll = value


async def async_added_to_hass(self) -> None:
"""
Run when this Entity has been added to HA.
Expand Down Expand Up @@ -150,7 +168,8 @@ async def async_added_to_hass(self) -> None:
self._socket.AddListener(SoundTouchNotifyCategorys.SoundTouchSdkInfo, self._OnSoundTouchInfoEvent)

# add our listener that will handle SoundTouch websocket related events.
self._socket.AddListener(SoundTouchNotifyCategorys.WebSocketClose, self._OnSoundTouchWebSocketCloseEvent)
self._socket.AddListener(SoundTouchNotifyCategorys.WebSocketClose, self._OnSoundTouchWebSocketConnectionEvent)
self._socket.AddListener(SoundTouchNotifyCategorys.WebSocketOpen, self._OnSoundTouchWebSocketConnectionEvent)
self._socket.AddListener(SoundTouchNotifyCategorys.WebSocketError, self._OnSoundTouchWebSocketErrorEvent)

# start receiving device event notifications.
Expand Down Expand Up @@ -384,15 +403,6 @@ def mute_volume(self, mute:bool) -> None:
""" Send mute command. """
self._client.Mute()

# if device notification events thread is stopped, then restart it.
# this can happen if the SoundTouch device loses power and drops the connection.
# we could probably check for this somewhere else to have it restart automatically,
# but let's start with here.
if self._socket is not None:
if self._socket.IsThreadRunForeverActive == False:
self._socket.StopNotification()
self._socket.StartNotification()


def set_repeat(self, repeat:RepeatMode) -> None:
""" Set repeat mode. """
Expand Down Expand Up @@ -429,11 +439,36 @@ def turn_on(self) -> None:

def update(self) -> None:
""" Retrieve the latest data. """
_logsi.LogVerbose("'%s': update method (should_poll=%s)" % (self.name, self.should_poll))
self._nowPlayingStatus = self._client.GetNowPlayingStatus(self.should_poll)
self._volume = self._client.GetVolume(self.should_poll)
self._zone = self._client.GetZoneStatus(self.should_poll)

_logsi.LogVerbose("'%s': update method (_attr_should_poll=%s)" % (self.name, self._attr_should_poll))

# get updated device status.
_logsi.LogVerbose("'%s': update method - getting nowPlaying status" % (self.name))
self._nowPlayingStatus = self._client.GetNowPlayingStatus(self._attr_should_poll)
_logsi.LogVerbose("'%s': update method - getting volume status" % (self.name))
self._volume = self._client.GetVolume(self._attr_should_poll)
_logsi.LogVerbose("'%s': update method - getting zone status" % (self.name))
self._zone = self._client.GetZoneStatus(self._attr_should_poll)

# does this device support websocket notifications?
# note - if socket is None, it denotes that websocket notifications are
# NOT supported for the device, and we should not try to restart.
if self._socket is not None:

# is polling enabled? if so it should NOT be since websockets are supported.
# this denotes that a websocket error previously occured which broke the connection.
# this can happen if the SoundTouch device loses power and drops the connection.
if self._attr_should_poll == True:

_logsi.LogVerbose("'%s': update method - checking _socket.IsThreadRunForeverActive status" % (self.name))

# if device notification events thread is stopped, then restart it if possible.
if self._socket.IsThreadRunForeverActive == False:
_logsi.LogVerbose("'%s': update is re-starting websocket notifications" % (self.name))
self._socket.StopNotification()
self._socket.StartNotification()
_logsi.LogVerbose("'%s': update is setting _attr_should_poll=False since event notifications are active again" % (self.name))
self._attr_should_poll = False


def volume_down(self) -> None:
""" Volume down media player. """
Expand Down Expand Up @@ -606,16 +641,28 @@ async def async_browse_media(
# -----------------------------------------------------------------------------------

@callback
def _OnSoundTouchWebSocketCloseEvent(self, client:SoundTouchClient, args) -> None:
def _OnSoundTouchWebSocketConnectionEvent(self, client:SoundTouchClient, args:str) -> None:
if (args != None):
_logsi.LogError("SoundTouch device websocket close event: %s" % (str(args)), colorValue=SIColors.LightGreen)
_logsi.LogError("SoundTouch device websocket connection event: %s" % (str(args)), colorValue=SIColors.Coral)


@callback
def _OnSoundTouchWebSocketErrorEvent(self, client:SoundTouchClient, ex:Exception) -> None:
if (ex != None):
_logsi.LogError("SoundTouch device websocket error event: %s" % (str(ex)), colorValue=SIColors.LightGreen)

_logsi.LogError("SoundTouch device websocket error event: (%s) %s" % (str(type(ex)), str(ex)), colorValue=SIColors.Coral)
_logsi.LogVerbose("'%s': Setting _attr_should_poll=True due to websocket error event" % (client.Device.DeviceName), colorValue=SIColors.Coral)
self._attr_should_poll = True

# at this point we will assume that the device lost power since it lost the websocket connection.
# reset nowPlayingStatus, which will drive a MediaPlayerState.OFF state.
_logsi.LogVerbose("'%s': Setting _nowPlayingStatus to None to simulate a MediaPlayerState.OFF state" % (client.Device.DeviceName), colorValue=SIColors.Coral)
self._nowPlayingStatus = None

# inform Home Assistant of the status update.
# this will turn the player off in the Home Assistant UI.
_logsi.LogVerbose("'%s': Calling async_write_ha_state to update player status" % (client.Device.DeviceName), colorValue=SIColors.Coral)
self.async_write_ha_state()


@callback
def _OnSoundTouchInfoEvent(self, client:SoundTouchClient, args:Element) -> None:
Expand Down Expand Up @@ -702,6 +749,7 @@ def _OnSoundTouchUpdateEvent_zoneUpdated(self, client:SoundTouchClient, args:Ele
# inform Home Assistant of the status update.
self.async_write_ha_state()


# -----------------------------------------------------------------------------------
# Helpfer functions
# -----------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ homeassistant==2023.8.0
pip>=21.0,<23.4
ruff==0.1.3
smartinspectPython>=3.0.27
# bosesoundtouchapi>=1.0.3
# bosesoundtouchapi>=1.0.4
1 change: 1 addition & 0 deletions soundtouchplus.pyproj
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
<Content Include="custom_components\soundtouchplus\translations\en.json" />
<Content Include="hacs.json" />
<Content Include="LICENSE" />
<Content Include="New_Release_Instructions.txt" />
<Content Include="README.md" />
<Content Include="requirements.txt" />
<Content Include="scripts\develop" />
Expand Down

0 comments on commit c6349f2

Please sign in to comment.