-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Squeezebox] Add LMS favorites channels to server and player #3190
Conversation
Signed-off-by: Mark Hilbush <[email protected]>
Signed-off-by: Mark Hilbush <[email protected]>
Instead of having this channel as a string of comma-separated values, wouldn't it be better for the list (received from the server) to dynamically update the state options for the channel? |
Yes, I'm actually working on that right now. :-) Passing the favorites list to the players, who will then update the state options on their favoritesPlay channel. Just learning how to do that... I still may keep the server channel the way it is, since someone might find it handy to process that in a rule. |
Signed-off-by: Mark Hilbush <[email protected]>
BTW @tavalin , would you be able to point me to a good example of how to update the StateDescription during runtime? ;-) |
The HarmonyHub binding updates the state options during runtime. Have a look here https://github.com/openhab/openhab2-addons/blob/master/addons/binding/org.openhab.binding.harmonyhub/src/main/java/org/openhab/binding/harmonyhub/handler/HarmonyDeviceHandler.java#L160 |
I added some details to eclipse-archived/smarthome#4942. But lets continue our discussion about the best practice how to implement that feature here. imho the best way to solve this is to implement a DynamicStateDescriptionProvider for the binding. Have a look here for an example: https://github.com/openhab/openhab2-addons/pull/2345/files#diff-005ee22fbafd96dfe88650528963939f |
@cweitkamp That's a great example, and it seems to work very well. Much simpler than everything else I looked at. Thanks! It doesn't look like HABpanel updates the options dynamically during runtime. I posed a question about that on the habpanel repo. In fact, it looks a refresh of the Control page is needed in PaperUI to refresh the options. Is that you're understanding of how it works, or how it's supposed to work? |
Signed-off-by: Mark Hilbush <[email protected]>
Signed-off-by: Mark Hilbush <[email protected]>
Signed-off-by: Mark Hilbush <[email protected]>
@cweitkamp Your review of this would be appreciated. I followed your recommendation to use DynamicStateDescriptionProvider for managing the update to the channel's state options. Thank you for that! |
You are welcome. I'm going to review your PR in the next few days. Thanks for asking.
To be honest, I can't tell you. I suppose such an event doesn't exist. Paper UI has no chance to listen for a dynamically updated options list. Could be true for other UIs as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work. I added some thoughts.
options.add(new StateOption(favorite.shortId, favorite.name)); | ||
} | ||
stateDescriptionProvider.setStateOptions( | ||
new ChannelUID(getThing().getUID(), SqueezeBoxBindingConstants.CHANNEL_FAVORITES_PLAY), options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove SqueezeBoxBindingConstants.
.
<description>Comma-separated list of favorites of form favoriteId=favoriteName</description> | ||
<state readOnly="true" pattern="%s"></state> | ||
</channel-type> | ||
<channel-type id="favoritesPlay"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was asking myself if playFavorite
sounds better. Wdyt?
@@ -39,10 +41,13 @@ | |||
import org.eclipse.smarthome.core.types.Command; | |||
import org.eclipse.smarthome.core.types.RefreshType; | |||
import org.eclipse.smarthome.core.types.State; | |||
import org.eclipse.smarthome.core.types.StateOption; | |||
import org.eclipse.smarthome.core.types.UnDefType; | |||
import org.eclipse.smarthome.io.net.http.HttpUtil; | |||
import org.openhab.binding.squeezebox.SqueezeBoxBindingConstants; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this import necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. It's used in updateFavoritesListEvent
to create the arraylist
for setStateOptions
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You removed the reference in one of your last commits. And as you already imported it static
it's redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, there was another reference that I missed. I'll fix it.
/** | ||
* Dynamic provider of state options while leaving other state description fields as original. | ||
* | ||
* @author Gregory Moyer - Initial contribution |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you do it like this you should add the Also-by
tag to the description or your PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can do that. I wasn't sure the best way to handle this. Is there another way that's more preferred?
public void deactivate() { | ||
channelOptionsMap.clear(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing newline.
* | ||
*/ | ||
public class Favorite { | ||
// id is of form xxxxxxxx.nn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please use JavaDoc here?
|
||
private void updateChannelFavoritesList(List<Favorite> favorites) { | ||
// Get config parameter indicating whether name should be wrapped with double quotes | ||
final boolean includeQuotes = getConfigAs(SqueezeBoxServerConfig.class).quoteFavoritesList; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need those quotes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was looking to provide some flexibility, depending on where/how someone was going to use this list (presumably in a rule).
1=Rock,2=Folk,3=Classic Alternative
Or
1="Rock",2="Folk",3="Classic Alternative"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, just a guess: Isn't the quoteFavoritesList
configuration more a channel configuration than a thing configuration? Is it? But I don't want you to blow up this PR with it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, yeah, I actually thought about that after I implemented this way. It probably should be a channel configuration, huh?
return; | ||
} | ||
|
||
List<Favorite> favorites = new ArrayList<Favorite>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Diamond operator -> new ArrayList<>()
.
for (Favorite favorite : favorites) { | ||
// If not quoting, we don't want any embedded commas | ||
adjustedName = includeQuotes ? favorite.name : favorite.name.replaceAll(",", ""); | ||
sb.append(favorite.shortId + "="); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid concatenation of strings, if you use a StringBuilder
. Use the append
method all the time.
|
||
// If the channel doesn't exist we can't update it | ||
if (getThing().getChannel(CHANNEL_FAVORITES_LIST) == null) { | ||
logger.debug("Channel '{}' does not exist. Delete and readd player thing to pick up channel.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I see double whitespaces between the sentences?
Signed-off-by: Mark Hilbush <[email protected]>
Signed-off-by: Mark Hilbush <[email protected]>
@cweitkamp Thanks for the feedback; very much appreciated. I believe I addressed all your feedback except for where I added comments/questions above. That double-space between sentences thing... Showing my age. :-( |
You're welcome.
Don't bother. You are doing a good job and I really enjoy working together with you. I thought twice about commenting that line or not. ;-) |
Signed-off-by: Mark Hilbush <[email protected]>
Signed-off-by: Mark Hilbush <[email protected]>
@martinvw This is ready for final review. Thanks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@cweitkamp So what do you think about moving the config to the channel? If that's the more correct approach, then it would be better to do now. WDYT? |
I'm pretty sure you can foresee my answer... |
Yeah, it was somewhat of a rhetorical question. LOL It's already done, so I just need to push it. :-) |
Signed-off-by: Mark Hilbush <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was quick. Some minor remarks.
/* | ||
* When true, wrap the name of the favorite in quotes | ||
*/ | ||
public boolean quoteFavoritesList; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Want to remove this property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok
{ | ||
Thing squeezeboxplayer myplayer[ mac="00:f1:bb:00:00:f1" ] | ||
} | ||
``` | ||
|
||
## Channels | ||
## Server Channels |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Touch documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. Missed that.
<description>Comma-separated list of favorites of form favoriteId=favoriteName</description> | ||
<state readOnly="true" pattern="%s"></state> | ||
<config-description> | ||
<parameter name="quoteFavoritesList" type="boolean" required="true"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about renaming it to quoteList?
For the sake of reusability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok
} | ||
|
||
private void updateChannelFavoritesList(List<Favorite> favorites) { | ||
final Channel channel = thing.getChannel(CHANNEL_FAVORITES_LIST); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getThing()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. But why does BaseThingHandler expose both?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to admit. This was nitpicking. Sry for that.
Signed-off-by: Mark Hilbush <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you very much. LGTM.
Thanks for the help @cweitkamp! @martinvw Ready for final review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks guys, I added some additional comments
logger.trace("Updating channel {} for {} to state {}", channelID, getThing().getUID(), state); | ||
try { | ||
updateState(channelID, state); | ||
} catch (IllegalStateException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In what case will this happen of did I already ask that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API spec for BaseThingHandler
says if handler is not initialized correctly, because no callback is present
And, I chose to not do this. ;-)
https://github.com/openhab/openhab2-addons/blob/master/addons/binding/org.openhab.binding.squeezebox/src/main/java/org/openhab/binding/squeezebox/handler/SqueezeBoxPlayerHandler.java#L471
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I'm not happy with this, @SJKA @triller-telekom do you have any useful suggestions here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These lines are in the BaseThingHandler
since 17.03.2015. So this Exception was thrown ever since then. Since it is a RuntimeException
it does not have to be caught at compile time. And since all the other bindings do not catch it at compile time I think it's alright to let it pass through to the code above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. I'll remove it from here and SqueezeBoxPlayerHandler
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you do that already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duh. It would help if I actually pushed the commit.
includeQuotes = (Boolean) channel.getConfiguration().get(CHANNEL_CONFIG_QUOTE_LIST); | ||
} | ||
|
||
final String quote = includeQuotes.booleanValue() ? "\"" : ""; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to leave away the final
here since they are not consistently used (in this binding) anyways.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok
} | ||
|
||
@Override | ||
public @Nullable StateDescription getStateDescription(@NonNull Channel channel, @Nullable StateDescription original, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CC @triller-telekom if I remember correctly we should not be using both of these in this case it should
See also https://www.eclipse.org/smarthome/documentation/development/guidelines.html#a-code-style A.12
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@triller-telekom Can you also weigh in on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can jump in here: In general we want to avoid using @NonNull
but use @NonNullByDefault
on class level. You can have a look at #3207 I already changed adapted it in my PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mhilbush Yes I second @cweitkamp's comment, we are only using @NonNullByDefault
and @Nullable
, see the link that @martinvw has posted.
} | ||
|
||
List<Favorite> favorites = new ArrayList<>(); | ||
String title = ""; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is this used for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chose to capture the favorites title and count, as they are returned in the response from the LMS, even though they currently are not used anywhere. Would you rather they be removed, or commented as currently unused?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or you can log them inside a isTraceEnabled
and not store it at all?
|
||
List<Favorite> favorites = new ArrayList<>(); | ||
String title = ""; | ||
String count = ""; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is this used for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment above.
// Favorite type | ||
else if (part.startsWith("type%3A")) { | ||
type = decode(part.substring("type%3A".length())); | ||
if (f != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the order guaranteed, will we revisit this if f
is null
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guaranteed is a little strong. :-)
The order is defined in the LMS code, and is highly unlikely to change at this point. The convention in the LMS code generally is that the id is the first value returned in a response. I was simply trying to be defensive in case of a corrupted response.
The response from the LMS looks like this.
favorites = title favorite1 favorite2 ... count
where favoriteN = id name type isaudio hasitems
String count = ""; | ||
Favorite f = null; | ||
for (String part : messageParts) { | ||
String id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need all these fields a quick looks suggest that you either assign them immediately or discard the data, wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as my comment above. They are returned in the response from the LMS, even though they currently are not used.
Signed-off-by: Mark Hilbush <[email protected]>
|
||
List<Favorite> favorites = new ArrayList<>(); | ||
Favorite f = null; | ||
for (String part : messageParts) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did actually not mean to remove all the fields but the big benefit is that this method now more or less fits on my screen :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I know. But this is actually more consistent with how the binding handles other LMS messages. ;-)
Signed-off-by: Mark Hilbush <[email protected]>
@martinvw I think I've addressed your comments. I do have a couple more questions.
|
Thanks all! |
Thank you @martinvw and @cweitkamp. |
@mhilbush Could you please provide an example how to use favoritList and playFavorit. I would like to use this feature :-) |
@staehler I added some directions here. |
@mhilbush How about a follow-up PR where you add the instructions to the official README.md of the binding? :) |
@triller-telekom Good idea. I'm working on a PR to replace the notification volume channel with a config parameter. I'll also add the favorites instructions to the README. |
Signed-off-by: Mark Hilbush [email protected]
Also-by: Gregory Moyer [email protected]
Add two new channels to enable use of the favorites list on the LMS (Logitech Media Server).
Add a readonly
favoritesList
channel of type String to the squeezebox server thing.favoritesList
channel is updated immediately when the binding starts, and whenever the favorites list is updated on the LMS (there’s an asynchronous notification from the LMS to the binding when the favorites list is updated).Add a
playFavorite
channel of type String to the squeezebox player thing.playFavorite
channel, it plays the favorite on that player.playFavorite
channel's state description to include the favorites as options.Dependencies: