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,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.fsinternetradio</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,116 @@
# FS Internet Radio Binding
This binding integrates internet radios based on the [Frontier Silicon chipset](https://www.frontier-silicon.com/).
## Supported Things
Successfully tested are internet radios:
* [Hama IR100](https://de.hama.com/00054823/hama-internetradio-ir110)
* [Medion MD87180, MD86988, MD86955, MD87528](http://internetradio.medion.com/)
* [Silvercrest SMRS18A1, SMRS30A1, SMRS35A1, SIRD 14 C2, SIRD 14 D1](https://www.silvercrest-multiroom.de/en/products/stereo-internet-radio/)
* [Roberts Stream 83i and 93i](https://www.robertsradio.com/uk/products/radio/smart-radio/)
* [Auna Connect 150, Auna KR200](https://www.auna.de/Radios/Internetradios/)
* [TechniSat DIGITRADIO 350 IR and 850](https://www.technisat.com/en_XX/DAB+-Radios-with-Internetradio/352-10996/)
* [TTMicro AS Pinell Supersound](https://www.ttmicro.no/radio)
* [Revo SuperConnect](https://revo.co.uk/products/)
* [Sangean WFR-28C](http://sg.sangean.com.tw/products/product_category.asp?cid=2)
* [Roku SoundBridge M1001](https://soundbridge.roku.com/soundbridge/index.php)
* [Dual IR 3a](https://www.dual.de/produkte/digitalradio/radio-station-ir-3a/)
* [Teufel 3sixty](https://www.teufel.de/stereo/radio-3sixty-p16568.html)
But in principle, all internet radios based on the [Frontier Silicon chipset](https://www.frontier-silicon.com/) should be supported because they share the same API.
So It is very likely that other internet radio models of the same manufacturers do also work.
## Community
For discussions and questions about supported radios, check out [this thread](https://community.openhab.org/t/internet-radio-i-need-your-help/2131).
## Discovery
The radios are discovered through UPnP in the local network.
If your radio is not discovered, please try to access its API via: `http://<radio-ip>/fsapi/CREATE_SESSION?pin=1234` (1234 is default pin, if you get a 403 error, check the radio menu for the correct pin).<br/>
If you get a 404 error, maybe a different port than the standard port 80 is used by your radio; try scanning the open ports of your radio.<br/>
If you get a result like `FS_OK 1902014387`, your radio is supported.
If this is the case, please [add your model to this documentation](https://github.com/openhab/openhab-addons/edit/master/bundles/org.openhab.binding.fsinternetradio/README.md) and/or provide discovery information in [this thread](https://community.openhab.org/t/internet-radio-i-need-your-help/2131).
## Binding Configuration
The binding itself does not need a configuration.
## Thing Configuration
Each radio must be configured via its ip address, port, pin, and a refresh rate.
* If the ip address is not discovered automatically, it must be manually set.
* The default port is `80` which should work for most radios.
* The default pin is `1234` for most radios, but if it does not work or if it was changed, look it up in the on-screen menu of the radio.
* The default refresh rate for the radio items is `60` seconds; `0` disables periodic refresh.
## Channels
All devices support some of the following channels:
| Channel Type ID | Item Type | Description | Access |
|-----------------|-----------|-------------|------- |
| power | Switch | Switch the radio on or off | R/W |
| volume-percent | Dimmer | Radio volume (min=0, max=100) | R/W |
| volume-absolute | Number | Radio volume (min=0, max=32) | R/W |
| mute | Switch | Mute the radio | R/W |
| mode | Number | The radio mode, e.g. FM radio, internet radio, AUX, etc. (model-specific, see list below) | R/W |
| preset | Number | Preset radio stations configured in the radio (write-only) | W |
| play-info-name | String | The name of the current radio station or track | R |
| play-info-text | String | Additional information e.g. of the current radio station | R |
The radio mode depends on the internet radio model (and its firmware version!).
This list is just an example how the mapping looks like for some of the devices, please try it out and adjust your sitemap for your particular radio.
| Radio Mode | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
|--------------------------|----------------|-------------------------|-----------|--------------|-----------|----------|--------------|--------------|-----------|-----------|--------|
| Hama IR110 | Internet Radio | Spotify | Player | AUX in | - | - | - | - | - | - |- |
| Medion MD87180 | Internet Radio | Music Player (USB, LAN) | DAB Radio | FM Radio | AUX in | - | - | - | - | - |- |
| Medion MD 86988 | Internet Radio | Music Player | FM Radio | AUX in | - | - | - | - | - | - |- |
| Technisat DigitRadio 580 | Internet Radio | Spotify | - | Music Player | DAB Radio | FM Radio | AUX in | CD | Bluetooth | - |- |
| Dual IR 3a | Internet Radio | Spotify | - | Music Player | DAB Radio | FM Radio | Bluetooth | - | - | - |- |
| Silvercrest SIRD 14 C1 | - | Napster | Deezer | Qobuz | Spotify | TIDAL | Spotify | Music Player | DAB Radio | FM Radio | AUX in |
| Silvercrest SIRD 14 C2 | Internet Radio | TIDAL | Deezer | Qobuz | Spotify | - | Music Player | DAB Radio | FM Radio | AUX in |- |
| Auna KR200 Kitchen Radio | Internet Radio | Spotify | - | Music Player | DAB Radio | FM Radio | AUX in | - | - | - |- |
## Full Example
demo.things:
```
fsinternetradio:radio:radioInKitchen [ ip="192.168.0.42" ]
```
demo.items:
```
Switch RadioPower "Radio Power" { channel="fsinternetradio:radio:radioInKitchen:power" }
Switch RadioMute "Radio Mute" { channel="fsinternetradio:radio:radioInKitchen:mute" }
Dimmer RadioVolume "Radio Volume" { channel="fsinternetradio:radio:radioInKitchen:volume-percent" }
Number RadioMode "Radio Mode" { channel="fsinternetradio:radio:radioInKitchen:mode" }
Number RadioPreset "Radio Stations" { channel="fsinternetradio:radio:radioInKitchen:preset" }
String RadioInfo1 "Radio Info1" { channel="fsinternetradio:radio:radioInKitchen:play-info-name" }
String RadioInfo2 "Radio Info2" { channel="fsinternetradio:radio:radioInKitchen:play-info-text" }
```
demo.sitemap:
```
sitemap demo label="Main Menu"
{
Frame {
Switch item=RadioPower
Slider visibility=[RadioPower==ON] item=RadioVolume
Switch visibility=[RadioPower==ON] item=RadioMute
Selection visibility=[RadioPower==ON] item=RadioPreset mappings=[0="Favourit 1", 1="Favourit 2", 2="Favourit 3", 3="Favourit 4"]
Selection visibility=[RadioPower==ON] item=RadioMode mappings=[0="Internet Radio", 1="Musik Player", 2="DAB", 3="FM", 4="AUX"]
Text visibility=[RadioPower==ON] item=RadioInfo1
Text visibility=[RadioPower==ON] item=RadioInfo2
}
}
```

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.fsinternetradio</artifactId>
<name>openHAB Add-ons :: Bundles :: FSInternetRadio Binding</name>
</project>

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>