added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.fsinternetradio-${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-fsinternetradio" description="Frontier Silicon Internet Radio 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.fsinternetradio/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,49 @@
/**
* 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.fsinternetradio.internal;
import org.openhab.core.thing.ThingTypeUID;
/**
* This {@link FSInternetRadioBindingConstants} interface defines common constants, which are
* used across the whole binding.
*
* @author Patrick Koenemann - Initial contribution
*/
public interface FSInternetRadioBindingConstants {
String BINDING_ID = "fsinternetradio";
// List of all Thing Type UIDs
ThingTypeUID THING_TYPE_RADIO = new ThingTypeUID(BINDING_ID, "radio");
// List of all Channel ids
String CHANNEL_POWER = "power";
String CHANNEL_PRESET = "preset";
String CHANNEL_VOLUME_PERCENT = "volume-percent";
String CHANNEL_VOLUME_ABSOLUTE = "volume-absolute";
String CHANNEL_MUTE = "mute";
String CHANNEL_PLAY_INFO_NAME = "play-info-name";
String CHANNEL_PLAY_INFO_TEXT = "play-info-text";
String CHANNEL_MODE = "mode";
// config properties
String CONFIG_PROPERTY_IP = "ip";
String CONFIG_PROPERTY_PIN = "pin";
String CONFIG_PROPERTY_PORT = "port";
String CONFIG_PROPERTY_REFRESH = "refresh";
// further properties
String PROPERTY_MANUFACTURER = "manufacturer";
String PROPERTY_MODEL = "model";
}

View File

@@ -0,0 +1,289 @@
/**
* 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.fsinternetradio.internal;
import static org.openhab.binding.fsinternetradio.internal.FSInternetRadioBindingConstants.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.jupnp.model.meta.DeviceDetails;
import org.jupnp.model.meta.ManufacturerDetails;
import org.jupnp.model.meta.ModelDetails;
import org.jupnp.model.meta.RemoteDevice;
import org.jupnp.model.meta.RemoteDeviceIdentity;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is the discovery service for internet radios based on the fontier silicon chipset. Unfortunately, it is not
* easily possible to detect from the upnp information which devices are supported. So currently, discovery only works
* for medion internet radios. {@link FSInternetRadioDiscoveryParticipant#getThingUID(RemoteDevice)} must be extended to
* add further supported devices!
*
* @author Patrick Koenemann - Initial contribution
* @author Mihaela Memova - removed the getLabel(RemoteDevice device) method due to its unreachable code lines
* @author Markus Michels - support for Teufel 3sixty discovery
*/
@Component(immediate = true)
public class FSInternetRadioDiscoveryParticipant implements UpnpDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(FSInternetRadioDiscoveryParticipant.class);
/** Map from UPnP manufacturer to model number for supported radios; filled in static initializer below. */
private static final Map<String, Set<String>> SUPPORTED_RADIO_MODELS = new HashMap<>();
static {
// to allow case-insensitive match: add all values UPPER-CASE!
// format: MANUFACTURER -> MODEL NAME, as shown e.g. by UPnP Tester as explained here:
// https://community.openhab.org/t/internet-radio-i-need-your-help/2131
// list of medion internet radios taken from: http://internetradio.medion.com/
final Set<String> medionRadios = new HashSet<>();
SUPPORTED_RADIO_MODELS.put("MEDION AG", medionRadios);
medionRadios.add("MD83813");
medionRadios.add("MD84017");
medionRadios.add("MD85651");
medionRadios.add("MD86062");
medionRadios.add("MD86250");
medionRadios.add("MD86562");
medionRadios.add("MD86672");
medionRadios.add("MD86698");
medionRadios.add("MD86869");
medionRadios.add("MD86891");
medionRadios.add("MD86955");
medionRadios.add("MD86988");
medionRadios.add("MD87090");
medionRadios.add("MD87180");
medionRadios.add("MD87238");
medionRadios.add("MD87267");
// list of hama internet radios taken from:
// https://www.hama.com/action/searchCtrl/search?searchMode=1&q=Internet%20Radio
final Set<String> hamaRadios = new HashSet<>();
SUPPORTED_RADIO_MODELS.put("HAMA", hamaRadios);
hamaRadios.add("IR100");
hamaRadios.add("IR110");
hamaRadios.add("IR250");
hamaRadios.add("IR320");
hamaRadios.add("DIR3000");
hamaRadios.add("DIR3100");
hamaRadios.add("DIR3110");
// as reported in: https://community.openhab.org/t/internet-radio-i-need-your-help/2131/19
// and: https://community.openhab.org/t/internet-radio-i-need-your-help/2131/20
// and: https://community.openhab.org/t/internet-radio-i-need-your-help/2131/23
// these radios do not provide model number, but the model name should also be ok
final Set<String> radiosWithoutManufacturer = new HashSet<>();
radiosWithoutManufacturer.add(""); // empty manufacturer / model name
radiosWithoutManufacturer.add(null); // missing manufacturer / model name
SUPPORTED_RADIO_MODELS.put("SMRS18A1", radiosWithoutManufacturer);
SUPPORTED_RADIO_MODELS.put("SMRS30A1", radiosWithoutManufacturer);
SUPPORTED_RADIO_MODELS.put("SMRS35A1", radiosWithoutManufacturer);
final Set<String> teufelRadios = new HashSet<>();
SUPPORTED_RADIO_MODELS.put("Teufel", teufelRadios);
teufelRadios.add("Radio 3sixty");
// as reported in: https://community.openhab.org/t/internet-radio-i-need-your-help/2131/5
final Set<String> ttmicroRadios = new HashSet<>();
SUPPORTED_RADIO_MODELS.put("TTMICRO AS", ttmicroRadios);
ttmicroRadios.add("PINELL SUPERSOUND");
// as reported in: https://community.openhab.org/t/internet-radio-i-need-your-help/2131/7
final Set<String> revoRadios = new HashSet<>();
SUPPORTED_RADIO_MODELS.put("REVO TECHNOLOGIES LTD", revoRadios);
revoRadios.add("S10");
// as reported in: https://community.openhab.org/t/internet-radio-i-need-your-help/2131/10
// and: https://community.openhab.org/t/internet-radio-i-need-your-help/2131/21
final Set<String> robertsRadios = new HashSet<>();
SUPPORTED_RADIO_MODELS.put("ROBERTS RADIO LIMITED", robertsRadios);
robertsRadios.add("ROBERTS STREAM 93I");
robertsRadios.add("ROBERTS STREAM 83I");
// as reported in: https://community.openhab.org/t/internet-radio-i-need-your-help/2131/11
final Set<String> aunaRadios = new HashSet<>();
SUPPORTED_RADIO_MODELS.put("AUNA", aunaRadios);
aunaRadios.add("10028154 & 10028155");
aunaRadios.add("10028154");
aunaRadios.add("10028155");
// as reported in: https://community.openhab.org/t/internet-radio-i-need-your-help/2131/22
final Set<String> sangeanRadios = new HashSet<>();
SUPPORTED_RADIO_MODELS.put("SANGEAN RADIO LIMITED", sangeanRadios);
sangeanRadios.add("28");
// as reported in: https://community.openhab.org/t/internet-radio-i-need-your-help/2131/25
final Set<String> rokuRadios = new HashSet<>();
SUPPORTED_RADIO_MODELS.put("ROKU", rokuRadios);
rokuRadios.add("M1001");
}
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Collections.singleton(THING_TYPE_RADIO);
}
@Override
public DiscoveryResult createResult(RemoteDevice device) {
final ThingUID uid = getThingUID(device);
if (uid != null) {
final Map<String, Object> properties = new HashMap<>(1);
final String ip = getIp(device);
if (ip != null) {
properties.put(CONFIG_PROPERTY_IP, ip);
// add manufacturer and model, if provided
final String manufacturer = getManufacturer(device);
if (manufacturer != null) {
properties.put(PROPERTY_MANUFACTURER, manufacturer);
}
final String dm = getModel(device);
final String model = dm != null ? dm : getFriendlyName(device);
if (model != null) {
properties.put(PROPERTY_MODEL, model);
}
final String thingName = (manufacturer == null) && (getModel(device) == null) ? getFriendlyName(device)
: device.getDisplayString();
return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(thingName).build();
}
}
return null;
}
private String getManufacturer(RemoteDevice device) {
final DeviceDetails details = device.getDetails();
if ((details != null) && (details.getManufacturerDetails() != null)) {
String manufacturer = details.getManufacturerDetails().getManufacturer().trim();
return manufacturer.isEmpty() ? null : manufacturer;
}
return null;
}
private String getModel(RemoteDevice device) {
final DeviceDetails details = device.getDetails();
if ((details != null) && (details.getModelDetails().getModelNumber() != null)) {
String model = details.getModelDetails().getModelNumber().trim();
return model.isEmpty() ? null : model;
}
return null;
}
private String getFriendlyName(RemoteDevice device) {
final DeviceDetails details = device.getDetails();
if ((details != null) && (details.getFriendlyName() != null)) {
String name = details.getFriendlyName().trim();
return name.isEmpty() ? null : name;
}
return null;
}
private String getIp(RemoteDevice device) {
final DeviceDetails details = device.getDetails();
if (details != null) {
if (details.getBaseURL() != null) {
return details.getBaseURL().getHost();
}
}
final RemoteDeviceIdentity identity = device.getIdentity();
if (identity != null) {
if (identity.getDescriptorURL() != null) {
return identity.getDescriptorURL().getHost();
}
}
return null;
}
/**
* If <code>device</code> is a supported device, a unique thing ID (e.g. serial number) must be returned. Further
* supported devices should be added here, based on the available UPnP information.
*/
@SuppressWarnings("null")
@Override
public ThingUID getThingUID(RemoteDevice device) {
final DeviceDetails details = device.getDetails();
final String friendlyName = details.getFriendlyName();
logger.debug("Discovered unit: {}", friendlyName);
if (details != null) {
final ManufacturerDetails manufacturerDetails = details.getManufacturerDetails();
final ModelDetails modelDetails = details.getModelDetails();
if (modelDetails != null) {
// check manufacturer and model number
final String manufacturer = manufacturerDetails == null ? null : manufacturerDetails.getManufacturer();
final String modelNumber = modelDetails.getModelNumber();
String serialNumber = details.getSerialNumber();
logger.debug("Discovered unit: {} {} - {}", manufacturer, modelNumber, friendlyName);
if (modelNumber != null) {
if (manufacturer != null) {
final Set<String> supportedRadios = SUPPORTED_RADIO_MODELS
.get(manufacturer.trim().toUpperCase());
if (supportedRadios != null && supportedRadios.contains(modelNumber.toUpperCase())) {
return new ThingUID(THING_TYPE_RADIO, serialNumber);
}
}
// check model name and number
final String modelName = modelDetails.getModelName();
if (modelName != null) {
final Set<String> supportedRadios = SUPPORTED_RADIO_MODELS.get(modelName.trim().toUpperCase());
if (supportedRadios != null && supportedRadios.contains(modelNumber.toUpperCase())) {
return new ThingUID(THING_TYPE_RADIO, serialNumber);
}
// Teufel reports empty manufacturer and model, but friendly name
if (friendlyName.contains("Teufel")) {
logger.debug("haha");
}
if (!friendlyName.isEmpty()) {
for (Set<String> models : SUPPORTED_RADIO_MODELS.values()) {
for (String model : models) {
if ((model != null) && !model.isEmpty() && friendlyName.contains(model)) {
return new ThingUID(THING_TYPE_RADIO, serialNumber);
}
}
}
}
}
}
if (((manufacturer == null) || manufacturer.trim().isEmpty())
&& ((modelNumber == null) || modelNumber.trim().isEmpty())) {
// Some devices report crappy UPnP device description so manufacturer and model are ""
// In this case we try to find the match in friendlyName
final String uname = friendlyName.toUpperCase();
for (Map.Entry<String, Set<String>> entry : SUPPORTED_RADIO_MODELS.entrySet()) {
for (Set<String> set : SUPPORTED_RADIO_MODELS.values()) {
for (String model : set) {
if ((model != null) && !model.isEmpty() && uname.contains(model)) {
return new ThingUID(THING_TYPE_RADIO, serialNumber);
}
}
}
}
}
}
// maybe we can add further indicators, whether the device is a supported one
}
// device not supported
return null;
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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.fsinternetradio.internal;
import static org.openhab.binding.fsinternetradio.internal.FSInternetRadioBindingConstants.THING_TYPE_RADIO;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.fsinternetradio.internal.handler.FSInternetRadioHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link FSInternetRadioHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Patrick Koenemann - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.fsinternetradio")
@NonNullByDefault
public class FSInternetRadioHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_RADIO);
private final HttpClient httpClient;
@Activate
public FSInternetRadioHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@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 (thingTypeUID.equals(THING_TYPE_RADIO)) {
return new FSInternetRadioHandler(thing, httpClient);
}
return null;
}
}

View File

@@ -0,0 +1,246 @@
/**
* 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.fsinternetradio.internal.handler;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.openhab.binding.fsinternetradio.internal.FSInternetRadioBindingConstants.*;
import java.math.BigDecimal;
import java.util.concurrent.ScheduledFuture;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.fsinternetradio.internal.radio.FrontierSiliconRadio;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link FSInternetRadioHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Patrick Koenemann - Initial contribution
* @author Mihaela Memova - removed the unused boolean parameter, changed the check for the PIN
* @author Svilen Valkanov - changed handler initialization
*/
public class FSInternetRadioHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(FSInternetRadioHandler.class);
FrontierSiliconRadio radio;
private final HttpClient httpClient;
/** Job that runs {@link #updateRunnable}. */
private ScheduledFuture<?> updateJob;
/** Runnable for job {@link #updateJob} for periodic refresh. */
private final Runnable updateRunnable = new Runnable() {
@Override
public void run() {
if (!radio.isLoggedIn()) {
// radio is not set, so set all channels to 'undefined'
for (Channel channel : getThing().getChannels()) {
updateState(channel.getUID(), UnDefType.UNDEF);
}
// now let's silently check if it's back online
radioLogin();
return; // if login is successful, this method is called again :-)
}
try {
final boolean radioOn = radio.getPower();
for (Channel channel : getThing().getChannels()) {
if (!radioOn && !CHANNEL_POWER.equals(channel.getUID().getId())) {
// if radio is off, set all channels (except for 'POWER') to 'undefined'
updateState(channel.getUID(), UnDefType.UNDEF);
} else if (isLinked(channel.getUID().getId())) {
// update all channels that are linked
switch (channel.getUID().getId()) {
case CHANNEL_POWER:
updateState(channel.getUID(), radioOn ? OnOffType.ON : OnOffType.OFF);
break;
case CHANNEL_VOLUME_ABSOLUTE:
updateState(channel.getUID(),
DecimalType.valueOf(String.valueOf(radio.getVolumeAbsolute())));
break;
case CHANNEL_VOLUME_PERCENT:
updateState(channel.getUID(),
PercentType.valueOf(String.valueOf(radio.getVolumePercent())));
break;
case CHANNEL_MODE:
updateState(channel.getUID(), DecimalType.valueOf(String.valueOf(radio.getMode())));
break;
case CHANNEL_MUTE:
updateState(channel.getUID(), radio.getMuted() ? OnOffType.ON : OnOffType.OFF);
break;
case CHANNEL_PRESET:
// preset is write-only, ignore
break;
case CHANNEL_PLAY_INFO_NAME:
updateState(channel.getUID(), StringType.valueOf(radio.getPlayInfoName()));
break;
case CHANNEL_PLAY_INFO_TEXT:
updateState(channel.getUID(), StringType.valueOf(radio.getPlayInfoText()));
break;
default:
logger.warn("Ignoring unknown channel during update: {}", channel.getLabel());
}
}
}
updateStatus(ThingStatus.ONLINE); // set it back online, maybe it was offline before
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
};
public FSInternetRadioHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
}
@Override
public void initialize() {
// read configuration
final String ip = (String) getThing().getConfiguration().get(CONFIG_PROPERTY_IP);
final BigDecimal port = (BigDecimal) getThing().getConfiguration().get(CONFIG_PROPERTY_PORT);
final String pin = (String) getThing().getConfiguration().get(CONFIG_PROPERTY_PIN);
if (ip == null || StringUtils.isEmpty(pin) || port.intValue() == 0) {
// configuration incomplete
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration incomplete");
} else {
radio = new FrontierSiliconRadio(ip, port.intValue(), pin, httpClient);
logger.debug("Initializing connection to {}:{}", ip, port);
// Long running initialization should be done asynchronously in background
radioLogin();
// also schedule a thread for polling with configured refresh rate
final BigDecimal period = (BigDecimal) getThing().getConfiguration().get(CONFIG_PROPERTY_REFRESH);
if (period != null && period.intValue() > 0) {
updateJob = scheduler.scheduleWithFixedDelay(updateRunnable, period.intValue(), period.intValue(),
SECONDS);
}
}
}
private void radioLogin() {
scheduler.execute(new Runnable() {
@Override
public void run() {
try {
if (radio.login()) {
// Thing initialized. If done set status to ONLINE to indicate proper working.
updateStatus(ThingStatus.ONLINE);
// now update all channels!
updateRunnable.run();
}
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
});
}
@Override
public void dispose() {
if (updateJob != null) {
updateJob.cancel(true);
}
updateJob = null;
radio = null;
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
if (!radio.isLoggedIn()) {
// connection to radio is not initialized, log ignored command and set status, if it is not already offline
logger.debug("Ignoring command {} = {} because device is offline.", channelUID.getId(), command);
if (ThingStatus.ONLINE.equals(getThing().getStatus())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
return;
}
try {
switch (channelUID.getId()) {
case CHANNEL_POWER:
if (OnOffType.ON.equals(command)) {
radio.setPower(true);
} else if (OnOffType.OFF.equals(command)) {
radio.setPower(false);
}
// now all items should be updated! (wait some seconds so that text items are up-to-date)
scheduler.schedule(updateRunnable, 4, SECONDS);
break;
case CHANNEL_VOLUME_PERCENT:
if (IncreaseDecreaseType.INCREASE.equals(command) || UpDownType.UP.equals(command)) {
radio.increaseVolumeAbsolute();
} else if (IncreaseDecreaseType.DECREASE.equals(command) || UpDownType.DOWN.equals(command)) {
radio.decreaseVolumeAbsolute();
} else if (command instanceof PercentType) {
radio.setVolumePercent(((PercentType) command).intValue());
}
// absolute value should also be updated now, so let's update all items
scheduler.schedule(updateRunnable, 1, SECONDS);
break;
case CHANNEL_VOLUME_ABSOLUTE:
if (IncreaseDecreaseType.INCREASE.equals(command) || UpDownType.UP.equals(command)) {
radio.increaseVolumeAbsolute();
} else if (IncreaseDecreaseType.DECREASE.equals(command) || UpDownType.DOWN.equals(command)) {
radio.decreaseVolumeAbsolute();
} else if (command instanceof DecimalType) {
radio.setVolumeAbsolute(((DecimalType) command).intValue());
}
// percent value should also be updated now, so let's update all items
scheduler.schedule(updateRunnable, 1, SECONDS);
break;
case CHANNEL_MODE:
if (command instanceof DecimalType) {
radio.setMode(((DecimalType) command).intValue());
}
break;
case CHANNEL_PRESET:
if (command instanceof DecimalType) {
radio.setPreset(((DecimalType) command).intValue());
}
break;
case CHANNEL_MUTE:
if (command instanceof OnOffType) {
radio.setMuted(OnOffType.ON.equals(command));
}
break;
default:
logger.warn("Ignoring unknown command: {}", command);
}
// make sure that device state is online
updateStatus(ThingStatus.ONLINE);
} catch (Exception e) {
// set device state to offline
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
}

View File

@@ -0,0 +1,241 @@
/**
* 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.fsinternetradio.internal.radio;
import static org.openhab.binding.fsinternetradio.internal.radio.FrontierSiliconRadioConstants.*;
import java.io.IOException;
import org.eclipse.jetty.client.HttpClient;
/**
* Class representing a internet radio based on the frontier silicon chipset. Tested with "hama IR110" and Medion
* MD87180" internet radios.
*
* @author Rainer Ostendorf
* @author Patrick Koenemann
* @author Mihaela Memova - removed duplicated check for the percent value range
*/
public class FrontierSiliconRadio {
/** The http connection/session used for controlling the radio. */
private final FrontierSiliconRadioConnection conn;
/** the volume of the radio. we cache it for fast increase/decrease. */
private int currentVolume = 0;
/**
* Constructor for the Radio class
*
* @param hostname Host name of the Radio addressed, e.g. "192.168.0.100"
* @param port Port number, default: 80 (http)
* @param pin Access PIN number of the radio. Must be 4 digits, e.g. "1234"
* @param httpClient http client instance to use
*
* @author Rainer Ostendorf
*/
public FrontierSiliconRadio(String hostname, int port, String pin, HttpClient httpClient) {
this.conn = new FrontierSiliconRadioConnection(hostname, port, pin, httpClient);
}
public boolean isLoggedIn() {
return conn.isLoggedIn();
}
/**
* Perform login to the radio and establish new session
*
* @author Rainer Ostendorf
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public boolean login() throws IOException {
return conn.doLogin();
}
/**
* get the radios power state
*
* @return true when radio is on, false when radio is off
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public boolean getPower() throws IOException {
final FrontierSiliconRadioApiResult result = conn.doRequest(REQUEST_GET_POWER);
return result.getValueU8AsBoolean();
}
/**
* Turn radio on/off
*
* @param powerOn
* true turns on the radio, false turns it off
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public void setPower(boolean powerOn) throws IOException {
final String params = "value=" + (powerOn ? "1" : "0");
conn.doRequest(REQUEST_SET_POWER, params);
}
/**
* read the volume (as absolute value, 0-32)
*
* @return volume: 0=muted, 32=max. volume
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public int getVolumeAbsolute() throws IOException {
FrontierSiliconRadioApiResult result = conn.doRequest(REQUEST_GET_VOLUME);
currentVolume = result.getValueU8AsInt();
return currentVolume;
}
/**
* read the volume (as percent value, 0-100)
*
* @return volume: 0=muted, 100=max. volume (100 corresponds 32 absolute value)
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public int getVolumePercent() throws IOException {
FrontierSiliconRadioApiResult result = conn.doRequest(REQUEST_GET_VOLUME);
currentVolume = result.getValueU8AsInt();
return (currentVolume * 100) / 32;
}
/**
* Set the radios volume
*
* @param volume
* Radio volume: 0=mute, 32=max. volume
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public void setVolumeAbsolute(int volume) throws IOException {
final int newVolume = volume < 0 ? 0 : volume > 32 ? 32 : volume;
final String params = "value=" + newVolume;
conn.doRequest(REQUEST_SET_VOLUME, params);
currentVolume = volume;
}
/**
* Set the radios volume in percent
*
* @param volume
* Radio volume: 0=muted, 100=max. volume (100 corresponds 32 absolute value)
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public void setVolumePercent(int volume) throws IOException {
final int newVolumeAbsolute = (volume * 32) / 100;
final String params = "value=" + newVolumeAbsolute;
conn.doRequest(REQUEST_SET_VOLUME, params);
currentVolume = volume;
}
/**
* Increase radio volume by 1 step, max is 32.
*
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public void increaseVolumeAbsolute() throws IOException {
if (currentVolume < 32) {
setVolumeAbsolute(currentVolume + 1);
}
}
/**
* Decrease radio volume by 1 step.
*
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public void decreaseVolumeAbsolute() throws IOException {
if (currentVolume > 0) {
setVolumeAbsolute(currentVolume - 1);
}
}
/**
* Read the radios operating mode
*
* @return operating mode. On hama radio: 0="Internet Radio", 1=Spotify, 2=Player, 3="AUX IN"
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public int getMode() throws IOException {
FrontierSiliconRadioApiResult result = conn.doRequest(REQUEST_GET_MODE);
return result.getValueU32AsInt();
}
/**
* Set the radio operating mode
*
* @param mode
* On hama radio: 0="Internet Radio", 1=Spotify, 2=Player, 3="AUX IN"
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public void setMode(int mode) throws IOException {
final String params = "value=" + mode;
conn.doRequest(REQUEST_SET_MODE, params);
}
/**
* Read the Station info name, e.g. "WDR2"
*
* @return the station name, e.g. "WDR2"
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public String getPlayInfoName() throws IOException {
FrontierSiliconRadioApiResult result = conn.doRequest(REQUEST_GET_PLAY_INFO_NAME);
return result.getValueC8ArrayAsString();
}
/**
* read the stations radio text like the song name currently playing
*
* @return the radio info text, e.g. music title
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public String getPlayInfoText() throws IOException {
FrontierSiliconRadioApiResult result = conn.doRequest(REQUEST_GET_PLAY_INFO_TEXT);
return result.getValueC8ArrayAsString();
}
/**
* set a station preset. Tunes the radio to a preselected station.
*
* @param presetId
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public void setPreset(Integer presetId) throws IOException {
conn.doRequest(REQUEST_SET_PRESET, "value=1");
conn.doRequest(REQUEST_SET_PRESET_ACTION, "value=" + presetId.toString());
conn.doRequest(REQUEST_SET_PRESET, "value=0");
}
/**
* read the muted state
*
* @return true: radio is muted, false: radio is not muted
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public boolean getMuted() throws IOException {
FrontierSiliconRadioApiResult result = conn.doRequest(REQUEST_GET_MUTE);
return result.getValueU8AsBoolean();
}
/**
* mute the radio volume
*
* @param muted
* true: mute the radio, false: unmute the radio
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public void setMuted(boolean muted) throws IOException {
final String params = "value=" + (muted ? "1" : "0");
conn.doRequest(REQUEST_SET_MUTE, params);
}
}

View File

@@ -0,0 +1,232 @@
/**
* 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.fsinternetradio.internal.radio;
import java.io.IOException;
import java.io.StringReader;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* This class hold the result of a request read from the radio. Upon a request the radio returns a XML document like
* this:
*
* <pre>
* <xmp>
* <fsapiResponse> <status>FS_OK</status> <value><u8>1</u8></value> </fsapiResponse>
* </xmp>
* </pre>
*
* This class parses this XML data and provides functions for reading and casting typical fields.
*
* @author Rainer Ostendorf
* @author Patrick Koenemann
*
*/
public class FrontierSiliconRadioApiResult {
/**
* XML structure holding the parsed response
*/
final Document xmlDoc;
private final Logger logger = LoggerFactory.getLogger(FrontierSiliconRadioApiResult.class);
/**
* Create result object from XML that was received from the radio.
*
* @param requestResultString
* The XML string received from the radio.
* @throws IOException in case the XML returned by the radio is invalid.
*/
public FrontierSiliconRadioApiResult(String requestResultString) throws IOException {
Document xml = null;
try {
xml = getXmlDocFromString(requestResultString);
} catch (Exception e) {
logger.trace("converting to XML failed: '{}' with {}: {}", requestResultString, e.getClass().getName(),
e.getMessage());
logger.debug("converting to XML failed with {}: {}", e.getClass().getName(), e.getMessage());
if (e instanceof IOException) {
throw (IOException) e;
}
throw new IOException(e);
}
xmlDoc = xml;
}
/**
* Extract the field "status" from the result and return it
*
* @return result field as string.
*/
private String getStatus() {
final Element fsApiResult = (Element) xmlDoc.getElementsByTagName("fsapiResponse").item(0);
final Element statusNode = (Element) fsApiResult.getElementsByTagName("status").item(0);
final String status = getCharacterDataFromElement(statusNode);
logger.trace("status is: {}", status);
return status;
}
/**
* checks if the responses status code was "FS_OK"
*
* @return true if status is "FS_OK", false else
*/
public boolean isStatusOk() {
return ("FS_OK").equals(getStatus());
}
/**
* read the &lt;value&gt;&lt;u8&gt; field as boolean
*
* @return value.u8 field as bool
*/
public boolean getValueU8AsBoolean() {
try {
final Element fsApiResult = (Element) xmlDoc.getElementsByTagName("fsapiResponse").item(0);
final Element valueNode = (Element) fsApiResult.getElementsByTagName("value").item(0);
final Element u8Node = (Element) valueNode.getElementsByTagName("u8").item(0);
final String value = getCharacterDataFromElement(u8Node);
logger.trace("value is: {}", value);
return "1".equals(value);
} catch (Exception e) {
logger.error("getting Value.U8 failed with {}: {}", e.getClass().getName(), e.getMessage());
return false;
}
}
/**
* read the &lt;value&gt;&lt;u8&gt; field as int
*
* @return value.u8 field as int
*/
public int getValueU8AsInt() {
try {
final Element fsApiResult = (Element) xmlDoc.getElementsByTagName("fsapiResponse").item(0);
final Element valueNode = (Element) fsApiResult.getElementsByTagName("value").item(0);
final Element u8Node = (Element) valueNode.getElementsByTagName("u8").item(0);
final String value = getCharacterDataFromElement(u8Node);
logger.trace("value is: {}", value);
return Integer.parseInt(value);
} catch (Exception e) {
logger.error("getting Value.U8 failed with {}: {}", e.getClass().getName(), e.getMessage());
return 0;
}
}
/**
* read the &lt;value&gt;&lt;u32&gt; field as int
*
* @return value.u32 field as int
*/
public int getValueU32AsInt() {
try {
final Element fsApiResult = (Element) xmlDoc.getElementsByTagName("fsapiResponse").item(0);
final Element valueNode = (Element) fsApiResult.getElementsByTagName("value").item(0);
final Element u32Node = (Element) valueNode.getElementsByTagName("u32").item(0);
final String value = getCharacterDataFromElement(u32Node);
logger.trace("value is: {}", value);
return Integer.parseInt(value);
} catch (Exception e) {
logger.error("getting Value.U32 failed with {}: {}", e.getClass().getName(), e.getMessage());
return 0;
}
}
/**
* read the &lt;value&gt;&lt;c8_array&gt; field as String
*
* @return value.c8_array field as String
*/
public String getValueC8ArrayAsString() {
try {
final Element fsApiResult = (Element) xmlDoc.getElementsByTagName("fsapiResponse").item(0);
final Element valueNode = (Element) fsApiResult.getElementsByTagName("value").item(0);
final Element c8Array = (Element) valueNode.getElementsByTagName("c8_array").item(0);
final String value = getCharacterDataFromElement(c8Array);
logger.trace("value is: {}", value);
return value;
} catch (Exception e) {
logger.error("getting Value.c8array failed with {}: {}", e.getClass().getName(), e.getMessage());
return "";
}
}
/**
* read the &lt;sessionId&gt; field as String
*
* @return value of sessionId field
*/
public String getSessionId() {
final NodeList sessionIdTagList = xmlDoc.getElementsByTagName("sessionId");
final String givenSessId = getCharacterDataFromElement((Element) sessionIdTagList.item(0));
return givenSessId;
}
/**
* converts the string we got from the radio to a parsable XML document
*
* @param xmlString
* the XML string read from the radio
* @return the parsed XML document
* @throws ParserConfigurationException
* @throws SAXException
* @throws IOException
*/
private Document getXmlDocFromString(String xmlString)
throws ParserConfigurationException, SAXException, IOException {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
final DocumentBuilder builder = factory.newDocumentBuilder();
final Document xmlDocument = builder.parse(new InputSource(new StringReader(xmlString)));
return xmlDocument;
}
/**
* convert the value of a given XML element to a string for further processing
*
* @param e
* XML Element
* @return the elements value converted to string
*/
private static String getCharacterDataFromElement(Element e) {
final Node child = e.getFirstChild();
if (child instanceof CharacterData) {
final CharacterData cd = (CharacterData) child;
return cd.getData();
}
return "";
}
}

View File

@@ -0,0 +1,205 @@
/**
* 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.fsinternetradio.internal.radio;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class holds the http-connection and session information for controlling the radio.
*
* @author Rainer Ostendorf
* @author Patrick Koenemann
* @author Svilen Valkanov - replaced Apache HttpClient with Jetty
* @author Mihaela Memova - changed the calling of the stopHttpClient() method, fixed the hardcoded URL path, fixed the
* for loop condition part
*/
public class FrontierSiliconRadioConnection {
private final Logger logger = LoggerFactory.getLogger(FrontierSiliconRadioConnection.class);
/** Timeout for HTTP requests in ms */
private static final int SOCKET_TIMEOUT = 5000;
/** Hostname of the radio. */
private final String hostname;
/** Port number, usually 80. */
private final int port;
/** Access pin, passed upon login as GET parameter. */
private final String pin;
/** The session ID we get from the radio after logging in. */
private String sessionId;
/** http clients, store cookies, so it is kept in connection class. */
private HttpClient httpClient = null;
/** Flag indicating if we are successfully logged in. */
private boolean isLoggedIn = false;
public FrontierSiliconRadioConnection(String hostname, int port, String pin, HttpClient httpClient) {
this.hostname = hostname;
this.port = port;
this.pin = pin;
this.httpClient = httpClient;
}
public boolean isLoggedIn() {
return isLoggedIn;
}
/**
* Perform login/establish a new session. Uses the PIN number and when successful saves the assigned sessionID for
* future requests.
*
* @return <code>true</code> if login was successful; <code>false</code> otherwise.
* @throws IOException if communication with the radio failed, e.g. because the device is not reachable.
*/
public boolean doLogin() throws IOException {
isLoggedIn = false; // reset login flag
final String url = "http://" + hostname + ":" + port + FrontierSiliconRadioConstants.CONNECTION_PATH
+ "/CREATE_SESSION?pin=" + pin;
logger.trace("opening URL: {}", url);
Request request = httpClient.newRequest(url).method(HttpMethod.GET).timeout(SOCKET_TIMEOUT,
TimeUnit.MILLISECONDS);
try {
ContentResponse response = request.send();
int statusCode = response.getStatus();
if (statusCode != HttpStatus.OK_200) {
String reason = response.getReason();
logger.debug("Communication with radio failed: {} {}", statusCode, reason);
if (statusCode == HttpStatus.FORBIDDEN_403) {
throw new IllegalStateException("Radio does not allow connection, maybe wrong pin?");
}
throw new IOException("Communication with radio failed, return code: " + statusCode);
}
final String responseBody = response.getContentAsString();
if (!responseBody.isEmpty()) {
logger.trace("login response: {}", responseBody);
}
final FrontierSiliconRadioApiResult result = new FrontierSiliconRadioApiResult(responseBody);
if (result.isStatusOk()) {
logger.trace("login successful");
sessionId = result.getSessionId();
isLoggedIn = true;
return true; // login successful :-)
}
} catch (Exception e) {
logger.debug("Fatal transport error: {}", e.toString());
throw new IOException(e);
}
return false; // login not successful
}
/**
* Performs a request to the radio with no further parameters.
*
* Typically used for polling state info.
*
* @param REST
* API requestString, e.g. "GET/netRemote.sys.power"
* @return request result
* @throws IOException if the request failed.
*/
public FrontierSiliconRadioApiResult doRequest(String requestString) throws IOException {
return doRequest(requestString, null);
}
/**
* Performs a request to the radio with addition parameters.
*
* Typically used for changing parameters.
*
* @param REST
* API requestString, e.g. "SET/netRemote.sys.power"
* @param params
* , e.g. "value=1"
* @return request result
* @throws IOException if the request failed.
*/
public FrontierSiliconRadioApiResult doRequest(String requestString, String params) throws IOException {
// 3 retries upon failure
for (int i = 0; i < 3; i++) {
if (!isLoggedIn && !doLogin()) {
continue; // not logged in and login was not successful - try again!
}
final String url = "http://" + hostname + ":" + port + FrontierSiliconRadioConstants.CONNECTION_PATH + "/"
+ requestString + "?pin=" + pin + "&sid=" + sessionId
+ (params == null || params.trim().length() == 0 ? "" : "&" + params);
logger.trace("calling url: '{}'", url);
Request request = httpClient.newRequest(url).method(HttpMethod.GET).timeout(SOCKET_TIMEOUT,
TimeUnit.MILLISECONDS);
try {
ContentResponse response = request.send();
final int statusCode = response.getStatus();
if (statusCode != HttpStatus.OK_200) {
/*-
* Issue: https://github.com/eclipse/smarthome/issues/2548
* If the session expired, we might get a 404 here. That's ok, remember that we are not logged-in
* and try again. Print warning only if this happens in the last iteration.
*/
if (i >= 2) {
String reason = response.getReason();
logger.warn("Method failed: {} {}", statusCode, reason);
}
isLoggedIn = false;
continue;
}
final String responseBody = response.getContentAsString();
if (!responseBody.isEmpty()) {
logger.trace("got result: {}", responseBody);
} else {
logger.debug("got empty result");
isLoggedIn = false;
continue;
}
final FrontierSiliconRadioApiResult result = new FrontierSiliconRadioApiResult(responseBody);
if (result.isStatusOk()) {
return result;
}
isLoggedIn = false;
continue; // try again
} catch (Exception e) {
logger.error("Fatal transport error: {}", e.toString());
throw new IOException(e);
}
}
isLoggedIn = false; // 3 tries failed. log in again next time, maybe our session went invalid (radio restarted?)
return null;
}
}

View File

@@ -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.fsinternetradio.internal.radio;
/**
* Internal constants for the frontier silicon radio.
*
* @author Markus Rathgeb - Moved the constants to separate class
*/
public class FrontierSiliconRadioConstants {
public static final String REQUEST_SET_POWER = "SET/netRemote.sys.power";
public static final String REQUEST_GET_POWER = "GET/netRemote.sys.power";
public static final String REQUEST_GET_MODE = "GET/netRemote.sys.mode";
public static final String REQUEST_SET_MODE = "SET/netRemote.sys.mode";
public static final String REQUEST_SET_VOLUME = "SET/netRemote.sys.audio.volume";
public static final String REQUEST_GET_VOLUME = "GET/netRemote.sys.audio.volume";
public static final String REQUEST_SET_MUTE = "SET/netRemote.sys.audio.mute";
public static final String REQUEST_GET_MUTE = "GET/netRemote.sys.audio.mute";
public static final String REQUEST_SET_PRESET = "SET/netRemote.nav.state";
public static final String REQUEST_SET_PRESET_ACTION = "SET/netRemote.nav.action.selectPreset";
public static final String REQUEST_GET_PLAY_INFO_TEXT = "GET/netRemote.play.info.text";
public static final String REQUEST_GET_PLAY_INFO_NAME = "GET/netRemote.play.info.name";
/** URL path, must begin with a slash (/) */
public static final String CONNECTION_PATH = "/fsapi";
private FrontierSiliconRadioConstants() {
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="fsinternetradio" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>FSInternetRadio Binding</name>
<description>This is the binding for internet radios based on the Frontier Silicon chipset.</description>
<author>Patrick Koenemann</author>
</binding:binding>

View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="fsinternetradio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="radio">
<label>Internet Radio</label>
<description>An internet radio device based on the Frontier Silicon chipset.</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="mode" typeId="mode"/>
<channel id="volume-absolute" typeId="volume-absolute"/>
<channel id="volume-percent" typeId="volume-percent"/>
<channel id="mute" typeId="mute"/>
<channel id="play-info-name" typeId="play-info-name"/>
<channel id="play-info-text" typeId="play-info-text"/>
<channel id="preset" typeId="preset"/>
</channels>
<properties>
<property name="vendor">Frontiersilicon</property>
<property name="modelId"></property>
</properties>
<config-description>
<parameter name="ip" type="text" required="true">
<context>network-address</context>
<label>Network Address</label>
<description>The IP address (name or numeric) of the internet radio.</description>
</parameter>
<parameter name="port" type="integer" required="true">
<label>Port</label>
<description>The port of the internet radio (default: 80).</description>
<default>80</default>
</parameter>
<parameter name="pin" type="text" required="true">
<label>Pin</label>
<description>The PIN configured in the internet radio (default: 1234).</description>
<default>1234</default>
</parameter>
<parameter name="refresh" type="integer">
<label>Refresh Interval</label>
<description>Specifies the refresh interval in seconds.</description>
<default>60</default>
</parameter>
</config-description>
</thing-type>
<channel-type id="power">
<item-type>Switch</item-type>
<label>Power</label>
<description>Switch the radio on or off.</description>
<category>Switch</category>
</channel-type>
<channel-type id="preset">
<item-type>Number</item-type>
<label>Preset</label>
<description>Preset radio stations configured in the radio.</description>
</channel-type>
<channel-type id="volume-absolute" advanced="true">
<item-type>Number</item-type>
<label>Volume</label>
<description>Volume (min=0, max=32).</description>
<category>SoundVolume</category>
<state min="0" max="32" step="1"/>
</channel-type>
<channel-type id="volume-percent">
<item-type>Dimmer</item-type>
<label>Volume</label>
<description>Volume (in percent).</description>
<category>SoundVolume</category>
<state min="0" max="100" step="3"/> <!-- 3% correspond to 1 absolute step -->
</channel-type>
<channel-type id="mute">
<item-type>Switch</item-type>
<label>Mute</label>
<description>Mute the radio.</description>
</channel-type>
<channel-type id="play-info-name">
<item-type>String</item-type>
<label>Current Title</label>
<description>The name of the current radio station or track.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="play-info-text">
<item-type>String</item-type>
<label>Info Text</label>
<description>Additional information e.g. of the current radio station.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="mode">
<item-type>Number</item-type>
<label>Mode</label>
<description>The radio mode, e.g. FM radio, internet radio, AUX, etc.</description>
<state min="0" step="1"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,34 @@
/**
* 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.fsinternetradio.internal.handler;
import org.openhab.binding.fsinternetradio.internal.radio.FrontierSiliconRadio;
/**
* Utils for the handler.
*
* @author Markus Rathgeb - Initial contribution
*/
public class HandlerUtils {
/**
* Get the radio of a radio handler.
*
* @param handler the handler
* @return the managed radio object
*/
public static FrontierSiliconRadio getRadio(final FSInternetRadioHandler handler) {
return handler.radio;
}
}

View File

@@ -0,0 +1,171 @@
/**
* 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.fsinternetradio.test;
import static org.junit.Assert.*;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import org.junit.Before;
import org.junit.Test;
import org.jupnp.model.ValidationException;
import org.jupnp.model.meta.DeviceDetails;
import org.jupnp.model.meta.ManufacturerDetails;
import org.jupnp.model.meta.ModelDetails;
import org.jupnp.model.meta.RemoteDevice;
import org.jupnp.model.meta.RemoteDeviceIdentity;
import org.jupnp.model.meta.RemoteService;
import org.jupnp.model.types.DeviceType;
import org.jupnp.model.types.UDN;
import org.openhab.binding.fsinternetradio.internal.FSInternetRadioBindingConstants;
import org.openhab.binding.fsinternetradio.internal.FSInternetRadioDiscoveryParticipant;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
import org.openhab.core.thing.ThingUID;
/**
* OSGi tests for the {@link FSInternetRadioDiscoveryParticipant}.
*
* @author Mihaela Memova - Initial contribution
* @author Markus Rathgeb - Migrated from Groovy to pure Java test, made more robust
* @author Velin Yordanov - Migrated to mockito
*
*/
public class FSInternetRadioDiscoveryParticipantJavaTest {
UpnpDiscoveryParticipant discoveryParticipant;
// default device variables used in the tests
DeviceType DEFAULT_TYPE = new DeviceType("namespace", "type");
String DEFAULT_UPC = "upc";
URI DEFAULT_URI = null;
// default radio variables used in most of the tests
private static final RemoteDeviceIdentity DEFAULT_RADIO_IDENTITY;
private static final URL DEFAULT_RADIO_BASE_URL;
String DEFAULT_RADIO_NAME = "HamaRadio";
static {
try {
DEFAULT_RADIO_IDENTITY = new RemoteDeviceIdentity(new UDN("radioUDN"), 60,
new URL("http://radioDescriptiveURL"), null, null);
DEFAULT_RADIO_BASE_URL = new URL("http://radioBaseURL");
} catch (final MalformedURLException ex) {
throw new Error("Initialization error", ex);
}
}
/*
* The default radio is chosen from the {@link FrontierSiliconRadioDiscoveryParticipant}'s
* set of supported radios
*/
String DEFAULT_RADIO_MANIFACTURER = "HAMA";
String DEFAULT_RADIO_MODEL_NAME = "IR";
String DEFAULT_RADIO_MODEL_DESCRIPTION = "IR Radio";
String DEFAULT_RADIO_MODEL_NUMBER = "IR100";
String DEFAULT_RADIO_SERIAL_NUMBER = "serialNumber123";
String RADIO_BINDING_ID = "fsinternetradio"; // taken from the binding.xml file
String RADIO_THING_TYPE_ID = "radio"; // taken from the thing-types.xml file
String DEFAULT_RADIO_THING_UID = String.format("%s:%s:%s", RADIO_BINDING_ID, RADIO_THING_TYPE_ID,
DEFAULT_RADIO_SERIAL_NUMBER);
@Before
public void setUp() {
discoveryParticipant = new FSInternetRadioDiscoveryParticipant();
}
/**
* Verify correct supported types.
*/
@Test
public void correctSupportedTypes() {
assertEquals(1, discoveryParticipant.getSupportedThingTypeUIDs().size());
assertEquals(FSInternetRadioBindingConstants.THING_TYPE_RADIO,
discoveryParticipant.getSupportedThingTypeUIDs().iterator().next());
}
/**
* Verify valid DiscoveryResult with completeFSInterntRadioDevice.
*
* @throws ValidationException
*/
@Test
public void validDiscoveryResultWithComplete() throws ValidationException {
RemoteDevice completeFSInternetRadioDevice = createDefaultFSInternetRadioDevice(DEFAULT_RADIO_BASE_URL);
final DiscoveryResult result = discoveryParticipant.createResult(completeFSInternetRadioDevice);
assertEquals(new ThingUID(DEFAULT_RADIO_THING_UID), result.getThingUID());
assertEquals(FSInternetRadioBindingConstants.THING_TYPE_RADIO, result.getThingTypeUID());
assertEquals(DEFAULT_RADIO_MANIFACTURER,
result.getProperties().get(FSInternetRadioBindingConstants.PROPERTY_MANUFACTURER));
assertEquals(DEFAULT_RADIO_MODEL_NUMBER,
result.getProperties().get(FSInternetRadioBindingConstants.PROPERTY_MODEL));
}
/**
* Verify no discovery result for unknown device.
*
* @throws ValidationException
* @throws MalformedURLException
*/
@Test
public void noDiscoveryResultIfUnknown() throws MalformedURLException, ValidationException {
RemoteDevice unknownRemoteDevice = createUnknownRemoteDevice();
assertNull(discoveryParticipant.createResult(unknownRemoteDevice));
}
/**
* Verify valid DiscoveryResult with FSInterntRadio device without base URL.
*
* @throws ValidationException
*/
@Test
public void validDiscoveryResultIfWithoutBaseUrl() throws ValidationException {
RemoteDevice fsInternetRadioDeviceWithoutUrl = createDefaultFSInternetRadioDevice(null);
final DiscoveryResult result = discoveryParticipant.createResult(fsInternetRadioDeviceWithoutUrl);
assertEquals(new ThingUID(DEFAULT_RADIO_THING_UID), result.getThingUID());
assertEquals(FSInternetRadioBindingConstants.THING_TYPE_RADIO, result.getThingTypeUID());
assertEquals(DEFAULT_RADIO_MANIFACTURER,
result.getProperties().get(FSInternetRadioBindingConstants.PROPERTY_MANUFACTURER));
assertEquals(DEFAULT_RADIO_MODEL_NUMBER,
result.getProperties().get(FSInternetRadioBindingConstants.PROPERTY_MODEL));
}
private RemoteDevice createDefaultFSInternetRadioDevice(URL baseURL) throws ValidationException {
ManufacturerDetails manifacturerDetails = new ManufacturerDetails(DEFAULT_RADIO_MANIFACTURER);
ModelDetails modelDetails = new ModelDetails(DEFAULT_RADIO_MODEL_NAME, DEFAULT_RADIO_MODEL_DESCRIPTION,
DEFAULT_RADIO_MODEL_NUMBER);
DeviceDetails deviceDetails = new DeviceDetails(baseURL, DEFAULT_RADIO_NAME, manifacturerDetails, modelDetails,
DEFAULT_RADIO_SERIAL_NUMBER, DEFAULT_UPC, DEFAULT_URI);
final RemoteService remoteService = null;
return new RemoteDevice(DEFAULT_RADIO_IDENTITY, DEFAULT_TYPE, deviceDetails, remoteService);
}
private RemoteDevice createUnknownRemoteDevice() throws ValidationException, MalformedURLException {
int deviceIdentityMaxAgeSeconds = 60;
RemoteDeviceIdentity identity = new RemoteDeviceIdentity(new UDN("unknownUDN"), deviceIdentityMaxAgeSeconds,
new URL("http://unknownDescriptorURL"), null, null);
URL anotherBaseURL = new URL("http://unknownBaseUrl");
String friendlyName = "Unknown remote device";
ManufacturerDetails manifacturerDetails = new ManufacturerDetails("UnknownManifacturer");
ModelDetails modelDetails = new ModelDetails("unknownModel");
String serialNumber = "unknownSerialNumber";
DeviceDetails deviceDetails = new DeviceDetails(anotherBaseURL, friendlyName, manifacturerDetails, modelDetails,
serialNumber, DEFAULT_UPC, DEFAULT_URI);
final RemoteService remoteService = null;
return new RemoteDevice(identity, DEFAULT_TYPE, deviceDetails, remoteService);
}
}

View File

@@ -0,0 +1,896 @@
/**
* 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.fsinternetradio.test;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.*;
import static org.openhab.binding.fsinternetradio.internal.FSInternetRadioBindingConstants.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.openhab.binding.fsinternetradio.internal.FSInternetRadioBindingConstants;
import org.openhab.binding.fsinternetradio.internal.handler.FSInternetRadioHandler;
import org.openhab.binding.fsinternetradio.internal.handler.HandlerUtils;
import org.openhab.binding.fsinternetradio.internal.radio.FrontierSiliconRadio;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.items.Item;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.test.TestPortUtil;
import org.openhab.core.test.TestServer;
import org.openhab.core.test.java.JavaTest;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
import org.openhab.core.types.UnDefType;
/**
* OSGi tests for the {@link FSInternetRadioHandler}.
*
* @author Mihaela Memova - Initial contribution
* @author Markus Rathgeb - Migrated from Groovy to pure Java test, made more robust
* @author Velin Yordanov - Migrated to mockito
*
*/
public class FSInternetRadioHandlerJavaTest extends JavaTest {
private static final String DEFAULT_TEST_THING_NAME = "testRadioThing";
private static final String DEFAULT_TEST_ITEM_NAME = "testItem";
private final String VOLUME = "volume";
// The request send for preset is "SET/netRemote.nav.action.selectPreset";
private static final String PRESET = "Preset";
private static final int TIMEOUT = 10 * 1000;
private static final ThingTypeUID DEFAULT_THING_TYPE_UID = FSInternetRadioBindingConstants.THING_TYPE_RADIO;
private static final ThingUID DEFAULT_THING_UID = new ThingUID(DEFAULT_THING_TYPE_UID, DEFAULT_TEST_THING_NAME);
private static final RadioServiceDummy radioServiceDummy = new RadioServiceDummy();
/**
* In order to test a specific channel, it is necessary to create a Thing with two channels - CHANNEL_POWER
* and the tested channel. So before each test, the power channel is created and added
* to an ArrayList of channels. Then in the tests an additional channel is created and added to the ArrayList
* when it's needed.
*/
private Channel powerChannel;
private ThingHandlerCallback callback;
private static TestServer server;
/**
* A HashMap which saves all the 'channel-acceppted_item_type' pairs.
* It is set before all the tests.
*/
private static Map<String, String> acceptedItemTypes;
/**
* ArrayList of channels which is used to initialize a radioThing in the test cases.
*/
private final List<Channel> channels = new ArrayList<>();
private FSInternetRadioHandler radioHandler;
private Thing radioThing;
private static HttpClient httpClient;
// default configuration properties
private static final String DEFAULT_CONFIG_PROPERTY_IP = "127.0.0.1";
private static final String DEFAULT_CONFIG_PROPERTY_PIN = "1234";
private static final int DEFAULT_CONFIG_PROPERTY_PORT = TestPortUtil.findFreePort();
/** The default refresh interval is 60 seconds. For the purposes of the tests it is set to 1 second */
private static final String DEFAULT_CONFIG_PROPERTY_REFRESH = "1";
private static final Configuration DEFAULT_COMPLETE_CONFIGURATION = createDefaultConfiguration();
@BeforeClass
public static void setUpClass() throws Exception {
ServletHolder holder = new ServletHolder(radioServiceDummy);
server = new TestServer(DEFAULT_CONFIG_PROPERTY_IP, DEFAULT_CONFIG_PROPERTY_PORT, TIMEOUT, holder);
setTheChannelsMap();
server.startServer();
httpClient = new HttpClient();
httpClient.start();
}
@Before
public void setUp() {
createThePowerChannel();
}
@AfterClass
public static void tearDownClass() throws Exception {
server.stopServer();
httpClient.stop();
}
private static @NonNull Channel getChannel(final @NonNull Thing thing, final @NonNull String channelId) {
final Channel channel = thing.getChannel(channelId);
Assert.assertNotNull(channel);
return channel;
}
private static @NonNull ChannelUID getChannelUID(final @NonNull Thing thing, final @NonNull String channelId) {
final ChannelUID channelUID = getChannel(thing, channelId).getUID();
Assert.assertNotNull(channelUID);
return channelUID;
}
/**
* Verify OFFLINE Thing status when the IP is NULL.
*/
@Test
public void offlineIfNullIp() {
Configuration config = createConfiguration(null, DEFAULT_CONFIG_PROPERTY_PIN,
String.valueOf(DEFAULT_CONFIG_PROPERTY_PORT), DEFAULT_CONFIG_PROPERTY_REFRESH);
Thing radioThingWithNullIP = initializeRadioThing(config);
testRadioThingConsideringConfiguration(radioThingWithNullIP);
}
/**
* Verify OFFLINE Thing status when the PIN is empty String.
*/
@Test
public void offlineIfEmptyPIN() {
Configuration config = createConfiguration(DEFAULT_CONFIG_PROPERTY_IP, "",
String.valueOf(DEFAULT_CONFIG_PROPERTY_PORT), DEFAULT_CONFIG_PROPERTY_REFRESH);
Thing radioThingWithEmptyPIN = initializeRadioThing(config);
testRadioThingConsideringConfiguration(radioThingWithEmptyPIN);
}
/**
* Verify OFFLINE Thing status when the PORT is zero.
*/
@Test
public void offlineIfZeroPort() {
Configuration config = createConfiguration(DEFAULT_CONFIG_PROPERTY_IP, DEFAULT_CONFIG_PROPERTY_PIN, "0",
DEFAULT_CONFIG_PROPERTY_REFRESH);
Thing radioThingWithZeroPort = initializeRadioThing(config);
testRadioThingConsideringConfiguration(radioThingWithZeroPort);
}
/**
* Verify OFFLINE Thing status when the PIN is wrong.
*/
@Test
public void offlineIfWrongPIN() {
final String wrongPin = "5678";
Configuration config = createConfiguration(DEFAULT_CONFIG_PROPERTY_IP, wrongPin,
String.valueOf(DEFAULT_CONFIG_PROPERTY_PORT), DEFAULT_CONFIG_PROPERTY_REFRESH);
initializeRadioThing(config);
waitForAssert(() -> {
String exceptionMessage = "Radio does not allow connection, maybe wrong pin?";
verifyCommunicationError(exceptionMessage);
});
}
/**
* Verify OFFLINE Thing status when the HTTP response cannot be parsed correctly.
*/
@Test
public void offlineIfParseError() {
// create a thing with two channels - the power channel and any of the others
String modeChannelID = FSInternetRadioBindingConstants.CHANNEL_MODE;
String acceptedItemType = acceptedItemTypes.get(modeChannelID);
createChannel(DEFAULT_THING_UID, modeChannelID, acceptedItemType);
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
ChannelUID modeChannelUID = getChannelUID(radioThing, modeChannelID);
/*
* Setting the isInvalidResponseExpected variable to true
* in order to get the incorrect XML response from the servlet
*/
radioServiceDummy.setInvalidResponse(true);
// try to handle a command
radioHandler.handleCommand(modeChannelUID, DecimalType.valueOf("1"));
waitForAssert(() -> {
String exceptionMessage = "java.io.IOException: org.xml.sax.SAXParseException; lineNumber: 1; columnNumber: 2;";
verifyCommunicationError(exceptionMessage);
});
radioServiceDummy.setInvalidResponse(false);
}
/**
* Verify the HTTP status is handled correctly when it is not OK_200.
*/
@Test
public void httpStatusNokHandling() {
// create a thing with two channels - the power channel and any of the others
String modeChannelID = FSInternetRadioBindingConstants.CHANNEL_MODE;
String acceptedItemType = acceptedItemTypes.get(modeChannelID);
createChannel(DEFAULT_THING_UID, modeChannelID, acceptedItemType);
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
// turn-on the radio
turnTheRadioOn(radioThing);
/*
* Setting the needed boolean variable to false, so we can be sure
* that the XML response won't have a OK_200 status
*/
ChannelUID modeChannelUID = getChannelUID(radioThing, modeChannelID);
Item modeTestItem = initializeItem(modeChannelUID, CHANNEL_MODE, acceptedItemType);
// try to handle a command
radioHandler.handleCommand(modeChannelUID, DecimalType.valueOf("1"));
waitForAssert(() -> {
assertSame(UnDefType.NULL, modeTestItem.getState());
});
}
/**
* Verify ONLINE status of a Thing with complete configuration.
*/
@Test
public void verifyOnlineStatus() {
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
}
/**
* Verify the power channel is updated.
*/
@Test
public void powerChannelUpdated() {
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
ChannelUID powerChannelUID = powerChannel.getUID();
initializeItem(powerChannelUID, DEFAULT_TEST_ITEM_NAME,
acceptedItemTypes.get(FSInternetRadioBindingConstants.CHANNEL_POWER));
radioHandler.handleCommand(powerChannelUID, OnOffType.ON);
waitForAssert(() -> {
assertTrue("We should be able to turn on the radio",
radioServiceDummy.containsRequestParameter(1, CHANNEL_POWER));
radioServiceDummy.clearRequestParameters();
});
radioHandler.handleCommand(powerChannelUID, OnOffType.OFF);
waitForAssert(() -> {
assertTrue("We should be able to turn off the radio",
radioServiceDummy.containsRequestParameter(0, CHANNEL_POWER));
radioServiceDummy.clearRequestParameters();
});
/*
* Setting the needed boolean variable to true, so we can be sure
* that an invalid value will be returned in the XML response
*/
radioHandler.handleCommand(powerChannelUID, OnOffType.ON);
waitForAssert(() -> {
assertTrue("We should be able to turn on the radio",
radioServiceDummy.containsRequestParameter(1, CHANNEL_POWER));
radioServiceDummy.clearRequestParameters();
});
}
/**
* Verify the mute channel is updated.
*/
@Test
public void muteChhannelUpdated() {
String muteChannelID = FSInternetRadioBindingConstants.CHANNEL_MUTE;
String acceptedItemType = acceptedItemTypes.get(muteChannelID);
createChannel(DEFAULT_THING_UID, muteChannelID, acceptedItemType);
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
turnTheRadioOn(radioThing);
ChannelUID muteChannelUID = getChannelUID(radioThing, FSInternetRadioBindingConstants.CHANNEL_MUTE);
initializeItem(muteChannelUID, DEFAULT_TEST_ITEM_NAME, acceptedItemType);
radioHandler.handleCommand(muteChannelUID, OnOffType.ON);
waitForAssert(() -> {
assertTrue("We should be able to mute the radio",
radioServiceDummy.containsRequestParameter(1, CHANNEL_MUTE));
radioServiceDummy.clearRequestParameters();
});
radioHandler.handleCommand(muteChannelUID, OnOffType.OFF);
waitForAssert(() -> {
assertTrue("We should be able to unmute the radio",
radioServiceDummy.containsRequestParameter(0, CHANNEL_MUTE));
radioServiceDummy.clearRequestParameters();
});
/*
* Setting the needed boolean variable to true, so we can be sure
* that an invalid value will be returned in the XML response
*/
}
/**
* Verify the mode channel is updated.
*/
@Test
public void modeChannelUdpated() {
String modeChannelID = FSInternetRadioBindingConstants.CHANNEL_MODE;
String acceptedItemType = acceptedItemTypes.get(modeChannelID);
createChannel(DEFAULT_THING_UID, modeChannelID, acceptedItemType);
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
turnTheRadioOn(radioThing);
ChannelUID modeChannelUID = getChannelUID(radioThing, modeChannelID);
initializeItem(modeChannelUID, DEFAULT_TEST_ITEM_NAME, acceptedItemType);
radioHandler.handleCommand(modeChannelUID, DecimalType.valueOf("1"));
waitForAssert(() -> {
assertTrue("We should be able to update the mode channel correctly",
radioServiceDummy.containsRequestParameter(1, CHANNEL_MODE));
radioServiceDummy.clearRequestParameters();
});
/*
* Setting the needed boolean variable to true, so we can be sure
* that an invalid value will be returned in the XML response
*/
radioHandler.handleCommand(modeChannelUID, DecimalType.valueOf("3"));
waitForAssert(() -> {
assertTrue("We should be able to update the mode channel correctly",
radioServiceDummy.containsRequestParameter(3, CHANNEL_MODE));
radioServiceDummy.clearRequestParameters();
});
}
/**
* Verify the volume is updated through the CHANNEL_VOLUME_ABSOLUTE using INCREASE and DECREASE commands.
*/
@Test
public void volumechannelUpdatedAbsIncDec() {
String absoluteVolumeChannelID = FSInternetRadioBindingConstants.CHANNEL_VOLUME_ABSOLUTE;
String absoluteAcceptedItemType = acceptedItemTypes.get(absoluteVolumeChannelID);
createChannel(DEFAULT_THING_UID, absoluteVolumeChannelID, absoluteAcceptedItemType);
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
turnTheRadioOn(radioThing);
ChannelUID absoluteVolumeChannelUID = getChannelUID(radioThing, absoluteVolumeChannelID);
Item volumeTestItem = initializeItem(absoluteVolumeChannelUID, DEFAULT_TEST_ITEM_NAME,
absoluteAcceptedItemType);
testChannelWithINCREASEAndDECREASECommands(absoluteVolumeChannelUID, volumeTestItem);
}
/**
* Verify the volume is updated through the CHANNEL_VOLUME_ABSOLUTE using UP and DOWN commands.
*/
@Test
public void volumeChannelUpdatedAbsUpDown() {
String absoluteVolumeChannelID = FSInternetRadioBindingConstants.CHANNEL_VOLUME_ABSOLUTE;
String absoluteAcceptedItemType = acceptedItemTypes.get(absoluteVolumeChannelID);
createChannel(DEFAULT_THING_UID, absoluteVolumeChannelID, absoluteAcceptedItemType);
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
turnTheRadioOn(radioThing);
ChannelUID absoluteVolumeChannelUID = getChannelUID(radioThing, absoluteVolumeChannelID);
Item volumeTestItem = initializeItem(absoluteVolumeChannelUID, DEFAULT_TEST_ITEM_NAME,
absoluteAcceptedItemType);
testChannelWithUPAndDOWNCommands(absoluteVolumeChannelUID, volumeTestItem);
}
/**
* Verify the invalid values when updating CHANNEL_VOLUME_ABSOLUTE are handled correctly.
*/
@Test
public void invalidAbsVolumeValues() {
String absoluteVolumeChannelID = FSInternetRadioBindingConstants.CHANNEL_VOLUME_ABSOLUTE;
String absoluteAcceptedItemType = acceptedItemTypes.get(absoluteVolumeChannelID);
createChannel(DEFAULT_THING_UID, absoluteVolumeChannelID, absoluteAcceptedItemType);
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
turnTheRadioOn(radioThing);
ChannelUID absoluteVolumeChannelUID = getChannelUID(radioThing, absoluteVolumeChannelID);
initializeItem(absoluteVolumeChannelUID, DEFAULT_TEST_ITEM_NAME, absoluteAcceptedItemType);
// Trying to set a value that is greater than the maximum volume
radioHandler.handleCommand(absoluteVolumeChannelUID, DecimalType.valueOf("36"));
waitForAssert(() -> {
assertTrue("The volume should not exceed the maximum value",
radioServiceDummy.containsRequestParameter(32, VOLUME));
radioServiceDummy.clearRequestParameters();
});
// Trying to increase the volume more than its maximum value using the INCREASE command
radioHandler.handleCommand(absoluteVolumeChannelUID, IncreaseDecreaseType.INCREASE);
waitForAssert(() -> {
assertTrue("The volume should not be increased above the maximum value",
radioServiceDummy.areRequestParametersEmpty());
radioServiceDummy.clearRequestParameters();
});
// Trying to increase the volume more than its maximum value using the UP command
radioHandler.handleCommand(absoluteVolumeChannelUID, UpDownType.UP);
waitForAssert(() -> {
assertTrue("The volume should not be increased above the maximum value",
radioServiceDummy.areRequestParametersEmpty());
radioServiceDummy.clearRequestParameters();
});
// Trying to set a value that is lower than the minimum volume value
radioHandler.handleCommand(absoluteVolumeChannelUID, DecimalType.valueOf("-10"));
waitForAssert(() -> {
assertTrue("The volume should not be decreased below 0",
radioServiceDummy.containsRequestParameter(0, VOLUME));
radioServiceDummy.clearRequestParameters();
});
/*
* Setting the needed boolean variable to true, so we can be sure
* that an invalid value will be returned in the XML response
*/
// trying to set the volume
radioHandler.handleCommand(absoluteVolumeChannelUID, DecimalType.valueOf("15"));
waitForAssert(() -> {
assertTrue("We should be able to set the volume correctly",
radioServiceDummy.containsRequestParameter(15, VOLUME));
radioServiceDummy.clearRequestParameters();
});
}
/**
* Verify the volume is updated through the CHANNEL_VOLUME_PERCENT using INCREASE and DECREASE commands.
*/
@Test
public void volumeChannelUpdatedPercIncDec() {
/*
* The volume is set through the CHANNEL_VOLUME_PERCENT in order to check if
* the absolute volume will be updated properly.
*/
String absoluteVolumeChannelID = FSInternetRadioBindingConstants.CHANNEL_VOLUME_ABSOLUTE;
String absoluteAcceptedItemType = acceptedItemTypes.get(absoluteVolumeChannelID);
createChannel(DEFAULT_THING_UID, absoluteVolumeChannelID, absoluteAcceptedItemType);
String percentVolumeChannelID = FSInternetRadioBindingConstants.CHANNEL_VOLUME_PERCENT;
String percentAcceptedItemType = acceptedItemTypes.get(percentVolumeChannelID);
createChannel(DEFAULT_THING_UID, percentVolumeChannelID, percentAcceptedItemType);
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
turnTheRadioOn(radioThing);
ChannelUID absoluteVolumeChannelUID = getChannelUID(radioThing, absoluteVolumeChannelID);
Item volumeTestItem = initializeItem(absoluteVolumeChannelUID, DEFAULT_TEST_ITEM_NAME,
absoluteAcceptedItemType);
ChannelUID percentVolumeChannelUID = getChannelUID(radioThing, percentVolumeChannelID);
testChannelWithINCREASEAndDECREASECommands(percentVolumeChannelUID, volumeTestItem);
}
/**
* Verify the volume is updated through the CHANNEL_VOLUME_PERCENT using UP and DOWN commands.
*/
@Test
public void volumeChannelUpdatedPercUpDown() {
/*
* The volume is set through the CHANNEL_VOLUME_PERCENT in order to check if
* the absolute volume will be updated properly.
*/
String absoluteVolumeChannelID = FSInternetRadioBindingConstants.CHANNEL_VOLUME_ABSOLUTE;
String absoluteAcceptedItemType = acceptedItemTypes.get(absoluteVolumeChannelID);
createChannel(DEFAULT_THING_UID, absoluteVolumeChannelID, absoluteAcceptedItemType);
String percentVolumeChannelID = FSInternetRadioBindingConstants.CHANNEL_VOLUME_PERCENT;
String percentAcceptedItemType = acceptedItemTypes.get(percentVolumeChannelID);
createChannel(DEFAULT_THING_UID, percentVolumeChannelID, percentAcceptedItemType);
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
turnTheRadioOn(radioThing);
ChannelUID absoluteVolumeChannelUID = getChannelUID(radioThing, absoluteVolumeChannelID);
Item volumeTestItem = initializeItem(absoluteVolumeChannelUID, DEFAULT_TEST_ITEM_NAME,
absoluteAcceptedItemType);
ChannelUID percentVolumeChannelUID = getChannelUID(radioThing, percentVolumeChannelID);
testChannelWithUPAndDOWNCommands(percentVolumeChannelUID, volumeTestItem);
}
/**
* Verify the valid and invalid values when updating CHANNEL_VOLUME_PERCENT are handled correctly.
*/
@Test
public void validInvalidPercVolume() {
String absoluteVolumeChannelID = FSInternetRadioBindingConstants.CHANNEL_VOLUME_ABSOLUTE;
String absoluteAcceptedItemType = acceptedItemTypes.get(absoluteVolumeChannelID);
createChannel(DEFAULT_THING_UID, absoluteVolumeChannelID, absoluteAcceptedItemType);
String percentVolumeChannelID = FSInternetRadioBindingConstants.CHANNEL_VOLUME_PERCENT;
String percentAcceptedItemType = acceptedItemTypes.get(percentVolumeChannelID);
createChannel(DEFAULT_THING_UID, percentVolumeChannelID, percentAcceptedItemType);
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
turnTheRadioOn(radioThing);
ChannelUID absoluteVolumeChannelUID = getChannelUID(radioThing, absoluteVolumeChannelID);
initializeItem(absoluteVolumeChannelUID, DEFAULT_TEST_ITEM_NAME, absoluteAcceptedItemType);
ChannelUID percentVolumeChannelUID = getChannelUID(radioThing, percentVolumeChannelID);
/*
* Giving the handler a valid percent value. According to the FrontierSiliconRadio's
* documentation 100 percents correspond to 32 absolute value
*/
radioHandler.handleCommand(percentVolumeChannelUID, PercentType.valueOf("50"));
waitForAssert(() -> {
assertTrue("We should be able to set the volume correctly using percentages.",
radioServiceDummy.containsRequestParameter(16, VOLUME));
radioServiceDummy.clearRequestParameters();
});
radioHandler.handleCommand(percentVolumeChannelUID, PercentType.valueOf("15"));
waitForAssert(() -> {
assertTrue("We should be able to set the volume correctly using percentages.",
radioServiceDummy.containsRequestParameter(4, VOLUME));
radioServiceDummy.clearRequestParameters();
});
}
private void testChannelWithINCREASEAndDECREASECommands(ChannelUID channelUID, Item item) {
synchronized (channelUID) {
// First we have to make sure that the item state is 0
radioHandler.handleCommand(channelUID, DecimalType.valueOf("0"));
waitForAssert(() -> {
assertTrue("We should be able to turn on the radio",
radioServiceDummy.containsRequestParameter(1, CHANNEL_POWER));
radioServiceDummy.clearRequestParameters();
});
radioHandler.handleCommand(channelUID, IncreaseDecreaseType.INCREASE);
waitForAssert(() -> {
assertTrue("We should be able to increase the volume correctly",
radioServiceDummy.containsRequestParameter(1, VOLUME));
radioServiceDummy.clearRequestParameters();
});
radioHandler.handleCommand(channelUID, IncreaseDecreaseType.DECREASE);
waitForAssert(() -> {
assertTrue("We should be able to increase the volume correctly",
radioServiceDummy.containsRequestParameter(0, VOLUME));
radioServiceDummy.clearRequestParameters();
});
// Trying to decrease one more time
radioHandler.handleCommand(channelUID, IncreaseDecreaseType.DECREASE);
waitForAssert(() -> {
assertFalse("We should be able to decrease the volume correctly",
radioServiceDummy.containsRequestParameter(0, VOLUME));
radioServiceDummy.clearRequestParameters();
});
}
}
private void testChannelWithUPAndDOWNCommands(ChannelUID channelUID, Item item) {
synchronized (channelUID) {
// First we have to make sure that the item state is 0
radioHandler.handleCommand(channelUID, DecimalType.valueOf("0"));
waitForAssert(() -> {
assertTrue("We should be able to turn on the radio",
radioServiceDummy.containsRequestParameter(1, CHANNEL_POWER));
radioServiceDummy.clearRequestParameters();
});
radioHandler.handleCommand(channelUID, UpDownType.UP);
waitForAssert(() -> {
assertTrue("We should be able to increase the volume correctly",
radioServiceDummy.containsRequestParameter(1, VOLUME));
radioServiceDummy.clearRequestParameters();
});
radioHandler.handleCommand(channelUID, UpDownType.DOWN);
waitForAssert(() -> {
assertTrue("We should be able to decrease the volume correctly",
radioServiceDummy.containsRequestParameter(0, VOLUME));
radioServiceDummy.clearRequestParameters();
});
// Trying to decrease one more time
radioHandler.handleCommand(channelUID, UpDownType.DOWN);
waitForAssert(() -> {
assertTrue("We shouldn't be able to decrease the volume below 0",
radioServiceDummy.areRequestParametersEmpty());
radioServiceDummy.clearRequestParameters();
});
}
}
/**
* Verify the preset channel is updated.
*/
@Test
public void presetChannelUpdated() {
String presetChannelID = FSInternetRadioBindingConstants.CHANNEL_PRESET;
String acceptedItemType = acceptedItemTypes.get(presetChannelID);
createChannel(DEFAULT_THING_UID, presetChannelID, acceptedItemType);
Thing radioThing = initializeRadioThing(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
turnTheRadioOn(radioThing);
ChannelUID presetChannelUID = getChannelUID(radioThing, FSInternetRadioBindingConstants.CHANNEL_PRESET);
initializeItem(presetChannelUID, DEFAULT_TEST_ITEM_NAME, acceptedItemType);
radioHandler.handleCommand(presetChannelUID, DecimalType.valueOf("100"));
waitForAssert(() -> {
assertTrue("We should be able to set value to the preset",
radioServiceDummy.containsRequestParameter(100, PRESET));
radioServiceDummy.clearRequestParameters();
});
}
/**
* Verify the playInfoName channel is updated.
*/
@Test
public void playInfoNameChannelUpdated() {
String playInfoNameChannelID = FSInternetRadioBindingConstants.CHANNEL_PLAY_INFO_NAME;
String acceptedItemType = acceptedItemTypes.get(playInfoNameChannelID);
createChannel(DEFAULT_THING_UID, playInfoNameChannelID, acceptedItemType);
Thing radioThing = initializeRadioThingWithMockedHandler(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
turnTheRadioOn(radioThing);
ChannelUID playInfoNameChannelUID = getChannelUID(radioThing,
FSInternetRadioBindingConstants.CHANNEL_PLAY_INFO_NAME);
initializeItem(playInfoNameChannelUID, DEFAULT_TEST_ITEM_NAME, acceptedItemType);
waitForAssert(() -> {
verifyOnlineStatusIsSet();
});
}
/**
* Verify the playInfoText channel is updated.
*/
@Test
public void playInfoTextChannelUpdated() {
String playInfoTextChannelID = FSInternetRadioBindingConstants.CHANNEL_PLAY_INFO_TEXT;
String acceptedItemType = acceptedItemTypes.get(playInfoTextChannelID);
createChannel(DEFAULT_THING_UID, playInfoTextChannelID, acceptedItemType);
Thing radioThing = initializeRadioThingWithMockedHandler(DEFAULT_COMPLETE_CONFIGURATION);
testRadioThingConsideringConfiguration(radioThing);
turnTheRadioOn(radioThing);
ChannelUID playInfoTextChannelUID = getChannelUID(radioThing,
FSInternetRadioBindingConstants.CHANNEL_PLAY_INFO_TEXT);
initializeItem(playInfoTextChannelUID, DEFAULT_TEST_ITEM_NAME, acceptedItemType);
waitForAssert(() -> {
verifyOnlineStatusIsSet();
});
}
private static Configuration createDefaultConfiguration() {
return createConfiguration(DEFAULT_CONFIG_PROPERTY_IP, DEFAULT_CONFIG_PROPERTY_PIN,
String.valueOf(DEFAULT_CONFIG_PROPERTY_PORT), DEFAULT_CONFIG_PROPERTY_REFRESH);
}
private static Configuration createConfiguration(String ip, String pin, String port, String refresh) {
Configuration config = new Configuration();
config.put(FSInternetRadioBindingConstants.CONFIG_PROPERTY_IP, ip);
config.put(FSInternetRadioBindingConstants.CONFIG_PROPERTY_PIN, pin);
config.put(FSInternetRadioBindingConstants.CONFIG_PROPERTY_PORT, new BigDecimal(port));
config.put(FSInternetRadioBindingConstants.CONFIG_PROPERTY_REFRESH, new BigDecimal(refresh));
return config;
}
private static void setTheChannelsMap() {
acceptedItemTypes = new HashMap<>();
acceptedItemTypes.put(FSInternetRadioBindingConstants.CHANNEL_POWER, "Switch");
acceptedItemTypes.put(FSInternetRadioBindingConstants.CHANNEL_MODE, "Number");
acceptedItemTypes.put(FSInternetRadioBindingConstants.CHANNEL_MUTE, "Switch");
acceptedItemTypes.put(FSInternetRadioBindingConstants.CHANNEL_PLAY_INFO_NAME, "String");
acceptedItemTypes.put(FSInternetRadioBindingConstants.CHANNEL_PLAY_INFO_TEXT, "String");
acceptedItemTypes.put(FSInternetRadioBindingConstants.CHANNEL_PRESET, "Number");
acceptedItemTypes.put(FSInternetRadioBindingConstants.CHANNEL_VOLUME_ABSOLUTE, "Number");
acceptedItemTypes.put(FSInternetRadioBindingConstants.CHANNEL_VOLUME_PERCENT, "Dimmer");
}
private void createThePowerChannel() {
String powerChannelID = FSInternetRadioBindingConstants.CHANNEL_POWER;
String acceptedItemType = acceptedItemTypes.get(powerChannelID);
powerChannel = createChannel(DEFAULT_THING_UID, powerChannelID, acceptedItemType);
}
private Item initializeItem(ChannelUID channelUID, String itemName, String acceptedItemType) {
Item item = null;
switch (acceptedItemType) {
case "Number":
item = new NumberItem(itemName);
break;
case "String":
item = new StringItem(itemName);
break;
case "Switch":
item = new SwitchItem(itemName);
break;
case "Dimmer":
item = new DimmerItem(itemName);
break;
}
return item;
}
private Channel createChannel(ThingUID thingUID, String channelID, String acceptedItemType) {
ChannelUID channelUID = new ChannelUID(thingUID, channelID);
Channel radioChannel = ChannelBuilder.create(channelUID, acceptedItemType).build();
channels.add(radioChannel);
return radioChannel;
}
private void testRadioThingConsideringConfiguration(Thing thing) {
Configuration config = thing.getConfiguration();
if (isConfigurationComplete(config)) {
waitForAssert(() -> {
verifyOnlineStatusIsSet();
});
} else {
waitForAssert(() -> {
verifyConfigurationError();
});
}
}
private boolean isConfigurationComplete(Configuration config) {
String ip = (String) config.get(FSInternetRadioBindingConstants.CONFIG_PROPERTY_IP);
BigDecimal port = (BigDecimal) config.get(FSInternetRadioBindingConstants.CONFIG_PROPERTY_PORT.toString());
String pin = (String) config.get(FSInternetRadioBindingConstants.CONFIG_PROPERTY_PIN.toString());
if (ip == null || port.compareTo(BigDecimal.ZERO) == 0 || StringUtils.isEmpty(pin)) {
return false;
}
return true;
}
@SuppressWarnings("null")
private Thing initializeRadioThing(Configuration config) {
radioThing = ThingBuilder.create(DEFAULT_THING_TYPE_UID, DEFAULT_THING_UID).withConfiguration(config)
.withChannels(channels).build();
callback = mock(ThingHandlerCallback.class);
radioHandler = new FSInternetRadioHandler(radioThing, httpClient);
radioHandler.setCallback(callback);
radioThing.setHandler(radioHandler);
radioThing.getHandler().initialize();
return radioThing;
}
@SuppressWarnings("null")
private Thing initializeRadioThingWithMockedHandler(Configuration config) {
radioThing = ThingBuilder.create(DEFAULT_THING_TYPE_UID, DEFAULT_THING_UID).withConfiguration(config)
.withChannels(channels).build();
callback = mock(ThingHandlerCallback.class);
radioHandler = new MockedRadioHandler(radioThing, httpClient);
radioHandler.setCallback(callback);
radioThing.setHandler(radioHandler);
radioThing.getHandler().initialize();
return radioThing;
}
private void turnTheRadioOn(Thing radioThing) {
radioHandler.handleCommand(getChannelUID(radioThing, FSInternetRadioBindingConstants.CHANNEL_POWER),
OnOffType.ON);
final FrontierSiliconRadio radio = HandlerUtils.getRadio(radioHandler);
waitForAssert(() -> {
try {
assertTrue(radio.getPower());
} catch (IOException ex) {
throw new AssertionError("I/O error", ex);
}
});
}
private void verifyOnlineStatusIsSet() {
ThingStatusInfoBuilder statusBuilder = ThingStatusInfoBuilder.create(ThingStatus.ONLINE,
ThingStatusDetail.NONE);
ThingStatusInfo statusInfo = statusBuilder.withDescription(null).build();
verify(callback, atLeast(1)).statusUpdated(radioThing, statusInfo);
}
private void verifyConfigurationError() {
ThingStatusInfoBuilder statusBuilder = ThingStatusInfoBuilder.create(ThingStatus.OFFLINE,
ThingStatusDetail.CONFIGURATION_ERROR);
ThingStatusInfo statusInfo = statusBuilder.withDescription("Configuration incomplete").build();
verify(callback, atLeast(1)).statusUpdated(radioThing, statusInfo);
}
private void verifyCommunicationError(String exceptionMessage) {
ArgumentCaptor<ThingStatusInfo> captor = ArgumentCaptor.forClass(ThingStatusInfo.class);
verify(callback, atLeast(1)).statusUpdated(isA(Thing.class), captor.capture());
ThingStatusInfo status = captor.getValue();
assertThat(status.getStatus(), is(ThingStatus.OFFLINE));
assertThat(status.getStatusDetail(), is(ThingStatusDetail.COMMUNICATION_ERROR));
assertThat(status.getDescription().contains(exceptionMessage), is(true));
}
}

View File

@@ -0,0 +1,37 @@
/**
* 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.fsinternetradio.test;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.fsinternetradio.internal.handler.FSInternetRadioHandler;
import org.openhab.core.thing.Thing;
/**
* A mock of FSInternetRadioHandler to enable testing.
*
* @author Velin Yordanov - initial contribution
*
*/
@NonNullByDefault
public class MockedRadioHandler extends FSInternetRadioHandler {
public MockedRadioHandler(Thing thing, HttpClient client) {
super(thing, client);
}
@Override
protected boolean isLinked(String channelUID) {
return true;
}
}

View File

@@ -0,0 +1,251 @@
/**
* 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.fsinternetradio.test;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.fsinternetradio.internal.radio.FrontierSiliconRadioConstants;
/**
* Radio service mock.
*
* @author Markus Rathgeb - Initial contribution
* @author Velin Yordanov - Small adjustments
*/
public class RadioServiceDummy extends HttpServlet {
private static Map<Integer, String> requestParameters = new ConcurrentHashMap<>();
private static final long serialVersionUID = 1L;
private static final String MOCK_RADIO_PIN = "1234";
private static final String REQUEST_SET_POWER = "/" + FrontierSiliconRadioConstants.REQUEST_SET_POWER;
private static final String REQUEST_GET_POWER = "/" + FrontierSiliconRadioConstants.REQUEST_GET_POWER;
private static final String REQUEST_GET_MODE = "/" + FrontierSiliconRadioConstants.REQUEST_GET_MODE;
private static final String REQUEST_SET_MODE = "/" + FrontierSiliconRadioConstants.REQUEST_SET_MODE;
private static final String REQUEST_SET_VOLUME = "/" + FrontierSiliconRadioConstants.REQUEST_SET_VOLUME;
private static final String REQUEST_GET_VOLUME = "/" + FrontierSiliconRadioConstants.REQUEST_GET_VOLUME;
private static final String REQUEST_SET_MUTE = "/" + FrontierSiliconRadioConstants.REQUEST_SET_MUTE;
private static final String REQUEST_GET_MUTE = "/" + FrontierSiliconRadioConstants.REQUEST_GET_MUTE;
private static final String REQUEST_SET_PRESET_ACTION = "/"
+ FrontierSiliconRadioConstants.REQUEST_SET_PRESET_ACTION;
private static final String REQUEST_GET_PLAY_INFO_TEXT = "/"
+ FrontierSiliconRadioConstants.REQUEST_GET_PLAY_INFO_TEXT;
private static final String REQUEST_GET_PLAY_INFO_NAME = "/"
+ FrontierSiliconRadioConstants.REQUEST_GET_PLAY_INFO_NAME;
private static final String VALUE = "value";
/*
* For the purposes of the tests it is assumed that the current station and the additional information
* are always the same (random_station and additional_info)
*/
private final String playInfoNameValue = "random_station";
private final String playInfoNameTag = makeC8_arrayTag(playInfoNameValue);
private final String playInfoTextValue = "additional_info";
private final String playInfoTextTag = makeC8_arrayTag(playInfoTextValue);
private final int httpStatus;
private String tagToReturn = "";
private String responseToReturn = "";
private boolean isInvalidResponseExpected;
private boolean isInvalidValueExpected;
private boolean isOKAnswerExpected = true;
private String powerValue;
private String powerTag = "";
private String muteValue;
private String muteTag = "";
private String absoluteVolumeValue;
private String absoluteVolumeTag = "";
private String modeValue;
private String modeTag = "";
private String radioStation = "";
public RadioServiceDummy() {
this.httpStatus = HttpStatus.OK_200;
}
public String getRadioStation() {
return radioStation;
}
public void setRadioStation(final String radioStation) {
this.radioStation = radioStation;
}
public void setInvalidResponseExpected(boolean isInvalidResponseExpected) {
this.isInvalidResponseExpected = isInvalidResponseExpected;
}
public void setOKAnswerExpected(boolean isOKAnswerExpected) {
this.isOKAnswerExpected = isOKAnswerExpected;
}
public boolean containsRequestParameter(int value, String parameter) {
String url = requestParameters.get(value);
if (url == null) {
return false;
}
return url.contains(parameter);
}
public void clearRequestParameters() {
requestParameters.clear();
}
public boolean areRequestParametersEmpty() {
return requestParameters.isEmpty();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String queryString = request.getQueryString();
Collection<String> requestParameterNames = Collections.list(request.getParameterNames());
if (queryString != null && requestParameterNames.contains(VALUE)) {
StringBuffer fullUrl = request.getRequestURL().append("?").append(queryString);
int value = Integer.parseInt(request.getParameter(VALUE));
requestParameters.put(value, fullUrl.toString());
}
String pin = request.getParameter("pin");
if (!MOCK_RADIO_PIN.equals(pin)) {
response.setStatus(HttpStatus.FORBIDDEN_403);
} else if (!isOKAnswerExpected) {
response.setStatus(HttpStatus.NOT_FOUND_404);
} else {
response.setStatus(HttpStatus.OK_200);
response.setContentType("text/xml");
String commandString = request.getPathInfo();
switch (commandString) {
case (REQUEST_SET_POWER):
if (isInvalidValueExpected) {
powerValue = null;
} else {
powerValue = request.getParameter(VALUE);
}
case (REQUEST_GET_POWER):
powerTag = makeU8Tag(powerValue);
tagToReturn = powerTag;
break;
case (REQUEST_SET_MUTE):
if (isInvalidValueExpected) {
muteValue = null;
} else {
muteValue = request.getParameter(VALUE);
}
case (REQUEST_GET_MUTE):
muteTag = makeU8Tag(muteValue);
tagToReturn = muteTag;
break;
case (REQUEST_SET_MODE):
if (isInvalidValueExpected) {
modeValue = null;
} else {
modeValue = request.getParameter(VALUE);
}
case (REQUEST_GET_MODE):
modeTag = makeU32Tag(modeValue);
tagToReturn = modeTag;
break;
case (REQUEST_SET_VOLUME):
if (isInvalidValueExpected) {
absoluteVolumeValue = null;
} else {
absoluteVolumeValue = request.getParameter(VALUE);
}
case (REQUEST_GET_VOLUME):
absoluteVolumeTag = makeU8Tag(absoluteVolumeValue);
tagToReturn = absoluteVolumeTag;
break;
case (REQUEST_SET_PRESET_ACTION):
final String station = request.getParameter(VALUE);
setRadioStation(station);
break;
case (REQUEST_GET_PLAY_INFO_NAME):
tagToReturn = playInfoNameTag;
break;
case (REQUEST_GET_PLAY_INFO_TEXT):
tagToReturn = playInfoTextTag;
break;
default:
tagToReturn = "";
break;
}
if (isInvalidResponseExpected) {
responseToReturn = makeInvalidXMLResponse();
} else {
responseToReturn = makeValidXMLResponse();
}
PrintWriter out = response.getWriter();
out.print(responseToReturn);
}
}
protected String makeU8Tag(final String value) {
return String.format("<value><u8>%s</u8></value>", value);
}
protected String makeU32Tag(final String value) {
return String.format("<value><u32>%s</u32></value>", value);
}
protected String makeC8_arrayTag(final String value) {
return String.format("<value><c8_array>%s</c8_array></value>", value);
}
private String makeValidXMLResponse() throws IOException {
return IOUtils.toString(getClass().getResourceAsStream("/validXml.xml"));
}
private String makeInvalidXMLResponse() throws IOException {
return IOUtils.toString(getClass().getResourceAsStream("/invalidXml.xml"));
}
public void setInvalidResponse(boolean value) {
isInvalidResponseExpected = value;
}
}

View File

@@ -0,0 +1,9 @@
<--xmmmmt version="1.0" encoding="UTF-8"?>
<pre>
<sessionId>111</sessionId>
<xmp>
<fsapiResponse>
<status>FS_OK</status>
</fsapiResponse>
</xmp>
</pre>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<pre>
<sessionId>111</sessionId>
<xmp>
<fsapiResponse>
<value>
<u8>1</u8>
</value>
<status>FS_OK</status>
</fsapiResponse>
</xmp>
</pre>