diff --git a/CODEOWNERS b/CODEOWNERS index 77674a2318817..e34b7d4c3f0f9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -207,6 +207,7 @@ /bundles/org.openhab.binding.sonyprojector/ @lolodomo /bundles/org.openhab.binding.spotify/ @Hilbrand /bundles/org.openhab.binding.squeezebox/ @digitaldan @mhilbush +/bundles/org.openhab.binding.mpd/ @stefanroellin /bundles/org.openhab.binding.synopanalyzer/ @clinique /bundles/org.openhab.binding.systeminfo/ @svilenvul /bundles/org.openhab.binding.tado/ @dfrommi diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 2843a73fa5829..9884c4def7bf6 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1024,6 +1024,11 @@ org.openhab.binding.modbus.sunspec ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.mpd + ${project.version} + org.openhab.addons.bundles org.openhab.binding.synopanalyzer diff --git a/bundles/org.openhab.binding.mpd/.classpath b/bundles/org.openhab.binding.mpd/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.mpd/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.mpd/.project b/bundles/org.openhab.binding.mpd/.project new file mode 100644 index 0000000000000..1a9ed616dc440 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.mpd + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.mpd/NOTICE b/bundles/org.openhab.binding.mpd/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.mpd/README.md b/bundles/org.openhab.binding.mpd/README.md new file mode 100644 index 0000000000000..5704e4d7c81e1 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/README.md @@ -0,0 +1,106 @@ +# MPD Binding + +[Music Player Daemon (MPD)](http://www.musicpd.org/) is a flexible, powerful, server-side application for playing music. Through plugins and libraries it can play a variety of sound files while being controlled by its network protocol. + +With the openHAB MPD binding you can control Music Player Daemons. + + +## Supported Things + +This binding supports one ThingType: mpd + +## Discovery + +If zeroconf is enabled in the Music Player Daemon, it is discovered. Each Music Player daemon requires a unique zeroconf_name for correct discovery. + + +## Thing Configuration + +The ThingType mpd requires the following configuration parameters: + +| Parameter Label | Parameter ID | Description | Required | +|-----------------|--------------|--------------------------------------------------------------------------|----------| +| IP Address | ipAddress | Host name or IP address of the Music Player Daemon | yes | +| Port | port | Port number on which the Music Player Daemon is listening. Default: 6600 | yes | +| Password | password | Password to access the Music Player Daemon | no | + + +## Channels + +The following channels are currently available: + +| Channel Type ID | Item Type | Description | +|-----------------|-----------|---------------------------| +| control | Player | Start/Pause/Next/Previous | +| volume | Dimmer | Volume in percent | +| stop | Switch | Stop playback | +| currentalbum | String | Current album | +| currentartist | String | Current artist | +| currentname | String | Current name | +| currentsong | Number | Current song | +| currentsongid | Number | Current song id | +| currenttitle | String | Current title | +| currenttrack | Number | Current track | + + +## Full Example + +#### Thing + +``` +mpd:mpd:music [ ipAddress="192.168.1.2", port=6600 ] +``` + +#### Items + +``` +Switch morning_music "Morning music" + +Player mpd_music_player "Player" { channel = "mpd:mpd:music:control" } +Dimmer mpd_music_volume "Volume [%d %%]" { channel = "mpd:mpd:music:volume" } +Switch mpd_music_stop "Stop" { channel = "mpd:mpd:music:stop" } +String mpd_music_album "Album [%s]" { channel = "mpd:mpd:music:currentalbum" } +String mpd_music_artist "Artist [%s]" { channel = "mpd:mpd:music:currentartist" } +String mpd_music_name "Name [%s]" { channel = "mpd:mpd:music:currentname" } +Number mpd_music_song "Song [%d]" { channel = "mpd:mpd:music:currentsong" } +Number mpd_music_song_id "Song Id [%d]" { channel = "mpd:mpd:music:currentsongid" } +String mpd_music_title "Title [%s]" { channel = "mpd:mpd:music:currenttitle" } +Number mpd_music_track "Track [%d]" { channel = "mpd:mpd:music:currenttrack" } +``` + +#### Sitemap + +``` +Frame label="Music" { + Default item=mpd_music_player + Slider item=mpd_music_volume + Switch item=mpd_music_stop + Text item=mpd_music_album + Text item=mpd_music_artist + Text item=mpd_music_name + Text item=mpd_music_song + Text item=mpd_music_song_id + Text item=mpd_music_title + Text item=mpd_music_track +} +``` + +#### Rule + +``` +rule "turn on morning music" +when + Item morning_music changed to ON +then + val actions = getActions("mpd","mpd:mpd:music") + if(actions === null) { + logWarn("myLog", "actions is null") + return + } + + actions.sendCommand("clear") + actions.sendCommand("load", "MorningMusic"); + actions.sendCommand("shuffle"); + actions.sendCommand("play"); +end +``` diff --git a/bundles/org.openhab.binding.mpd/pom.xml b/bundles/org.openhab.binding.mpd/pom.xml new file mode 100644 index 0000000000000..9de0ba224fd03 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.8-SNAPSHOT + + + org.openhab.binding.mpd + + openHAB Add-ons :: Bundles :: MPD Binding + + diff --git a/bundles/org.openhab.binding.mpd/src/main/feature/feature.xml b/bundles/org.openhab.binding.mpd/src/main/feature/feature.xml new file mode 100644 index 0000000000000..001bc18f424c1 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.mpd/${project.version} + + diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDBindingConstants.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDBindingConstants.java new file mode 100644 index 0000000000000..cba03de78a52d --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDBindingConstants.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link MPDBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public class MPDBindingConstants { + + private static final String BINDING_ID = "mpd"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_MPD = new ThingTypeUID(BINDING_ID, "mpd"); + + // List of all Channel ids + public static final String CHANNEL_CONTROL = "control"; + public static final String CHANNEL_CURRENT_ALBUM = "currentalbum"; + public static final String CHANNEL_CURRENT_ARTIST = "currentartist"; + public static final String CHANNEL_CURRENT_NAME = "currentname"; + public static final String CHANNEL_CURRENT_SONG = "currentsong"; + public static final String CHANNEL_CURRENT_SONG_ID = "currentsongid"; + public static final String CHANNEL_CURRENT_TITLE = "currenttitle"; + public static final String CHANNEL_CURRENT_TRACK = "currenttrack"; + public static final String CHANNEL_STOP = "stop"; + public static final String CHANNEL_VOLUME = "volume"; + + // Config Parameters + public static final String PARAMETER_IPADDRESS = "ipAddress"; + public static final String PARAMETER_PORT = "port"; + public static final String UNIQUE_ID = "uniqueId"; +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDConfiguration.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDConfiguration.java new file mode 100644 index 0000000000000..13d18d840d82b --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDConfiguration.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MPDConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public class MPDConfiguration { + + private String ipAddress = ""; + private Integer port = 0; + private String password = ""; + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDException.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDException.java new file mode 100644 index 0000000000000..b3c2e63a07f28 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDException.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MPDException} class is used for any exception thrown by the binding + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public class MPDException extends Exception { + private static final long serialVersionUID = 1L; + + public MPDException() { + } + + public MPDException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDHandlerFactory.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDHandlerFactory.java new file mode 100644 index 0000000000000..b29a0c8021c86 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDHandlerFactory.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal; + +import static org.openhab.binding.mpd.internal.MPDBindingConstants.THING_TYPE_MPD; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.openhab.binding.mpd.internal.handler.MPDHandler; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link MPDHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.mpd", service = ThingHandlerFactory.class) +public class MPDHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_MPD); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_MPD.equals(thingTypeUID)) { + return new MPDHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/action/IMPDActions.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/action/IMPDActions.java new file mode 100644 index 0000000000000..6accb2c11b1b1 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/action/IMPDActions.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal.action; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link IMPDActions} interface defines rule actions for sending commands to a Music Player Daemon + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public interface IMPDActions { + + public void sendCommand(@Nullable String command, @Nullable String parameter); + + public void sendCommand(@Nullable String command); +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/action/MPDActions.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/action/MPDActions.java new file mode 100644 index 0000000000000..26d7addc1ef1c --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/action/MPDActions.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal.action; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.binding.ThingActions; +import org.eclipse.smarthome.core.thing.binding.ThingActionsScope; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.openhab.binding.mpd.internal.handler.MPDHandler; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.RuleAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link @MPDActions} defines rule actions for the Music Player Daemon binding. + * + * @author Stefan Röllin - Initial contribution + */ +@ThingActionsScope(name = "mpd") +@NonNullByDefault +public class MPDActions implements ThingActions, IMPDActions { + + private final Logger logger = LoggerFactory.getLogger(MPDActions.class); + + private @Nullable MPDHandler handler = null; + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof MPDHandler) { + this.handler = (MPDHandler) handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return handler; + } + + @Override + @RuleAction(label = "MPD : Send command", description = "Send a command to the Music Player Daemon.") + public void sendCommand(@ActionInput(name = "command") @Nullable String command, + @ActionInput(name = "parameter") @Nullable String parameter) { + logger.debug("sendCommand called with {}", command); + + MPDHandler handler = this.handler; + if (handler != null) { + handler.sendCommand(command, parameter); + } else { + logger.warn("MPD Action service ThingHandler is null!"); + } + } + + @Override + @RuleAction(label = "MPD : Send command", description = "Send a command to the Music Player Daemon.") + public void sendCommand(@ActionInput(name = "command") @Nullable String command) { + logger.debug("sendCommand called with {}", command); + + MPDHandler handler = this.handler; + if (handler != null) { + handler.sendCommand(command); + } else { + logger.warn("MPD Action service ThingHandler is null!"); + } + } + + private static IMPDActions invokeMethodOf(@Nullable ThingActions actions) { + if (actions == null) { + throw new IllegalArgumentException("actions cannot be null"); + } + if (actions.getClass().getName().equals(MPDActions.class.getName())) { + if (actions instanceof IMPDActions) { + return (IMPDActions) actions; + } else { + return (IMPDActions) Proxy.newProxyInstance(IMPDActions.class.getClassLoader(), + new Class[] { IMPDActions.class }, (Object proxy, Method method, Object[] args) -> { + Method m = actions.getClass().getDeclaredMethod(method.getName(), + method.getParameterTypes()); + return m.invoke(actions, args); + }); + } + } + throw new IllegalArgumentException("Actions is not an instance of MPDActions"); + } + + public static void sendCommand(@Nullable ThingActions actions, @Nullable String command, + @Nullable String parameter) { + invokeMethodOf(actions).sendCommand(command, parameter); + } + + public static void sendCommand(@Nullable ThingActions actions, @Nullable String command) { + invokeMethodOf(actions).sendCommand(command); + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/discovery/MPDDiscoveryParticipant.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/discovery/MPDDiscoveryParticipant.java new file mode 100644 index 0000000000000..1300dcd0ed7aa --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/discovery/MPDDiscoveryParticipant.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal.discovery; + +import java.net.Inet4Address; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.jmdns.ServiceInfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.DiscoveryResult; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.mdns.MDNSDiscoveryParticipant; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.openhab.binding.mpd.internal.MPDBindingConstants; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of {@link MDNSDiscoveryParticipant} that will discover Music Player Daemons. + * + * @author Stefan Röllin - Initial contribution + * + */ +@NonNullByDefault +@Component(immediate = true) +public class MPDDiscoveryParticipant implements MDNSDiscoveryParticipant { + + private final Logger logger = LoggerFactory.getLogger(MPDDiscoveryParticipant.class); + + @Override + public Set getSupportedThingTypeUIDs() { + return Collections.singleton(MPDBindingConstants.THING_TYPE_MPD); + } + + @Override + public String getServiceType() { + return "_mpd._tcp.local."; + } + + @Override + @Nullable + public DiscoveryResult createResult(ServiceInfo service) { + ThingUID uid = getThingUID(service); + String host = getHostAddress(service); + int port = service.getPort(); + + logger.debug("Music Player Daemon found on host {} port {}", host, port); + + if (uid == null || host == null || host.isEmpty()) { + return null; + } + + String uniquePropVal = String.format("%s-%d", host, port); + + final Map properties = new HashMap<>(3); + properties.put(MPDBindingConstants.PARAMETER_IPADDRESS, host); + properties.put(MPDBindingConstants.PARAMETER_PORT, port); + properties.put(MPDBindingConstants.UNIQUE_ID, uniquePropVal); + + String name = service.getName(); + + final DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(name).withProperties(properties) + .withRepresentationProperty(MPDBindingConstants.UNIQUE_ID).build(); + return result; + } + + @Nullable + private String getHostAddress(ServiceInfo service) { + if (service.getInet4Addresses() != null) { + for (Inet4Address addr : service.getInet4Addresses()) { + if (addr != null) { + return addr.getHostAddress(); + } + } + } + return null; + } + + @Override + @Nullable + public ThingUID getThingUID(ServiceInfo service) { + if (getServiceType().equals(service.getType())) { + String name = getUIDName(service.getName()); + if (!name.isEmpty()) { + return new ThingUID(MPDBindingConstants.THING_TYPE_MPD, name); + } + } + return null; + } + + private String getUIDName(@Nullable String serviceName) { + if (serviceName == null) { + return ""; + } + return serviceName.replaceAll("[^A-Za-z0-9_]", "_").replaceAll("_+", "_"); + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/handler/MPDEventListener.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/handler/MPDEventListener.java new file mode 100644 index 0000000000000..18391bd88ff65 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/handler/MPDEventListener.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.mpd.internal.handler; + +import java.util.EventListener; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.openhab.binding.mpd.internal.protocol.MPDConnection; +import org.openhab.binding.mpd.internal.protocol.MPDSong; +import org.openhab.binding.mpd.internal.protocol.MPDStatus; + +/** + * Interface which has to be implemented by a class in order to get + * updates from a {@link MPDConnection} + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public interface MPDEventListener extends EventListener { + + void updateMPDSong(MPDSong song); + + void updateMPDStatus(MPDStatus status); + + void updateThingStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description); +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/handler/MPDHandler.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/handler/MPDHandler.java new file mode 100644 index 0000000000000..67654de6a16eb --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/handler/MPDHandler.java @@ -0,0 +1,293 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal.handler; + +import static org.openhab.binding.mpd.internal.MPDBindingConstants.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType; +import org.eclipse.smarthome.core.library.types.NextPreviousType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.PlayPauseType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +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.UnDefType; +import org.openhab.binding.mpd.internal.MPDBindingConstants; +import org.openhab.binding.mpd.internal.MPDConfiguration; +import org.openhab.binding.mpd.internal.action.MPDActions; +import org.openhab.binding.mpd.internal.protocol.MPDConnection; +import org.openhab.binding.mpd.internal.protocol.MPDSong; +import org.openhab.binding.mpd.internal.protocol.MPDStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link MPDHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public class MPDHandler extends BaseThingHandler implements MPDEventListener { + + private final Logger logger = LoggerFactory.getLogger(MPDHandler.class); + + private Map stateMap = Collections.synchronizedMap(new HashMap()); + + private final MPDConnection connection; + private int volume = 0; + + private @Nullable ScheduledFuture futureUpdateStatus; + private @Nullable ScheduledFuture futureUpdateCurrentSong; + + public MPDHandler(Thing thing) { + super(thing); + connection = new MPDConnection(this); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + handleCommandRefresh(channelUID.getId()); + } else { + handlePlayerCommand(channelUID.getId(), command); + } + } + + @Override + public void initialize() { + MPDConfiguration config = getConfigAs(MPDConfiguration.class); + String uniquePropVal = String.format("%s-%d", config.getIpAddress(), config.getPort()); + updateProperty(MPDBindingConstants.UNIQUE_ID, uniquePropVal); + + updateStatus(ThingStatus.UNKNOWN); + connection.start(config.getIpAddress(), config.getPort(), config.getPassword(), + "OH-binding-" + getThing().getUID().getAsString()); + } + + @Override + public void dispose() { + ScheduledFuture future = this.futureUpdateStatus; + if (future != null) { + future.cancel(true); + } + + future = this.futureUpdateCurrentSong; + if (future != null) { + future.cancel(true); + } + + connection.dispose(); + super.dispose(); + } + + @Override + public Collection> getServices() { + return Collections.singleton(MPDActions.class); + } + + /** + * send a command to the music player daemon + * + * @param command command to send + * @param parameter parameter of command + */ + public void sendCommand(@Nullable String command, String... parameter) { + if (command != null) { + connection.sendCommand(command, parameter); + } else { + logger.warn("can't send null command"); + } + } + + private void handleCommandRefresh(String channelId) { + stateMap.remove(channelId); + switch (channelId) { + case CHANNEL_CONTROL: + case CHANNEL_STOP: + case CHANNEL_VOLUME: + scheduleUpdateStatus(); + break; + case CHANNEL_CURRENT_ALBUM: + case CHANNEL_CURRENT_ARTIST: + case CHANNEL_CURRENT_NAME: + case CHANNEL_CURRENT_SONG: + case CHANNEL_CURRENT_SONG_ID: + case CHANNEL_CURRENT_TITLE: + case CHANNEL_CURRENT_TRACK: + scheduleUpdateCurrentSong(); + break; + } + } + + private synchronized void scheduleUpdateStatus() { + logger.debug("scheduleUpdateStatus"); + ScheduledFuture future = this.futureUpdateStatus; + if (future == null || future.isCancelled() || future.isDone()) { + this.futureUpdateStatus = scheduler.schedule(this::doUpdateStatus, 100, TimeUnit.MILLISECONDS); + } + } + + private void doUpdateStatus() { + connection.updateStatus(); + } + + private synchronized void scheduleUpdateCurrentSong() { + logger.debug("scheduleUpdateCurrentSong"); + ScheduledFuture future = this.futureUpdateCurrentSong; + if (future == null || future.isCancelled() || future.isDone()) { + this.futureUpdateCurrentSong = scheduler.schedule(this::doUpdateCurrentSong, 100, TimeUnit.MILLISECONDS); + } + } + + private void doUpdateCurrentSong() { + connection.updateCurrentSong(); + } + + private void handlePlayerCommand(String channelId, Command command) { + switch (channelId) { + case CHANNEL_CONTROL: + handleCommandControl(command); + break; + case CHANNEL_STOP: + handleCommandStop(command); + break; + case CHANNEL_VOLUME: + handleCommandVolume(command); + break; + } + } + + private void handleCommandControl(Command command) { + if (command instanceof PlayPauseType) { + if (command == PlayPauseType.PLAY) { + connection.play(); + } else if (command == PlayPauseType.PAUSE) { + connection.pause(); + } + } else if (command instanceof NextPreviousType) { + if (command == NextPreviousType.NEXT) { + connection.playNext(); + } else if (command == NextPreviousType.PREVIOUS) { + connection.playPrevious(); + } + } else { + // Rewind and Fast Forward are currently not implemented by the binding + logger.debug("Control command {} is not supported", command); + } + } + + private void handleCommandStop(Command command) { + if (command instanceof OnOffType) { + if (command == OnOffType.ON) { + connection.stop(); + } else if (command == OnOffType.OFF) { + connection.play(); + } + } else { + logger.debug("Stop Command {} is not supported", command); + return; + } + } + + private void handleCommandVolume(Command command) { + int newValue = 0; + if (command instanceof IncreaseDecreaseType) { + if (command == IncreaseDecreaseType.INCREASE) { + newValue = Math.min(100, volume + 1); + } else if (command == IncreaseDecreaseType.DECREASE) { + newValue = Math.max(0, volume - 1); + } + } else if (command instanceof OnOffType) { + if (command == OnOffType.ON) { + newValue = 100; + } else if (command == OnOffType.OFF) { + newValue = 0; + } + } else if (command instanceof DecimalType) { + newValue = ((DecimalType) command).intValue(); + } else if (command instanceof PercentType) { + newValue = ((PercentType) command).intValue(); + } else { + logger.debug("Command {} is not supported to change volume", command); + return; + } + + connection.setVolume(newValue); + } + + private void updateChannel(String channelID, State state) { + State previousState = stateMap.put(channelID, state); + if (previousState == null || !previousState.equals(state)) { + updateState(channelID, state); + } + } + + @Override + public void updateMPDStatus(MPDStatus status) { + volume = status.getVolume(); + updateChannel(CHANNEL_VOLUME, new PercentType(status.getVolume())); + + State newControlState = UnDefType.UNDEF; + switch (status.getState()) { + case PLAY: + newControlState = PlayPauseType.PLAY; + break; + case STOP: + case PAUSE: + newControlState = PlayPauseType.PAUSE; + break; + } + updateChannel(CHANNEL_CONTROL, newControlState); + + State newStopState = OnOffType.OFF; + if (status.getState() == MPDStatus.State.STOP) { + newStopState = OnOffType.ON; + } + updateChannel(CHANNEL_STOP, newStopState); + } + + @Override + public void updateMPDSong(MPDSong song) { + updateChannel(CHANNEL_CURRENT_ALBUM, new StringType(song.getAlbum())); + updateChannel(CHANNEL_CURRENT_ARTIST, new StringType(song.getArtist())); + updateChannel(CHANNEL_CURRENT_NAME, new StringType(song.getName())); + updateChannel(CHANNEL_CURRENT_SONG, new DecimalType(song.getSong())); + updateChannel(CHANNEL_CURRENT_SONG_ID, new DecimalType(song.getSongId())); + updateChannel(CHANNEL_CURRENT_TITLE, new StringType(song.getTitle())); + updateChannel(CHANNEL_CURRENT_TRACK, new DecimalType(song.getTrack())); + } + + @Override + public void updateThingStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { + updateStatus(status, statusDetail, description); + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDCommand.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDCommand.java new file mode 100644 index 0000000000000..7d20a71318144 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDCommand.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal.protocol; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class for encapsulating an MPD command + * + * @author Stefan Röllin - Initial contribution + */ + +@NonNullByDefault +public class MPDCommand { + + private final String command; + private final List parameters = new ArrayList<>(); + + /** + * Create an MPD command without parameters + * + * @param command the command to send + */ + public MPDCommand(String command) { + this.command = command; + } + + /** + * Create an MPD command with one Integer parameter + * + * @param command the command to send + * @param value parameter of the command + */ + public MPDCommand(String command, Integer value) { + this.command = command; + parameters.add(Integer.toString(value)); + } + + /** + * Create an MPD command with parameters + * + * @param command the command to send + * @param parameters the parameters of the command to send + */ + public MPDCommand(String command, String... parameters) { + this.command = command; + Collections.addAll(this.parameters, Arrays.copyOf(parameters, parameters.length)); + } + + /** + * Returns the command. + * + * @return the command + */ + public String getCommand() { + return command; + } + + /** + * Returns the command as one line, including the parameters + * + * @return the command and parameters + */ + public String asLine() { + StringBuilder builder = new StringBuilder(command); + + for (String param : parameters) { + builder.append(" "); + builder.append("\""); + builder.append(param.replaceAll("\"", "\\\\\"").replaceAll("'", "\\\\'")); + builder.append("\""); + } + + return builder.toString(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(command); + + for (String param : parameters) { + + builder.append(" "); + builder.append("\""); + if ("password".equals(command)) { + builder.append(param.replaceAll(".", ".")); + } else { + builder.append(param.replaceAll("\"", "\\\\\"").replaceAll("'", "\\\\'")); + } + builder.append("\""); + } + + return builder.toString(); + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDConnection.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDConnection.java new file mode 100644 index 0000000000000..4008867723a93 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDConnection.java @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.openhab.binding.mpd.internal.handler.MPDEventListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class for communicating with the music player daemon through a IP connection + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public class MPDConnection implements MPDResponseListener { + + private static final int DISPOSE_TIMEOUT_MS = 1000; + + private final Logger logger = LoggerFactory.getLogger(MPDConnection.class); + + private final MPDEventListener listener; + + private @Nullable MPDConnectionThread connectionThread = null; + + /** + * Constructor + * + * @param address the IP address of the music player daemon + * @param port the TCP port to be used + * @param password the password to connect to the music player daemon + */ + public MPDConnection(MPDEventListener listener) { + this.listener = listener; + } + + /** + * start the connection + * + * @param address the IP address of the music player daemon + * @param port the TCP port to be used + * @param password the password to connect to the music player daemon + * @param threadName the name of the thread + */ + public void start(String address, Integer port, String password, String threadName) { + if (connectionThread == null) { + final MPDConnectionThread connectionThread = new MPDConnectionThread(this, address, port, password); + connectionThread.setName(threadName); + connectionThread.start(); + this.connectionThread = connectionThread; + } + } + + /** + * dispose the connection + */ + public void dispose() { + final MPDConnectionThread connectionThread = this.connectionThread; + if (connectionThread != null) { + connectionThread.dispose(); + connectionThread.interrupt(); + try { + connectionThread.join(DISPOSE_TIMEOUT_MS); + } catch (InterruptedException ignore) { + } + this.connectionThread = null; + } + } + + /** + * send a command to the music player daemon + * + * @param command command to send + * @param parameter parameter of command + */ + public void sendCommand(String command, String... parameter) { + addCommand(new MPDCommand(command, parameter)); + } + + /** + * play + */ + public void play() { + sendCommand("play"); + } + + /** + * pause the music player daemon + */ + public void pause() { + addCommand(new MPDCommand("pause", 1)); + } + + /** + * play next track + */ + public void playNext() { + sendCommand("next"); + } + + /** + * play previous track + */ + public void playPrevious() { + sendCommand("previous"); + } + + /** + * stop the music player daemon + */ + public void stop() { + sendCommand("stop"); + } + + /** + * update status + */ + public void updateStatus() { + sendCommand("status"); + } + + /** + * update information regarding current song + */ + public void updateCurrentSong() { + sendCommand("currentsong"); + } + + /** + * set volume + * + * @param volume set new volume + */ + public void setVolume(int volume) { + addCommand(new MPDCommand("setvol", volume)); + } + + private void addCommand(MPDCommand command) { + MPDConnectionThread connectionThread = this.connectionThread; + if (connectionThread != null) { + connectionThread.addCommand(command); + } else { + logger.debug("could not add command {} since thing offline", command.getCommand()); + } + } + + @Override + public void updateThingStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String cause) { + listener.updateThingStatus(status, statusDetail, cause); + } + + @Override + public void onResponse(MPDResponse response) { + switch (response.getCommand()) { + case "idle": + handleResponseIdle(response); + break; + case "status": + handleResponseStatus(response); + break; + case "currentsong": + handleResponseCurrentSong(response); + break; + default: + break; + } + } + + private void handleResponseCurrentSong(MPDResponse response) { + MPDSong song = new MPDSong(response); + listener.updateMPDSong(song); + } + + private void handleResponseIdle(MPDResponse response) { + boolean updateStatus = false; + boolean updateCurrentSong = false; + for (String line : response.getLines()) { + if (line.startsWith("changed:")) { + line = line.substring(8).trim(); + switch (line) { + case "player": + updateStatus = true; + break; + case "mixer": + updateStatus = true; + break; + case "playlist": + updateCurrentSong = true; + break; + } + } + } + + if (updateStatus) { + updateStatus(); + } + if (updateCurrentSong) { + updateCurrentSong(); + } + } + + private void handleResponseStatus(MPDResponse response) { + MPDStatus song = new MPDStatus(response); + listener.updateMPDStatus(song); + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDConnectionThread.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDConnectionThread.java new file mode 100644 index 0000000000000..f43d734043bce --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDConnectionThread.java @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal.protocol; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.openhab.binding.mpd.internal.MPDException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class for communicating with the music player daemon through a IP connection + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public class MPDConnectionThread extends Thread { + + private static final int RECONNECTION_TIMEOUT_SEC = 60; + + private final Logger logger = LoggerFactory.getLogger(MPDConnectionThread.class); + + private final MPDResponseListener listener; + + private final String address; + private final Integer port; + private final String password; + + private @Nullable Socket socket = null; + private @Nullable InputStreamReader inputStreamReader = null; + private @Nullable BufferedReader reader = null; + + private final List pendingCommands = new ArrayList<>(); + private AtomicBoolean isInIdle = new AtomicBoolean(false); + private AtomicBoolean disposed = new AtomicBoolean(false); + + public MPDConnectionThread(MPDResponseListener listener, String address, Integer port, String password) { + this.listener = listener; + this.address = address; + this.port = port; + this.password = password; + setDaemon(true); + } + + @Override + public void run() { + try { + while (!disposed.get()) { + try { + synchronized (pendingCommands) { + pendingCommands.clear(); + pendingCommands.add(new MPDCommand("status")); + pendingCommands.add(new MPDCommand("currentsong")); + } + + establishConnection(); + updateThingStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null); + + processPendingCommands(); + } catch (UnknownHostException e) { + updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown host " + address); + } catch (IOException e) { + updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (MPDException e) { + updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } + + isInIdle.set(false); + closeSocket(); + + if (!disposed.get()) { + sleep(RECONNECTION_TIMEOUT_SEC * 1000); + } + } + } catch (InterruptedException ignore) { + } + } + + /** + * dispose the connection + */ + public void dispose() { + disposed.set(true); + Socket socket = this.socket; + if (socket != null) { + try { + socket.close(); + } catch (IOException ignore) { + } + this.socket = null; + } + } + + /** + * add a command to the pending commands queue + * + * @param command command to add + */ + public void addCommand(MPDCommand command) { + insertCommand(command, -1); + } + + private void insertCommand(MPDCommand command, int position) { + logger.debug("insert command '{}' at position {}", command.getCommand(), position); + int index = position; + synchronized (pendingCommands) { + if (index < 0) { + index = pendingCommands.size(); + } + pendingCommands.add(index, command); + sendNoIdleIfInIdle(); + } + } + + private void updateThingStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { + if (!disposed.get()) { + listener.updateThingStatus(status, statusDetail, description); + } + } + + private void sendNoIdleIfInIdle() { + if (isInIdle.compareAndSet(true, false)) { + try { + sendCommand(new MPDCommand("noidle")); + } catch (IOException e) { + logger.debug("sendCommand(noidle) failed", e); + } + } + } + + private void establishConnection() throws UnknownHostException, IOException, MPDException { + openSocket(); + + MPDCommand currentCommand = new MPDCommand("connect"); + MPDResponse response = readResponse(currentCommand); + + if (!response.isOk()) { + throw new MPDException("Failed to connect to " + this.address + ":" + this.port); + } + + if (!password.isEmpty()) { + currentCommand = new MPDCommand("password", password); + sendCommand(currentCommand); + response = readResponse(currentCommand); + if (!response.isOk()) { + throw new MPDException("Could not authenticate, please validate your password"); + } + } + } + + private void openSocket() throws UnknownHostException, IOException, MPDException { + logger.debug("opening connection to {} port {}", address, port); + + if (address.isEmpty()) { + throw new MPDException("The parameter 'ipAddress' is missing."); + } + if (port < 1 || port > 65335) { + throw new MPDException("The parameter 'port' has an invalid value."); + } + + Socket socket = new Socket(address, port); + + inputStreamReader = new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8); + reader = new BufferedReader(inputStreamReader); + + this.socket = socket; + } + + private void processPendingCommands() throws IOException, MPDException { + MPDCommand currentCommand; + + while (!disposed.get()) { + synchronized (pendingCommands) { + if (!pendingCommands.isEmpty()) { + currentCommand = pendingCommands.remove(0); + } else { + currentCommand = new MPDCommand("idle"); + } + + sendCommand(currentCommand); + if ("idle".equals(currentCommand.getCommand())) { + isInIdle.set(true); + } + } + + MPDResponse response = readResponse(currentCommand); + if (!response.isOk()) { + insertCommand(new MPDCommand("clearerror"), 0); + } + listener.onResponse(response); + } + } + + private void closeSocket() { + logger.debug("Closing socket"); + BufferedReader reader = this.reader; + if (reader != null) { + try { + reader.close(); + } catch (IOException ignore) { + } + this.reader = null; + } + + InputStreamReader inputStreamReader = this.inputStreamReader; + if (inputStreamReader != null) { + try { + inputStreamReader.close(); + } catch (IOException ignore) { + } + this.inputStreamReader = null; + } + + Socket socket = this.socket; + if (socket != null) { + try { + socket.close(); + } catch (IOException ignore) { + } + this.socket = null; + } + } + + private void sendCommand(MPDCommand command) throws IOException { + logger.trace("send command '{}'", command); + final Socket socket = this.socket; + if (socket != null) { + String line = command.asLine(); + socket.getOutputStream().write(line.getBytes(StandardCharsets.UTF_8)); + socket.getOutputStream().write('\n'); + } else { + throw new IOException("Connection closed unexpectedly."); + } + } + + private MPDResponse readResponse(MPDCommand command) throws IOException, MPDException { + logger.trace("read response for command '{}'", command.getCommand()); + MPDResponse response = new MPDResponse(command.getCommand()); + boolean done = false; + + final BufferedReader reader = this.reader; + if (reader != null) { + while (!done) { + String line = reader.readLine(); + logger.trace("received line '{}'", line); + + if (line != null) { + if (line.startsWith("ACK [4")) { + logger.warn("command '{}' failed with permission error '{}'", command, line); + isInIdle.set(false); + throw new MPDException( + "Please validate your password and/or your permissions on the Music Player Daemon."); + } else if (line.startsWith("ACK")) { + logger.warn("command '{}' failed with '{}'", command, line); + response.setFailed(); + done = true; + } else if (line.startsWith("OK")) { + done = true; + } else { + response.addLine(line.trim()); + } + } else { + isInIdle.set(false); + throw new IOException("Communication failed unexpectedly."); + } + } + } else { + isInIdle.set(false); + throw new IOException("Connection closed unexpectedly."); + } + + isInIdle.set(false); + + return response; + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDResponse.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDResponse.java new file mode 100644 index 0000000000000..23dd4885cae13 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDResponse.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal.protocol; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class for encapsulating an MPD response + * + * @author Stefan Röllin - Initial contribution + */ + +@NonNullByDefault +public class MPDResponse { + private final String command; + private final List lines = new ArrayList<>(); + private boolean failed = false; + + public MPDResponse(String command) { + this.command = command; + } + + public void addLine(String line) { + lines.add(line); + } + + public String getCommand() { + return command; + } + + public List getLines() { + return lines; + } + + public boolean isOk() { + return !failed; + } + + public void setFailed() { + failed = true; + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDResponseListener.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDResponseListener.java new file mode 100644 index 0000000000000..b4159642c6068 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDResponseListener.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.mpd.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; + +/** + * Listener for responses from Music Player Daemon. + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public interface MPDResponseListener { + void updateThingStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description); + + void onResponse(MPDResponse response); +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDResponseParser.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDResponseParser.java new file mode 100644 index 0000000000000..fae197b400487 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDResponseParser.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal.protocol; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class for parsing a response from a Music Player Daemon. + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public class MPDResponseParser { + + static Map responseToMap(MPDResponse response) { + Map map = new HashMap(); + + for (String line : response.getLines()) { + int offset = line.indexOf(':'); + if (offset >= 0) { + String key = line.substring(0, offset); + String value = line.substring(offset + 1).trim(); + + map.put(key, value); + } + } + + return map; + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDSong.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDSong.java new file mode 100644 index 0000000000000..c9453ba257a67 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDSong.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal.protocol; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class for representing a song. + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public class MPDSong { + + private final Logger logger = LoggerFactory.getLogger(MPDSong.class); + + private final String filename; + private final String album; + private final String artist; + private final String name; + private final int song; + private final int songId; + private final String title; + private final int track; + + public MPDSong(MPDResponse response) { + Map values = MPDResponseParser.responseToMap(response); + filename = values.getOrDefault("file", ""); + album = values.getOrDefault("Album", ""); + artist = values.getOrDefault("Artist", ""); + name = values.getOrDefault("Name", ""); + song = parseInteger(values.getOrDefault("Pos", "0"), 0); + songId = parseInteger(values.getOrDefault("Id", "0"), 0); + title = values.getOrDefault("Title", ""); + track = parseInteger(values.getOrDefault("Track", "-1"), -1); + } + + public String getFilename() { + return filename; + } + + public String getAlbum() { + return album; + } + + public String getArtist() { + return artist; + } + + public String getName() { + return name; + } + + public int getSong() { + return song; + } + + public int getSongId() { + return songId; + } + + public String getTitle() { + return title; + } + + public int getTrack() { + return track; + } + + private int parseInteger(String value, int aDefault) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + logger.debug("parseInt of {} failed", value); + } + return aDefault; + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDStatus.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDStatus.java new file mode 100644 index 0000000000000..f014e1904c1f6 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDStatus.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mpd.internal.protocol; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class for representing the status of a Music Player Daemon. + * + * @author Stefan Röllin - Initial contribution + */ +@NonNullByDefault +public class MPDStatus { + + public enum State { + PLAY, + PAUSE, + STOP + } + + private final Logger logger = LoggerFactory.getLogger(MPDStatus.class); + + private final State state; + private final int volume; + + public MPDStatus(MPDResponse response) { + Map values = MPDResponseParser.responseToMap(response); + state = parseState(values.getOrDefault("state", "")); + volume = parseVolume(values.getOrDefault("volume", "0")); + } + + public State getState() { + return state; + } + + public int getVolume() { + return volume; + } + + private State parseState(String value) { + switch (value) { + case "play": + return State.PLAY; + case "pause": + return State.PAUSE; + case "stop": + return State.STOP; + } + + return State.STOP; + } + + private int parseVolume(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + logger.debug("parseVolume of {} failed", value); + } + return 0; + } +} diff --git a/bundles/org.openhab.binding.mpd/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.mpd/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..9090f6c99ad63 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + MPD Binding + This is the binding for the Music Player Daemon. + Stefan Röllin + + diff --git a/bundles/org.openhab.binding.mpd/src/main/resources/ESH-INF/i18n/mpd_de.properties b/bundles/org.openhab.binding.mpd/src/main/resources/ESH-INF/i18n/mpd_de.properties new file mode 100644 index 0000000000000..13c548d821f0c --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/resources/ESH-INF/i18n/mpd_de.properties @@ -0,0 +1,28 @@ +binding.mpd.name = MPD Binding +binding.mpd.description = Das MPD Binding erlaubt es Music Player Daemons zu steuern. + +# thing types +thing-type.mpd.mpd.label = Music Player Daemon +thing-type.mpd.mpd.description = Music Player Daemon + +# thing type config description +thing-type.config.mpd.mpd.ipAddress.label = IP-Adresse +thing-type.config.mpd.mpd.ipAddress.description = Lokale IP-Adresse oder Hostname des Music Player Daemons. +thing-type.config.mpd.mpd.port.label = Port +thing-type.config.mpd.mpd.port.description = Port des Music Player Daemons. +thing-type.config.mpd.mpd.password.label = Passwort +thing-type.config.mpd.mpd.password.description = Passwort zur Authentifizierung am Music Player Daemon. + +# channel types +channel-type.mpd.currentalbum.label = Album +channel-type.mpd.currentalbum.description = Zeigt das Album des aktuellen Stücks an. +channel-type.mpd.currentname.label = Aktueller Name +channel-type.mpd.currentname.description = Name des aktuellen Stücks. Entspricht nicht dem Namen. Die genaue Bedeutung dieses Tags ist nicht genau definiert. Es wird häufig von schlecht konfigurierten Internetradiosendern mit defekten Tags verwendet, um sowohl den Künstlernamen als auch den Songtitel in einem Tag zusammenzufassen. +channel-type.mpd.currentsong.label = Aktuelle Song Nummer +channel-type.mpd.currentsong.description = Nummer des aktuellen Stücks. +channel-type.mpd.currentsongid.label = Aktuelle Song Id +channel-type.mpd.currentsongid.description = Id des aktuellen Stücks. +channel-type.mpd.currenttrack.label = Track Nummer +channel-type.mpd.currenttrack.description = Zeigt die Nummer des aktuellen Tracks. +channel-type.mpd.stop.label = Stop +channel-type.mpd.stop.description = Ermöglicht das Stoppen der Wiedergabe. diff --git a/bundles/org.openhab.binding.mpd/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mpd/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..9da31b97b7922 --- /dev/null +++ b/bundles/org.openhab.binding.mpd/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,89 @@ + + + + + + + Music Player Daemon Binding + + + + + + + + + + + + + + uniqueId + + + + + The IP or host name of the Music Player Daemon. + network-address + + + + Port for the Music Player Daemon + 6600 + + + + Password to access the Music Player Daemon. + true + password + + + + + + + String + + Name of the album currently playing. + + + + + String + + Name for current song. This is not the song title. The exact meaning of this tag is not well-defined. It + is often used by badly configured internet radio stations with broken tags to squeeze both the artist name and the + song title in one tag. + + + + + Number + + The current track number. + + + + + Number + + The current song number. + + + + + Number + + The current song id. + + + + + Switch + + Stop the Music Player Daemon. ON if the player is stopped. + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index f20f9ddd5fcde..66fcb341016ee 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -163,6 +163,7 @@ org.openhab.binding.minecraft org.openhab.binding.modbus org.openhab.binding.modbus.sunspec + org.openhab.binding.mpd org.openhab.binding.mqtt org.openhab.binding.mqtt.generic org.openhab.binding.mqtt.homeassistant