Skip to content
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

Merged
merged 14 commits into from
Feb 6, 2018

Conversation

mhilbush
Copy link
Contributor

@mhilbush mhilbush commented Jan 29, 2018

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.

  • the SqueezeBox Server 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).
  • the channel is formatted with comma-separated values of the form favoriteId=favoriteName, like this:
1=Rock,2=Folk,3=Classic Alternative
  • there’s a config option that allows you to wrap the favoriteName with double quotes
1="Rock",2="Folk",3="Classic Alternative"
  • only favorites at the root level of the LMS favorites hierarchy are returned
  • the server handler sends the favorites list to the player handler

Add a playFavorite channel of type String to the squeezebox player thing.

  • when you send the favoriteId to an item linked to a player's playFavorite channel, it plays the favorite on that player.
  • the player uses the favorites list from the server handler to update the playFavorite channel's state description to include the favorites as options.

Dependencies:

  • Sitemap needs to be updated to be able to use the item's state options as the selection list whenever the state options change
  • HABpanel needs to be able to update the choices in the Selection widget whenever the state options change

@mhilbush mhilbush changed the title [squeezebox] Add favoriteList/favoritesPlay channel to server/player [squeezebox] WIP Add favoriteList/favoritesPlay channel to server/player Jan 29, 2018
@tavalin
Copy link

tavalin commented Jan 30, 2018

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?

@mhilbush
Copy link
Contributor Author

mhilbush commented Jan 30, 2018

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.

@mhilbush
Copy link
Contributor Author

BTW @tavalin , would you be able to point me to a good example of how to update the StateDescription during runtime? ;-)

@tavalin
Copy link

tavalin commented Jan 30, 2018

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

@cweitkamp
Copy link
Contributor

cweitkamp commented Jan 30, 2018

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

@mhilbush
Copy link
Contributor Author

@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]>
@mhilbush mhilbush changed the title [squeezebox] WIP Add favoriteList/favoritesPlay channel to server/player [squeezebox] Add LMS favorites channels to server and player Jan 31, 2018
@mhilbush
Copy link
Contributor Author

@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!

@cweitkamp
Copy link
Contributor

cweitkamp commented Jan 31, 2018

You are welcome. I'm going to review your PR in the next few days. Thanks for asking.

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?

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.

Copy link
Contributor

@cweitkamp cweitkamp left a 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);
Copy link
Contributor

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">
Copy link
Contributor

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this import necessary?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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();
}
}
Copy link
Contributor

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
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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"

Copy link
Contributor

@cweitkamp cweitkamp Feb 2, 2018

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.

Copy link
Contributor Author

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>();
Copy link
Contributor

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 + "=");
Copy link
Contributor

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.",
Copy link
Contributor

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]>
@mhilbush
Copy link
Contributor Author

mhilbush commented Feb 1, 2018

@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. :-(

@cweitkamp
Copy link
Contributor

You're welcome.

That double-space between sentences thing... Showing my age. :-(

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]>
@mhilbush
Copy link
Contributor Author

mhilbush commented Feb 2, 2018

@martinvw This is ready for final review. Thanks.

Copy link
Contributor

@cweitkamp cweitkamp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mhilbush
Copy link
Contributor Author

mhilbush commented Feb 2, 2018

@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?

@cweitkamp
Copy link
Contributor

I'm pretty sure you can foresee my answer...

@mhilbush
Copy link
Contributor Author

mhilbush commented Feb 2, 2018

Yeah, it was somewhat of a rhetorical question. LOL

It's already done, so I just need to push it. :-)

Copy link
Contributor

@cweitkamp cweitkamp left a 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;
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Touch documentation.

Copy link
Contributor Author

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">
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getThing()?

Copy link
Contributor Author

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?

Copy link
Contributor

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]>
Copy link
Contributor

@cweitkamp cweitkamp left a 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.

@mhilbush
Copy link
Contributor Author

mhilbush commented Feb 3, 2018

Thanks for the help @cweitkamp!

@martinvw Ready for final review

Copy link
Member

@martinvw martinvw left a 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) {
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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.

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.

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

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() ? "\"" : "";
Copy link
Member

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.

Copy link
Contributor Author

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,
Copy link
Member

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

Copy link
Contributor Author

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?

Copy link
Contributor

@cweitkamp cweitkamp Feb 6, 2018

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.

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 = "";
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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 = "";
Copy link
Member

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?

Copy link
Contributor Author

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) {
Copy link
Member

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?

Copy link
Contributor Author

@mhilbush mhilbush Feb 5, 2018

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;
Copy link
Member

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?

Copy link
Contributor Author

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) {
Copy link
Member

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 :-)

Copy link
Contributor Author

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]>
@mhilbush
Copy link
Contributor Author

mhilbush commented Feb 6, 2018

@martinvw I think I've addressed your comments.

I do have a couple more questions.

  • Is there a more preferred way here to handle the case where the favorites list is empty?
  • Perhaps for another PR, but what is the guidance for which classes should be in the org.openhab.binding.squeezebox.handler package?

@martinvw martinvw merged commit d449969 into openhab:master Feb 6, 2018
@martinvw
Copy link
Member

martinvw commented Feb 6, 2018

Thanks all!

@mhilbush
Copy link
Contributor Author

mhilbush commented Feb 6, 2018

Thank you @martinvw and @cweitkamp.

@staehler
Copy link

staehler commented Feb 6, 2018

@mhilbush Could you please provide an example how to use favoritList and playFavorit. I would like to use this feature :-)
Thank you very much.

@mhilbush
Copy link
Contributor Author

mhilbush commented Feb 6, 2018

Could you please provide an example

@staehler I added some directions here.
https://community.openhab.org/t/squeezebox-favorites/39355/12

@mhilbush mhilbush deleted the squeezebox-favorites branch February 6, 2018 22:29
@triller-telekom
Copy link

@mhilbush How about a follow-up PR where you add the instructions to the official README.md of the binding? :)

@mhilbush
Copy link
Contributor Author

mhilbush commented Feb 7, 2018

@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.

@martinvw martinvw changed the title [squeezebox] Add LMS favorites channels to server and player [Squeezebox] Add LMS favorites channels to server and player May 25, 2018
@martinvw martinvw added this to the 2.3 milestone May 25, 2018
@martinvw martinvw added the enhancement An enhancement or new feature for an existing add-on label May 25, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement An enhancement or new feature for an existing add-on
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants