From 413fe5edcb645ed8721aa71672756d57c265d2ef Mon Sep 17 00:00:00 2001 From: mang1985 Date: Fri, 12 Apr 2024 19:32:25 +0800 Subject: [PATCH 1/3] Resolve the issue where the config flow speaker list isn't correctly filtered when users modify the player entity ID. --- .../ytube_music_player/config_flow.py | 14 +++++++++++--- .../ytube_music_player/media_player.py | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/custom_components/ytube_music_player/config_flow.py b/custom_components/ytube_music_player/config_flow.py index f2528fc..91d53bd 100644 --- a/custom_components/ytube_music_player/config_flow.py +++ b/custom_components/ytube_music_player/config_flow.py @@ -191,13 +191,21 @@ async def async_create_form(hass, user_input, page=1): data_schema[vol.Required(CONF_CODE+"TT", default="https://www.google.com/device?user_code="+user_input[CONF_CODE]["user_code"])] = str # name of the component without domain data_schema[vol.Required(CONF_NAME, default=user_input[CONF_NAME])] = str # name of the component without domain elif(page == 2): + # Generate a list of excluded entities. + # This method is more reliable because it won't become invalid + # if users modify entity IDs, and it supports multiple instances. + _exclude_entities = [] + for _ytm_player in hass.data[DOMAIN].values(): + _LOGGER.warning(_ytm_player[DOMAIN_MP].entity_id) + _exclude_entities.append(_ytm_player[DOMAIN_MP].entity_id) + data_schema[vol.Required(CONF_RECEIVERS,default=user_input[CONF_RECEIVERS])] = selector({ "entity": { "multiple": "true", "filter": [{"domain": DOMAIN_MP}], - "exclude_entities": [DOMAIN_MP+"."+user_input[CONF_NAME]] - } - }) + "exclude_entities": _exclude_entities + } + }) data_schema[vol.Required(CONF_API_LANGUAGE, default=user_input[CONF_API_LANGUAGE])] = selector({ "select": { "options": languages, diff --git a/custom_components/ytube_music_player/media_player.py b/custom_components/ytube_music_player/media_player.py index 4c93820..9e5a187 100644 --- a/custom_components/ytube_music_player/media_player.py +++ b/custom_components/ytube_music_player/media_player.py @@ -80,6 +80,7 @@ class yTubeMusicComponent(MediaPlayerEntity): def __init__(self, hass, config, name_add): self.hass = hass self._attr_unique_id = config.entry_id + self.hass.data[DOMAIN][self._attr_unique_id][DOMAIN_MP] = self self._debug_log_concat = "" self._debug_as_error = config.data.get(CONF_DEBUG_AS_ERROR, DEFAULT_DEBUG_AS_ERROR) self._org_name = config.data.get(CONF_NAME, DOMAIN + name_add) From 463a9777e5fbc541d0cb3fafa2fd087370be06e0 Mon Sep 17 00:00:00 2001 From: mang1985 Date: Fri, 12 Apr 2024 19:52:05 +0800 Subject: [PATCH 2/3] Fix issues with the option flow after upgrading the component. --- custom_components/ytube_music_player/const.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/ytube_music_player/const.py b/custom_components/ytube_music_player/const.py index dff2b1c..f8a973b 100644 --- a/custom_components/ytube_music_player/const.py +++ b/custom_components/ytube_music_player/const.py @@ -317,8 +317,15 @@ def ensure_config(user_input): out[CONF_MAX_DATARATE] = DEFAULT_MAX_DATARATE if user_input is not None: + # for the old shuffle_mode setting. out.update(user_input) - + if isinstance(_shuffle_mode := out[CONF_SHUFFLE_MODE], int): + if _shuffle_mode >= 1: + out[CONF_SHUFFLE_MODE] = ALL_SHUFFLE_MODES[_shuffle_mode - 1] + else: + out[CONF_SHUFFLE_MODE] = PLAYMODE_DIRECT + _LOGGER.error(f"shuffle_mode: {_shuffle_mode} is a deprecated value and has been replaced with '{out[CONF_SHUFFLE_MODE]}'.") + return out From 476e94096d4e0b116033cb9f3c52bddb28d1b774 Mon Sep 17 00:00:00 2001 From: mang1985 Date: Fri, 12 Apr 2024 20:00:31 +0800 Subject: [PATCH 3/3] Provide compatibility support for existing configurations using manually created input entities. --- .../ytube_music_player/config_flow.py | 13 +++- custom_components/ytube_music_player/const.py | 43 ++++++++++- .../ytube_music_player/media_player.py | 77 ++++++++++++++----- .../ytube_music_player/translations/en.json | 14 +++- .../translations/zh-Hans.json | 14 +++- 5 files changed, 132 insertions(+), 29 deletions(-) diff --git a/custom_components/ytube_music_player/config_flow.py b/custom_components/ytube_music_player/config_flow.py index 91d53bd..7c1ac4e 100644 --- a/custom_components/ytube_music_player/config_flow.py +++ b/custom_components/ytube_music_player/config_flow.py @@ -222,8 +222,8 @@ async def async_create_form(hass, user_input, page=1): "select": { "options": ALL_SHUFFLE_MODES, "mode": "dropdown" - } - }) + } + }) data_schema[vol.Optional(CONF_LIKE_IN_NAME, default=user_input[CONF_LIKE_IN_NAME])] = vol.Coerce(bool) # default like_in_name, TRUE/FALSE data_schema[vol.Optional(CONF_DEBUG_AS_ERROR, default=user_input[CONF_DEBUG_AS_ERROR])] = vol.Coerce(bool) # debug_as_error, TRUE/FALSE data_schema[vol.Optional(CONF_LEGACY_RADIO, default=user_input[CONF_LEGACY_RADIO])] = vol.Coerce(bool) # default radio generation typ @@ -233,8 +233,13 @@ async def async_create_form(hass, user_input, page=1): "select": { "options": ALL_DROPDOWNS, "multiple": "true" - } - }) + } + }) + # add for the old inputs. + for _old_conf_input in OLD_INPUTS.values(): + if user_input.get(_old_conf_input) is not None: + data_schema[vol.Optional(_old_conf_input, default=user_input[_old_conf_input])] = str + data_schema[vol.Optional(CONF_TRACK_LIMIT, default=user_input[CONF_TRACK_LIMIT])] = vol.Coerce(int) data_schema[vol.Optional(CONF_MAX_DATARATE, default=user_input[CONF_MAX_DATARATE])] = vol.Coerce(int) data_schema[vol.Optional(CONF_BRAND_ID, default=user_input[CONF_BRAND_ID])] = str # brand id diff --git a/custom_components/ytube_music_player/const.py b/custom_components/ytube_music_player/const.py index f8a973b..0654388 100644 --- a/custom_components/ytube_music_player/const.py +++ b/custom_components/ytube_music_player/const.py @@ -20,6 +20,7 @@ CONF_PASSWORD, STATE_PLAYING, STATE_PAUSED, + STATE_ON, STATE_OFF, STATE_IDLE, ATTR_COMMAND, @@ -41,8 +42,16 @@ DOMAIN as DOMAIN_MP, ) +# add for old settings +from homeassistant.components.input_boolean import ( + SERVICE_TURN_OFF as IB_OFF, + SERVICE_TURN_ON as IB_ON, + DOMAIN as DOMAIN_IB, +) + import homeassistant.components.select as select -import homeassistant.components.switch as switch +import homeassistant.components.input_select as input_select # add for old settings +import homeassistant.components.input_boolean as input_boolean # add for old settings # Should be equal to the name of your component. PLATFORMS = {"sensor", "select", "media_player" } @@ -100,8 +109,7 @@ SERVICE_CALL_MOVE_TRACK = "move_track_within_queue" SERVICE_CALL_APPEND_TRACK = "append_track_to_queue" - -CONF_RECEIVERS = 'speakers' # list of speakers (media_players) +CONF_RECEIVERS = 'speakers' # list of speakers (media_players) CONF_HEADER_PATH = 'header_path' CONF_API_LANGUAGE = 'api_language' CONF_SHUFFLE = 'shuffle' @@ -124,6 +132,25 @@ CONF_PROXY_URL = 'proxy_url' CONF_PROXY_PATH = 'proxy_path' +# add for old settings +CONF_SELECT_SOURCE = 'select_source' +CONF_SELECT_PLAYLIST = 'select_playlist' +CONF_SELECT_SPEAKERS = 'select_speakers' +CONF_SELECT_PLAYMODE = 'select_playmode' +CONF_SELECT_PLAYCONTINUOUS = 'select_playcontinuous' +OLD_INPUTS = { + "playlists": CONF_SELECT_PLAYLIST, + "speakers": CONF_SELECT_SPEAKERS, + "playmode": CONF_SELECT_PLAYMODE, + "radiomode": CONF_SELECT_SOURCE, + "repeatmode": CONF_SELECT_PLAYCONTINUOUS +} +DEFAULT_SELECT_PLAYCONTINUOUS = "" +DEFAULT_SELECT_SOURCE = "" +DEFAULT_SELECT_PLAYLIST = "" +DEFAULT_SELECT_PLAYMODE = "" +DEFAULT_SELECT_SPEAKERS = "" + DEFAULT_HEADER_FILENAME = 'header_' DEFAULT_API_LANGUAGE = 'en' DEFAULT_LIKE_IN_NAME = False @@ -326,6 +353,16 @@ def ensure_config(user_input): out[CONF_SHUFFLE_MODE] = PLAYMODE_DIRECT _LOGGER.error(f"shuffle_mode: {_shuffle_mode} is a deprecated value and has been replaced with '{out[CONF_SHUFFLE_MODE]}'.") + # If old input(s) exists,uncheck the new corresponding select(s). + # If the old input is set to a blank space character, then permanently delete this field. + for dropdown in ALL_DROPDOWNS: + if (_old_conf_input := out.get(OLD_INPUTS[dropdown])) is not None: + if _old_conf_input.replace(" ","") == "": + del out[OLD_INPUTS[dropdown]] + else: + if dropdown in out[CONF_INIT_DROPDOWNS]: + out[CONF_INIT_DROPDOWNS].remove(dropdown) + _LOGGER.warning(f"old {dropdown} input_select: {_old_conf_input} exists,uncheck the corresponding new select.") return out diff --git a/custom_components/ytube_music_player/media_player.py b/custom_components/ytube_music_player/media_player.py index 9e5a187..90c5d13 100644 --- a/custom_components/ytube_music_player/media_player.py +++ b/custom_components/ytube_music_player/media_player.py @@ -93,11 +93,20 @@ def __init__(self, hass, config, name_add): # All entities are now automatically generated,will be registered in the async_update_selects method later. # This should be helpful for multiple accounts. self._selects = dict() # use a dict to store the dropdown entity_id should be more convenient. - self._selects['playlists'] = None - self._selects['playmode'] = None - self._selects['repeatmode'] = None # Previously, it was _select_playContinuous. - self._selects['speakers'] = None # Previously, it was _select_mediaPlayer. - self._selects['radiomode'] = None # Previously, it was _select_source. + # For old settings. + for k,v in OLD_INPUTS.items(): + if v == CONF_SELECT_PLAYCONTINUOUS: + _domain = input_boolean.DOMAIN + else: + _domain = input_select.DOMAIN + try: + self._selects[k] = config.data.get(v) + except: + pass + if self._selects[k] is not None and self._selects[k].replace(" ","") != "": + self._selects[k] = _domain + "." + self._selects[k].replace(_domain + ".", "") + self.log_me('error', "Found old {} {}: {},Please consider using the new select entities.".format(_domain, k, self._selects[k] )) + self._like_in_name = config.data.get(CONF_LIKE_IN_NAME, DEFAULT_LIKE_IN_NAME) self._attr_shuffle = config.data.get(CONF_SHUFFLE, DEFAULT_SHUFFLE) @@ -488,9 +497,22 @@ async def async_set_repeat(self, repeat: str): # Set repeat mode. self._attr_repeat = repeat if(self._selects['repeatmode'] is not None): - if self.hass.states.get(self._selects['repeatmode']).state != repeat: - data = {select.ATTR_OPTION: repeat, ATTR_ENTITY_ID: self._selects['repeatmode']} - await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) + if repeat == RepeatMode.ALL: + ib_repeat = STATE_ON + else: + ib_repeat = STATE_OFF + if (_state := self.hass.states.get(self._selects['repeatmode']).state) != repeat: + if input_boolean.DOMAIN in self._selects['repeatmode']: + if _state != ib_repeat: + data = {ATTR_ENTITY_ID: self._selects['repeatmode']} + if ib_repeat == STATE_ON: + await self.hass.services.async_call(input_boolean.DOMAIN, IB_ON, data) + else: + await self.hass.services.async_call(input_boolean.DOMAIN, IB_OFF, data) + else: + data = {select.ATTR_OPTION: repeat, ATTR_ENTITY_ID: self._selects['repeatmode']} + await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) + self.log_me('debug', f"[E] set_repeat: {repeat}") self.async_schedule_update_ha_state() @@ -1027,7 +1049,7 @@ async def async_update_selects(self, now=None): self.log_me('debug', "[S] async_update_selects") # -- Register dropdown(s). -- # for dropdown in self._init_dropdowns: - if self._selects[dropdown] is None: + if not await self.async_check_entity_exists(self._selects[dropdown], unavailable_is_ok=False): entity_id = self.hass.data[DOMAIN][self._attr_unique_id][f'select_{dropdown}'].entity_id if await self.async_check_entity_exists(entity_id, unavailable_is_ok=False): self._selects[dropdown] = entity_id @@ -1072,9 +1094,18 @@ async def async_update_selects(self, now=None): self._friendly_speakersList.update({a: friendly_name}) friendly_speakersList = list(self._friendly_speakersList.values()) if self._selects['speakers'] is not None: - await self.hass.data[DOMAIN][self._attr_unique_id]['select_speakers'].async_update(friendly_speakersList) # update speaker select - data = {select.ATTR_OPTION: friendly_speakersList[0], ATTR_ENTITY_ID: self._selects['speakers']} # select the first one in the list as the default player - await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) + if input_select.DOMAIN in self._selects['speakers']: + _select = input_select + else: + _select = select + data = {_select.ATTR_OPTIONS: friendly_speakersList, ATTR_ENTITY_ID: self._selects['speakers']} + if _select == input_select: + await self.hass.services.async_call(input_select.DOMAIN, input_select.SERVICE_SET_OPTIONS, data) + else: + await self.hass.data[DOMAIN][self._attr_unique_id]['select_speakers'].async_update(friendly_speakersList) # update speaker select + + data = {_select.ATTR_OPTION: friendly_speakersList[0], ATTR_ENTITY_ID: self._selects['speakers']} # select the first one in the list as the default player + await self.hass.services.async_call(_select.DOMAIN, _select.SERVICE_SELECT_OPTION, data) # finally call update playlist to fill the list .. if it exists await self.async_update_playlists() @@ -1143,7 +1174,12 @@ async def async_update_playlists(self, now=None): # sort with case-ignore playlists = sorted(list(self._playlist_to_index.keys()), key=str.casefold) await self.async_update_extra_sensor('playlists', playlists_to_extra) # update extra sensor - await self.hass.data[DOMAIN][self._attr_unique_id]['select_playlists'].async_update() # update playlist select + if self._selects['playlists'] is not None: # update playlist select + if input_select.DOMAIN in self._selects['playlists']: + data = {input_select.ATTR_OPTIONS: list(playlists), ATTR_ENTITY_ID: self._selects["playlists"]} + await self.hass.services.async_call(input_select.DOMAIN, input_select.SERVICE_SET_OPTIONS, data) + else: + await self.hass.data[DOMAIN][self._attr_unique_id]['select_playlists'].async_update() except: self.exc() msg = "Caught error while loading playlist. please log for details" @@ -1192,14 +1228,15 @@ async def async_update_playmode(self, entity_id=None, old_state=None, new_state= self.log_me('debug', "[S] async_update_playmode") try: if self._selects['repeatmode'] is not None: - await self.async_set_repeat(self.hass.states.get(self._selects['repeatmode']).state) + if (_state := self.hass.states.get(self._selects['repeatmode']).state) == STATE_ON: + _state = RepeatMode.ALL + await self.async_set_repeat(_state) except: self.log_me('debug', "- Selection field " + self._selects['repeatmode'] + " not found, skipping") try: if self._selects['playmode'] is not None: - _playmode = self.hass.states.get(self._selects['playmode']).state - if _playmode is not None: + if (_playmode := self.hass.states.get(self._selects['playmode']).state) is not None: if _playmode in (PLAYMODE_SHUFFLE,PLAYMODE_DIRECT): shuffle = False else: @@ -1573,8 +1610,12 @@ async def async_play_media(self, media_type, media_id, _player=None, **kwargs): if _player is not None: await self.async_update_remote_player(remote_player=_player) if self._selects['speakers'] is not None: - data = {"option": _player, "entity_id": self._selects['speakers']} - await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) + if input_select.DOMAIN in self._selects['speakers']: + _select = input_select + else: + _select = select + data = {_select.ATTR_OPTION: _player, ATTR_ENTITY_ID: self._selects['speakers']} + await self.hass.services.async_call(_select.DOMAIN, _select.SERVICE_SELECT_OPTION, data) # load Tracks depending on input try: diff --git a/custom_components/ytube_music_player/translations/en.json b/custom_components/ytube_music_player/translations/en.json index 2e9fffc..fb6a3fd 100644 --- a/custom_components/ytube_music_player/translations/en.json +++ b/custom_components/ytube_music_player/translations/en.json @@ -33,7 +33,12 @@ "legacy_radio": "Create radio as watchlist of random playlist track", "sort_browser": "Sort results in the media browser", "extra_sensor": "Create sensor that provide extra information", - "dropdowns": "Create the dropdown(s) you want to use" + "dropdowns": "Create the dropdown(s) you want to use", + "select_speakers": "Entity id of input_select for speaker selection(Deprecated. Leaving a space can permanently delete this field)", + "select_playmode": "Entity id of input_select for playmode selection(Deprecated. Leaving a space can permanently delete this field)", + "select_source": "Entity id of input_select for playlist/radio selection(Deprecated. Leaving a space can permanently delete this field)", + "select_playlist": "Entity id of input_select for playlist selection(Deprecated. Leaving a space can permanently delete this field)", + "select_playcontinuous": "Entity id of input_boolean for play continuous selection(Deprecated. Leaving a space can permanently delete this field)" } } }, @@ -74,7 +79,12 @@ "legacy_radio": "Create radio as watchlist of random playlist track", "sort_browser": "Sort results in the media browser", "extra_sensor": "Create sensor that provide extra information", - "dropdowns": "Create the dropdown(s) you want to use" + "dropdowns": "Create the dropdown(s) you want to use", + "select_speakers": "Entity id of input_select for speaker selection(Deprecated. Leaving a space can permanently delete this field)", + "select_playmode": "Entity id of input_select for playmode selection(Deprecated. Leaving a space can permanently delete this field)", + "select_source": "Entity id of input_select for playlist/radio selection(Deprecated. Leaving a space can permanently delete this field)", + "select_playlist": "Entity id of input_select for playlist selection(Deprecated. Leaving a space can permanently delete this field)", + "select_playcontinuous": "Entity id of input_boolean for play continuous selection(Deprecated. Leaving a space can permanently delete this field)" } } }, diff --git a/custom_components/ytube_music_player/translations/zh-Hans.json b/custom_components/ytube_music_player/translations/zh-Hans.json index 5e18027..cbbc255 100644 --- a/custom_components/ytube_music_player/translations/zh-Hans.json +++ b/custom_components/ytube_music_player/translations/zh-Hans.json @@ -33,7 +33,12 @@ "legacy_radio": "将随机播放列表曲目创建为收藏夹电台", "sort_browser": "在媒体浏览器中对结果进行排序", "extra_sensor": "创建提供额外信息的传感器实体", - "dropdowns": "创建你需要的下拉菜单实体" + "dropdowns": "创建你需要的下拉菜单实体", + "select_speakers": "播放设备下拉菜单实体ID(已过期,留下一个空格字符可以永久删除这个字段)", + "select_playmode":"循环模式下拉菜单实体ID(已过期,留下一个空格字符可以永久删除这个字段)", + "select_source":"播放列表/电台选择下拉菜单实体ID(已过期,留下一个空格字符可以永久删除这个字段)", + "select_playlist":"播放列表下拉菜单实体ID(已过期,留下一个空格字符可以永久删除这个字段)", + "select_playcontinuous":"持续播放模式下拉菜单实体ID(已过期,留下一个空格字符可以永久删除这个字段)" } } }, @@ -74,7 +79,12 @@ "legacy_radio": "将随机播放列表曲目创建为收藏夹电台", "sort_browser": "在媒体浏览器中对结果进行排序", "extra_sensor": "创建提供额外信息的传感器实体", - "dropdowns": "创建你需要的下拉菜单实体" + "dropdowns": "创建你需要的下拉菜单实体", + "select_speakers": "播放设备下拉菜单实体ID(已过期,留下一个空格字符可以永久删除这个字段)", + "select_playmode":"循环模式下拉菜单实体ID(已过期,留下一个空格字符可以永久删除这个字段)", + "select_source":"播放列表/电台选择下拉菜单实体ID(已过期,留下一个空格字符可以永久删除这个字段)", + "select_playlist":"播放列表下拉菜单实体ID(已过期,留下一个空格字符可以永久删除这个字段)", + "select_playcontinuous":"持续播放模式下拉菜单实体ID(已过期,留下一个空格字符可以永久删除这个字段)" } } },