Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@
/bundles/org.openhab.binding.lgtvserial/ @fa2k
/bundles/org.openhab.binding.lgwebos/ @sprehn
/bundles/org.openhab.binding.lifx/ @wborn
/bundles/org.openhab.binding.linkplay/ @digitaldan
/bundles/org.openhab.binding.linktap/ @dag81
/bundles/org.openhab.binding.linky/ @clinique @lolodomo
/bundles/org.openhab.binding.linuxinput/ @t-8ch
Expand Down
5 changes: 5 additions & 0 deletions bom/openhab-addons/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,11 @@
<artifactId>org.openhab.binding.lifx</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.linkplay</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.linktap</artifactId>
Expand Down
13 changes: 13 additions & 0 deletions bundles/org.openhab.binding.linkplay/NOTICE
Original file line number Diff line number Diff line change
@@ -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
428 changes: 428 additions & 0 deletions bundles/org.openhab.binding.linkplay/README.md

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions bundles/org.openhab.binding.linkplay/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>5.1.0-SNAPSHOT</version>
</parent>

<artifactId>org.openhab.binding.linkplay</artifactId>

<name>openHAB Add-ons :: Bundles :: LinkPlay Binding</name>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.linkplay-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>

<feature name="openhab-binding-linkplay" description="LinkPlay Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-upnp</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.linkplay/${project.version}</bundle>
</feature>
</features>
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
* Copyright (c) 2010-2025 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.linkplay.internal;

import java.io.IOException;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSinkSync;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.StreamServed;
import org.openhab.core.audio.URLAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.UnsupportedAudioStreamException;
import org.openhab.core.library.types.PercentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Audio sink for LinkPlay players that uses the HTTP audio servlet to serve the audio stream.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class LinkPlayAudioSink extends AudioSinkSync {

private final Logger logger = LoggerFactory.getLogger(LinkPlayAudioSink.class);

private static final Set<Class<? extends AudioStream>> SUPPORTED_STREAMS = Set.of(AudioStream.class);
protected LinkPlayHandler handler;
protected AudioHTTPServer audioHTTPServer;
protected @Nullable String callbackUrl;

public LinkPlayAudioSink(LinkPlayHandler handler, AudioHTTPServer audioHTTPServer, @Nullable String callbackUrl) {
this.handler = handler;
this.audioHTTPServer = audioHTTPServer;
this.callbackUrl = callbackUrl;
}

@Override
public String getId() {
return handler.getThing().getUID().toString();
}

@Override
public @Nullable String getLabel(@Nullable Locale locale) {
return handler.getThing().getLabel();
}

@Override
public Set<AudioFormat> getSupportedFormats() {
return Set.of(AudioFormat.WAV, AudioFormat.MP3, AudioFormat.OGG, AudioFormat.AAC);
}

@Override
public Set<Class<? extends AudioStream>> getSupportedStreams() {
return SUPPORTED_STREAMS;
}

@Override
public PercentType getVolume() throws IOException {
if (handler.getState(LinkPlayBindingConstants.GROUP_PLAYBACK,
LinkPlayBindingConstants.CHANNEL_VOLUME) instanceof PercentType volume) {
return volume;
}
return new PercentType(0);
}

@Override
public void setVolume(@Nullable PercentType volume) throws IOException {
if (volume != null) {
try {
handler.setVolume(volume);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IOException("Error while setting volume: " + e.getMessage(), e);
}
}
}

@Override
public void process(@Nullable AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
logger.debug("{}: process: {}", handler.getThing().getUID(), audioStream);
if (audioStream instanceof URLAudioStream) {
processAsynchronously(audioStream);
} else {
processSynchronously(audioStream);
}
}

@Override
public CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) {
logger.debug("{}: processAndComplete: {}", handler.getThing().getUID(), audioStream);
if (audioStream instanceof URLAudioStream) {
// Asynchronous handling for URLAudioStream
CompletableFuture<@Nullable Void> completableFuture = new CompletableFuture<@Nullable Void>();
try {
processAsynchronously(audioStream);
} catch (UnsupportedAudioFormatException | UnsupportedAudioStreamException e) {
completableFuture.completeExceptionally(e);
}
return completableFuture;
} else {
return super.processAndComplete(audioStream);
}
}

@Override
protected void processSynchronously(@Nullable AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
logger.debug("{}: processSynchronously: {}", handler.getThing().getUID(), audioStream);
if (audioStream instanceof URLAudioStream) {
return;
}

if (audioStream == null) {
// in case the audioStream is null, this should be interpreted as a request to end any currently playing
// stream.
logger.trace("Stop currently playing stream.");
try {
handler.stopPlaying();
} catch (InterruptedException | ExecutionException | TimeoutException e) {
logger.debug("Error while stopping media: {}", e.getMessage(), e);
}
return;
}

String callbackUrl = this.callbackUrl;
if (callbackUrl != null) {
StreamServed streamServed;
try {
streamServed = audioHTTPServer.serve(audioStream, 10, true);
logger.debug("Audio stream accessible through HTTP served at {} for playback", streamServed.url());
} catch (IOException e) {
try {
audioStream.close();
} catch (IOException ex) {
}
throw new UnsupportedAudioStreamException(
"Was not able to handle the audio stream (cache on disk failed).", audioStream.getClass(), e);
}
String url = callbackUrl + streamServed.url();
try {
playMedia(url).get();
} catch (InterruptedException | ExecutionException e) {
logger.debug("Error while playing notification: {}", e.getMessage(), e);
}
} else {
logger.warn("We do not have any callback url, so cannot play the audio stream!");
try {
audioStream.close();
} catch (IOException e) {
}
}
}

private void processAsynchronously(@Nullable AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
logger.debug("{}: processAsynchronously: {}", handler.getThing().getUID(), audioStream);
if (audioStream instanceof URLAudioStream urlAudioStream) {
// it is an external URL, the speaker can access it itself and play it.
playMedia(urlAudioStream.getURL());
try {
audioStream.close();
} catch (IOException e) {
}
}
}

protected CompletableFuture<@Nullable Void> playMedia(String url) {
String newUrl = url;
if (!url.startsWith("x-") && !url.startsWith("http")) {
newUrl = "x-file-cifs:" + url;
}
return handler.playNotification(newUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (c) 2010-2025 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.linkplay.internal;

import java.util.Collections;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;

/**
* The {@link LinkPlayBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class LinkPlayBindingConstants {

private static final String BINDING_ID = "linkplay";

// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_PLAYER = new ThingTypeUID(BINDING_ID, "player");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_PLAYER);

public static final String PROPERTY_FIRMWARE = "firmwareVersion";
public static final String PROPERTY_MODEL = "model";
public static final String PROPERTY_IP = "ipAddress";
public static final String PROPERTY_MAC = "macAddress";
public static final String PROPERTY_MANUFACTURER = "manufacturer";
public static final String PROPERTY_DEVICE_NAME = "deviceName";
public static final String PROPERTY_GROUP_NAME = "groupName";
public static final String PROPERTY_IP_ADDRESS = "ipAddress";
public static final String PROPERTY_PORT = "port";
public static final String CONFIG_UDN = "udn";
public static final String CONFIG_REFRESH_INTERVAL = "refreshInterval";

// ----------------- Channel Group IDs -----------------
public static final String GROUP_PLAYBACK = "playback";
public static final String GROUP_METADATA = "metadata";
public static final String GROUP_INPUT = "input";
public static final String GROUP_EQUALISER = "equalizer";
public static final String GROUP_MULTIROOM = "multiroom";
public static final String GROUP_DEVICE = "device";
public static final String GROUP_PRESETS = "presets";
public static final String GROUP_PRESET = "preset";

// ----------------- Channel IDs -----------------
public static final String CHANNEL_PLAYER_CONTROL = "control";
public static final String CHANNEL_PLAYBACK_STATE = "state";
public static final String CHANNEL_TRACK_POSITION = "position";
public static final String CHANNEL_TRACK_DURATION = "duration";
public static final String CHANNEL_REPEAT_SHUFFLE_MODE = "repeat-shuffle-mode";
public static final String CHANNEL_EQ_PRESET = "eq-preset";
public static final String CHANNEL_VOLUME = "volume";
public static final String CHANNEL_MUTE = "mute";

// Metadata
public static final String CHANNEL_TRACK_TITLE = "track-title";
public static final String CHANNEL_TRACK_ARTIST = "track-artist";
public static final String CHANNEL_TRACK_ALBUM = "track-album";
public static final String CHANNEL_TRACK_URI = "track-uri";
public static final String CHANNEL_TRACK_SOURCE = "track-source";
public static final String CHANNEL_ALBUM_ART_URL = "album-art-url";
public static final String CHANNEL_ALBUM_ART = "album-art";
public static final String CHANNEL_SAMPLE_RATE = "sample-rate";
public static final String CHANNEL_BIT_DEPTH = "bit-depth";
public static final String CHANNEL_BIT_RATE = "bit-rate";
public static final String CHANNEL_CURRENT_PLAYLIST_NAME = "current-playlist-name";

// Input / Source
public static final String CHANNEL_SOURCE_INPUT = "source";
public static final String CHANNEL_BT_CONNECTED = "bluetooth-connected";
public static final String CHANNEL_BT_PAIRED_DEVICE = "bluetooth-paired-device";
public static final String CHANNEL_LINE_IN_ACTIVE = "line-in-active";

// Equaliser & Output
public static final String CHANNEL_EQ_ENABLED = "enabled";
public static final String CHANNEL_EQ_BAND = "band";
public static final String CHANNEL_OUTPUT_HW_MODE = "output-hardware-mode";
public static final String CHANNEL_CHANNEL_BALANCE = "channel-balance";
public static final String CHANNEL_SPDIF_DELAY = "spdif-switch-delay-ms";

// Multi-room
public static final String CHANNEL_MULTIROOM_VOLUME = "volume";
public static final String CHANNEL_MULTIROOM_MUTE = "mute";
public static final String CHANNEL_MULTIROOM_ACTIVE = "active";
public static final String CHANNEL_MULTIROOM_LEADER = "leader";
public static final String CHANNEL_MULTIROOM_JOIN = "join";
public static final String CHANNEL_MULTIROOM_LEAVE = "leave";
public static final String CHANNEL_MULTIROOM_MANAGE = "manage";
public static final String CHANNEL_MULTIROOM_UNGROUP = "ungroup";

// Device & System
public static final String CHANNEL_LED_ENABLED = "led-enabled";
public static final String CHANNEL_TOUCH_KEYS_ENABLED = "touch-keys-enabled";
public static final String CHANNEL_SHUTDOWN_TIMER = "shutdown-timer";
public static final String CHANNEL_REBOOT = "reboot";
public static final String CHANNEL_FACTORY_RESET = "factory-reset";

// Presets
public static final String CHANNEL_PRESET_COUNT = "count";
public static final String CHANNEL_PLAY_PRESET = "play";

// preset instance
public static final String CHANNEL_PRESET_URL = "url";
public static final String CHANNEL_PRESET_SOURCE = "source";
public static final String CHANNEL_PRESET_PIC_URL = "pic-url";
public static final String CHANNEL_PRESET_PIC = "pic";
public static final String CHANNEL_PRESET_PLAY = "play";
public static final String CHANNEL_PRESET_NAME = "name";

// Group proxy channels that are set by the leader when in a group
public static final Set<String> GROUP_PROXY_CHANNELS = Set.of(CHANNEL_PLAYER_CONTROL, CHANNEL_PLAYBACK_STATE,
CHANNEL_TRACK_POSITION, CHANNEL_TRACK_DURATION, CHANNEL_REPEAT_SHUFFLE_MODE, CHANNEL_TRACK_TITLE,
CHANNEL_TRACK_ARTIST, CHANNEL_TRACK_ALBUM, CHANNEL_ALBUM_ART_URL, CHANNEL_ALBUM_ART, CHANNEL_SAMPLE_RATE,
CHANNEL_BIT_DEPTH, CHANNEL_BIT_RATE);
}
Loading