From 5d5cd780a53e271b0837eb5557fbe75222767364 Mon Sep 17 00:00:00 2001 From: MatthiasKlocke <76820695+MatthiasKlocke@users.noreply.github.com> Date: Sun, 11 Apr 2021 20:02:00 +0200 Subject: [PATCH] [vdr] Initial contribution (#9947) Signed-off-by: Matthias Klocke --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.vdr/NOTICE | 13 + bundles/org.openhab.binding.vdr/README.md | 120 +++++ bundles/org.openhab.binding.vdr/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../vdr/internal/VDRBindingConstants.java | 54 +++ .../vdr/internal/VDRConfiguration.java | 52 +++ .../binding/vdr/internal/VDRHandler.java | 317 +++++++++++++ .../vdr/internal/VDRHandlerFactory.java | 66 +++ .../vdr/internal/svdrp/SVDRPChannel.java | 98 +++++ .../vdr/internal/svdrp/SVDRPClient.java | 135 ++++++ .../vdr/internal/svdrp/SVDRPClientImpl.java | 416 ++++++++++++++++++ .../svdrp/SVDRPConnectionException.java | 35 ++ .../vdr/internal/svdrp/SVDRPDiskStatus.java | 125 ++++++ .../vdr/internal/svdrp/SVDRPEpgEvent.java | 211 +++++++++ .../vdr/internal/svdrp/SVDRPException.java | 35 ++ .../svdrp/SVDRPParseResponseException.java | 51 +++ .../vdr/internal/svdrp/SVDRPResponse.java | 49 +++ .../vdr/internal/svdrp/SVDRPTimerList.java | 76 ++++ .../vdr/internal/svdrp/SVDRPVolume.java | 86 ++++ .../vdr/internal/svdrp/SVDRPWelcome.java | 112 +++++ .../main/resources/OH-INF/binding/binding.xml | 10 + .../resources/OH-INF/thing/thing-types.xml | 202 +++++++++ .../vdr/internal/SVDRPChannelTest.java | 48 ++ .../vdr/internal/SVDRPDiskStatusTest.java | 58 +++ .../vdr/internal/SVDRPEpgEventTest.java | 105 +++++ .../vdr/internal/SVDRPTimerListTest.java | 42 ++ .../binding/vdr/internal/SVDRPVolumeTest.java | 53 +++ .../vdr/internal/SVDRPWelcomeTest.java | 53 +++ bundles/pom.xml | 1 + 31 files changed, 2655 insertions(+) create mode 100644 bundles/org.openhab.binding.vdr/NOTICE create mode 100644 bundles/org.openhab.binding.vdr/README.md create mode 100644 bundles/org.openhab.binding.vdr/pom.xml create mode 100644 bundles/org.openhab.binding.vdr/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRBindingConstants.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRConfiguration.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRHandler.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRHandlerFactory.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPChannel.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPClient.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPClientImpl.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPConnectionException.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPDiskStatus.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPEpgEvent.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPException.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPParseResponseException.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPResponse.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPTimerList.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPVolume.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPWelcome.java create mode 100644 bundles/org.openhab.binding.vdr/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.vdr/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPChannelTest.java create mode 100644 bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPDiskStatusTest.java create mode 100644 bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPEpgEventTest.java create mode 100644 bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPTimerListTest.java create mode 100644 bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPVolumeTest.java create mode 100644 bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPWelcomeTest.java diff --git a/CODEOWNERS b/CODEOWNERS index 9ff610c2d1a..f24b1241558 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -286,6 +286,7 @@ /bundles/org.openhab.binding.upnpcontrol/ @mherwege /bundles/org.openhab.binding.urtsi/ @OLibutzki /bundles/org.openhab.binding.valloxmv/ @bjoernbrings +/bundles/org.openhab.binding.vdr/ @MatthiasKlocke /bundles/org.openhab.binding.vektiva/ @octa22 /bundles/org.openhab.binding.velbus/ @cedricboon /bundles/org.openhab.binding.velux/ @gs4711 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 18bab34ec45..2b591c4aa9b 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1406,6 +1406,11 @@ org.openhab.binding.valloxmv ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.vdr + ${project.version} + org.openhab.addons.bundles org.openhab.binding.vektiva diff --git a/bundles/org.openhab.binding.vdr/NOTICE b/bundles/org.openhab.binding.vdr/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.vdr/README.md b/bundles/org.openhab.binding.vdr/README.md new file mode 100644 index 00000000000..4b2522eba69 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/README.md @@ -0,0 +1,120 @@ +# Video Disk Recorder (VDR) Binding + +The Video Disk Recorder (VDR) binding allows openHAB to control your own [Video Disk Recorder](https://www.tvdr.de). + +The binding is based on VDR's own SVDRP (Simple VDR Protocol) connectivity. It supports remote control like changing volume and channels as well as sending key commands to your VDR. Also current and next EPG event data is available. + +## Supported Things + + +The binding provides only one thing type: `vdr`. You can create one thing for each VDR instance at your home. + +## Thing Configuration + +To configure a VDR, hostname or IP address and the actual SVDRP port are required. +Please note that until VDR version 1.7.15 the standard SVDRP port was 2001 and after that version it changed to 6419. +The VDR configuration file svdrphosts.conf needs to be configured to allow SVDRP access from host where openHAB instance is running. +Please check VDR documentation if you are unsure about this. + +| Configuration Parameter | Default | Required | Description | +|-------------------------|------------------|:--------:|--------------------------------------------------------------| +| host | | Yes | Hostname or IP Address of VDR instance | +| port | 6419 | Yes | SVDRP Port of VDR instance | +| refresh | 30 | No | Interval in seconds the data from VDR instance is refreshed | + +A typical thing configuration would look like this: + +``` +Thing vdr:vdr:livingRoom "VDR" @ "LivingRoom" [ host="192.168.0.51", port=6419, refresh=30 ] +``` + + +## Channels + +`power`, `channel` and `volume` can be used for basic control of your VDR. `diskUsage` might be used within a rule to notify if disk space for recordings runs short. It is also possible to display custom messages on VDR OSD, please use `message` for this. You can build your own remote control widget in openHAB by using the `keyCode` channel. + +Also you can show information about the current channel's program on your VDR by displaying the EPG Event Channels in your favorite openHAB user interface. + +To turn on the device VDR is running on please use Wake-On-LAN functionality from Network Binding. + + +| channel | type | description | +|----------------------|-------------|-----------------------------------------| +| power | Switch | Power State (to switch off VDR) | +| channel | Number | Current Channel Number (can be changed) | +| channelName | String | Name of Current Channel | +| volume | Dimmer | Current Volume | +| recording | Switch | Is currently a Recording Active? | +| diskUsage | Number | Current Disk Usage in % | +| message | String | Send Message to be displayed on VDR | +| keyCode | String | Send Key Code of Remote Control to VDR | +| currentEventTitle | String | Current EPG Event Title | +| currentEventSubTitle | String | Current EPG Event Sub Title | +| currentEventBegin | DateTime | Current EPG Event Begin | +| currentEventEnd | DateTime | Current EPG Event End | +| currentEventDuration | Number:Time | Current EPG Event Duration in Minutes | +| nextEventTitle | String | Next EPG Event Title | +| nextEventSubTitle | String | Next EPG Event Sub Title | +| nextEventBegin | DateTime | Next EPG Event Begin | +| nextEventEnd | DateTime | Next EPG Event End | +| nextEventDuration | Number:Time | Next EPG Event Duration in Minutes | + + +## Full Example + +### Things + +``` +Thing vdr:vdr:livingRoom "VDR" @ "LivingRoom" [ host="192.168.0.77", port=6419, refresh=30 ] +``` + +### Items + +``` +Switch VDR_LivingRoom_Power "Power" {channel="vdr:vdr:livingRoom:power" } +Number VDR_LivingRoom_Channel "Channel Number" {channel="vdr:vdr:livingRoom:channel" } +String VDR_LivingRoom_ChannelName "Channel Name" {channel="vdr:vdr:livingRoom:channelName" } +Dimmer VDR_LivingRoom_Volume "Volume" {channel="vdr:vdr:livingRoom:volume" } +Number VDR_LivingRoom_DiskUsage "Disk [%d %%]" {channel="vdr:vdr:livingRoom:diskUsage" } +Switch VDR_LivingRoom_Recording "Recording" {channel="vdr:vdr:livingRoom:recording" } +String VDR_LivingRoom_Message "Message" {channel="vdr:vdr:livingRoom:message" } +String VDR_LivingRoom_Key "Key Code" {channel="vdr:vdr:livingRoom:keyCode" } +String VDR_LivingRoom_CurrentEventTitle "Title (current)" {channel="vdr:vdr:livingRoom:currentEventTitle" } +String VDR_LivingRoom_CurrentEventSubTitle "Subtitle (current)" {channel="vdr:vdr:livingRoom:currentEventSubTitle" } +DateTime VDR_LivingRoom_CurrentEventBegin "Begin (current) [%1$td.%1$tm.%1$tY %1$tR]" {channel="vdr:vdr:livingRoom:currentEventBegin" } +DateTime VDR_LivingRoom_CurrentEventEnd "End (current) [%1$td.%1$tm.%1$tY %1$tR]" {channel="vdr:vdr:livingRoom:currentEventEnd" } +Number VDR_LivingRoom_CurrentEventDuration "Duration (current) [%d min]" {channel="vdr:vdr:livingRoom:currentEventDuration" } +String VDR_LivingRoom_NextEventTitle "Title (next)" {channel="vdr:vdr:livingRoom:nextEventTitle" } +String VDR_LivingRoom_NextEventSubTitle "Subtitle (next)" {channel="vdr:vdr:livingRoom:nextEventSubTitle" } +DateTime VDR_LivingRoom_NextEventBegin "Begin (next) [%1$td.%1$tm.%1$tY %1$tR]" {channel="vdr:vdr:livingRoom:nextEventBegin" } +DateTime VDR_LivingRoom_NextEventEnd "End (next) [%1$td.%1$tm.%1$tY %1$tR]" {channel="vdr:vdr:livingRoom:nextEventEnd" } +Number VDR_LivingRoom_NextEventDuration "Duration (next) [%d min]" {channel="vdr:vdr:livingRoom:nextEventDuration" } +``` + +### Sitemap + +``` +Frame label="VDR" { + Switch item=VDR_LivingRoom_Power + Selection item=VDR_LivingRoom_Channel mappings=[1="DasErste HD", 2="ZDF HD"] visibility=[VDR_LivingRoom_Power==ON] + Text item=VDR_LivingRoom_ChannelName visibility=[VDR_LivingRoom_Power==ON] + Slider item=VDR_LivingRoom_Volume visibility=[VDR_LivingRoom_Power==ON] + Text item=VDR_LivingRoom_DiskUsage + Switch item=VDR_LivingRoom_Recording + Selection item=VDR_LivingRoom_Key visibility=[VDR_LivingRoom_Power==ON] + Frame label="Now" visibility=[VDR_LivingRoom_Power==ON] { + Text item=VDR_LivingRoom_CurrentEventTitle + Text item=VDR_LivingRoom_CurrentEventSubTitle + Text item=VDR_LivingRoom_CurrentEventBegin + Text item=VDR_LivingRoom_CurrentEventEnd + Text item=VDR_LivingRoom_CurrentEventDuration + } + Frame label="Next" visibility=[VDR_LivingRoom_Power==ON] { + Text item=VDR_LivingRoom_NextEventTitle + Text item=VDR_LivingRoom_NextEventSubTitle + Text item=VDR_LivingRoom_NextEventBegin + Text item=VDR_LivingRoom_NextEventEnd + Text item=VDR_LivingRoom_NextEventDuration + } +} +``` diff --git a/bundles/org.openhab.binding.vdr/pom.xml b/bundles/org.openhab.binding.vdr/pom.xml new file mode 100644 index 00000000000..d517c2d0e66 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.vdr + + openHAB Add-ons :: Bundles :: VDR Binding + + diff --git a/bundles/org.openhab.binding.vdr/src/main/feature/feature.xml b/bundles/org.openhab.binding.vdr/src/main/feature/feature.xml new file mode 100644 index 00000000000..9acb70e014c --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.vdr/${project.version} + + diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRBindingConstants.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRBindingConstants.java new file mode 100644 index 00000000000..720d0da7e55 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRBindingConstants.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link VDRBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class VDRBindingConstants { + + private static final String BINDING_ID = "vdr"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_VDR = new ThingTypeUID(BINDING_ID, "vdr"); + + public static final String CHANNEL_UID_POWER = "power"; + public static final String CHANNEL_UID_MESSAGE = "message"; + public static final String CHANNEL_UID_CHANNEL = "channel"; + public static final String CHANNEL_UID_CHANNEL_NAME = "channelName"; + public static final String CHANNEL_UID_VOLUME = "volume"; + public static final String CHANNEL_UID_KEYCODE = "keyCode"; + public static final String CHANNEL_UID_RECORDING = "recording"; + public static final String CHANNEL_UID_DISKUSAGE = "diskUsage"; + public static final String CHANNEL_UID_CURRENT_EVENT_TITLE = "currentEventTitle"; + public static final String CHANNEL_UID_CURRENT_EVENT_SUBTITLE = "currentEventSubTitle"; + public static final String CHANNEL_UID_CURRENT_EVENT_DURATION = "currentEventDuration"; + public static final String CHANNEL_UID_CURRENT_EVENT_BEGIN = "currentEventBegin"; + public static final String CHANNEL_UID_CURRENT_EVENT_END = "currentEventEnd"; + public static final String CHANNEL_UID_NEXT_EVENT_TITLE = "nextEventTitle"; + public static final String CHANNEL_UID_NEXT_EVENT_SUBTITLE = "nextEventSubTitle"; + public static final String CHANNEL_UID_NEXT_EVENT_DURATION = "nextEventDuration"; + public static final String CHANNEL_UID_NEXT_EVENT_BEGIN = "nextEventBegin"; + public static final String CHANNEL_UID_NEXT_EVENT_END = "nextEventEnd"; + + public static final String KEY_CODE_POWER = "Power"; + + public static final String PROPERTY_VERSION = "version"; +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRConfiguration.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRConfiguration.java new file mode 100644 index 00000000000..caf7e311ba1 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRConfiguration.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link VDRConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class VDRConfiguration { + + private String host = "localhost"; + private int port = 6419; + private Integer refresh = 60; + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public Integer getRefresh() { + return refresh; + } + + public void setRefresh(Integer refresh) { + this.refresh = refresh; + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRHandler.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRHandler.java new file mode 100644 index 00000000000..d2b38d9597c --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRHandler.java @@ -0,0 +1,317 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.vdr.internal.svdrp.SVDRPChannel; +import org.openhab.binding.vdr.internal.svdrp.SVDRPClient; +import org.openhab.binding.vdr.internal.svdrp.SVDRPClientImpl; +import org.openhab.binding.vdr.internal.svdrp.SVDRPConnectionException; +import org.openhab.binding.vdr.internal.svdrp.SVDRPDiskStatus; +import org.openhab.binding.vdr.internal.svdrp.SVDRPEpgEvent; +import org.openhab.binding.vdr.internal.svdrp.SVDRPException; +import org.openhab.binding.vdr.internal.svdrp.SVDRPParseResponseException; +import org.openhab.binding.vdr.internal.svdrp.SVDRPVolume; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; +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.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link VDRHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class VDRHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(VDRHandler.class); + + private final TimeZoneProvider timeZoneProvider; + + private VDRConfiguration config = new VDRConfiguration(); + + private @Nullable ScheduledFuture refreshThreadFuture = null; + + public VDRHandler(Thing thing, TimeZoneProvider timeZoneProvider) { + super(thing); + this.timeZoneProvider = timeZoneProvider; + } + + /** + * when disposing refresh thread has to be cancelled + */ + @Override + public void dispose() { + stopRefreshThread(true); + } + + /** + * Initialization of {@link VDRHandler} + */ + @Override + public void initialize() { + config = getConfigAs(VDRConfiguration.class); + + updateStatus(ThingStatus.UNKNOWN); + + // initialize and schedule refresh thread + scheduleRefreshThread(); + } + + /** + * Update Thing's properties (e. g. VDR Version) + * + * @param client the {@link SVDRPClient} to be used for properties update + */ + public void updateProperties(SVDRPClient client) { + Map properties = editProperties(); + // set vdr version to properties of thing + String version = client.getSVDRPVersion(); + properties.put(VDRBindingConstants.PROPERTY_VERSION, version.toString()); + + // persist changes only if there are any changes. + if (!editProperties().equals(properties)) { + updateProperties(properties); + } + } + + /** + * Handling Commands for {@link VDRHandler} + * + * @param channelUID channel command was executed for + * @param command command to execute + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + SVDRPClient con = new SVDRPClientImpl(config.getHost(), config.getPort()); + try { + con.openConnection(); + + if (command instanceof RefreshType) { + this.onVDRRefresh(); + } else { + State result = UnDefType.NULL; + String cmd = command.toString(); + switch (channelUID.getId()) { + case VDRBindingConstants.CHANNEL_UID_MESSAGE: + con.sendSVDRPMessage(cmd); + break; + case VDRBindingConstants.CHANNEL_UID_POWER: + con.sendSVDRPKey(VDRBindingConstants.KEY_CODE_POWER); + break; + case VDRBindingConstants.CHANNEL_UID_CHANNEL: + SVDRPChannel channel = con.setSVDRPChannel(Integer.parseInt(cmd)); + result = new DecimalType(channel.getNumber()); + updateState(channelUID, result); + break; + case VDRBindingConstants.CHANNEL_UID_VOLUME: + SVDRPVolume volume = con.setSVDRPVolume(Integer.parseInt(cmd)); + result = new PercentType(volume.getVolume()); + updateState(channelUID, result); + break; + case VDRBindingConstants.CHANNEL_UID_KEYCODE: + con.sendSVDRPKey(cmd.trim()); + break; + } + } + } catch (SVDRPParseResponseException e) { + logger.trace("VDR handleCommand for Thing {}, ChannelUID {}, Parse Response failed. Message: {}", + this.getThing().getUID(), channelUID, e.getMessage()); + } catch (SVDRPConnectionException e) { + logger.debug("VDR handleCommand for Thing {}, ChannelUID {}, Connection failed. Message: {}", + this.getThing().getUID(), channelUID, e.getMessage()); + } finally { + try { + con.closeConnection(); + } catch (SVDRPException ex) { + logger.trace("Error on VDR handleCommand while closing SVDRP Connection for Thing : {} with message {}", + this.getThing().getUID(), ex.getMessage()); + } + } + } + + /** + * Refresh data from SVDRPClient (Polling) + */ + private void onVDRRefresh() { + SVDRPClient con = new SVDRPClientImpl(config.getHost(), config.getPort()); + Thing thing = getThing(); + try { + con.openConnection(); + updateStatus(ThingStatus.ONLINE); + updateProperties(con); + + thing.getChannels().stream().map(c -> c.getUID()).filter(this::isLinked).forEach(channelUID -> { + try { + logger.trace("updateChannel: {}", channelUID); + + SVDRPEpgEvent entry; + State result = UnDefType.NULL; + + switch (channelUID.getId()) { + case VDRBindingConstants.CHANNEL_UID_RECORDING: + boolean isRecording = con.isRecordingActive(); + if (isRecording) { + result = OnOffType.ON; + } else { + result = OnOffType.OFF; + } + break; + case VDRBindingConstants.CHANNEL_UID_VOLUME: + SVDRPVolume volume = con.getSVDRPVolume(); + result = new PercentType(volume.getVolume()); + break; + case VDRBindingConstants.CHANNEL_UID_CHANNEL: + SVDRPChannel channel = con.getCurrentSVDRPChannel(); + result = new DecimalType(channel.getNumber()); + break; + case VDRBindingConstants.CHANNEL_UID_CHANNEL_NAME: + SVDRPChannel svdrpChannel = con.getCurrentSVDRPChannel(); + result = new StringType(svdrpChannel.getName()); + break; + case VDRBindingConstants.CHANNEL_UID_POWER: + SVDRPDiskStatus status = con.getDiskStatus(); + if (status.getPercentUsed() >= 0) { + result = OnOffType.ON; + } else { + result = OnOffType.OFF; + } + break; + case VDRBindingConstants.CHANNEL_UID_DISKUSAGE: + SVDRPDiskStatus diskStatus = con.getDiskStatus(); + result = new DecimalType(diskStatus.getPercentUsed()); + break; + case VDRBindingConstants.CHANNEL_UID_CURRENT_EVENT_TITLE: + entry = con.getEpgEvent(SVDRPEpgEvent.TYPE.NOW); + result = new StringType(entry.getTitle()); + break; + case VDRBindingConstants.CHANNEL_UID_CURRENT_EVENT_SUBTITLE: + entry = con.getEpgEvent(SVDRPEpgEvent.TYPE.NOW); + result = new StringType(entry.getSubtitle()); + break; + case VDRBindingConstants.CHANNEL_UID_CURRENT_EVENT_DURATION: + entry = con.getEpgEvent(SVDRPEpgEvent.TYPE.NOW); + result = new QuantityType<>(entry.getDuration(), Units.MINUTE); + break; + case VDRBindingConstants.CHANNEL_UID_CURRENT_EVENT_BEGIN: + entry = con.getEpgEvent(SVDRPEpgEvent.TYPE.NOW); + result = new DateTimeType(LocalDateTime.ofInstant(entry.getBegin(), ZoneId.systemDefault()) + .atZone(timeZoneProvider.getTimeZone())); + break; + case VDRBindingConstants.CHANNEL_UID_CURRENT_EVENT_END: + entry = con.getEpgEvent(SVDRPEpgEvent.TYPE.NOW); + result = new DateTimeType(LocalDateTime.ofInstant(entry.getEnd(), ZoneId.systemDefault()) + .atZone(timeZoneProvider.getTimeZone())); + break; + case VDRBindingConstants.CHANNEL_UID_NEXT_EVENT_TITLE: + entry = con.getEpgEvent(SVDRPEpgEvent.TYPE.NEXT); + result = new StringType(entry.getTitle()); + break; + case VDRBindingConstants.CHANNEL_UID_NEXT_EVENT_SUBTITLE: + entry = con.getEpgEvent(SVDRPEpgEvent.TYPE.NEXT); + result = new StringType(entry.getSubtitle()); + break; + case VDRBindingConstants.CHANNEL_UID_NEXT_EVENT_DURATION: + entry = con.getEpgEvent(SVDRPEpgEvent.TYPE.NEXT); + result = new QuantityType<>(entry.getDuration(), Units.MINUTE); + break; + case VDRBindingConstants.CHANNEL_UID_NEXT_EVENT_BEGIN: + entry = con.getEpgEvent(SVDRPEpgEvent.TYPE.NEXT); + result = new DateTimeType(LocalDateTime.ofInstant(entry.getBegin(), ZoneId.systemDefault()) + .atZone(timeZoneProvider.getTimeZone())); + break; + case VDRBindingConstants.CHANNEL_UID_NEXT_EVENT_END: + entry = con.getEpgEvent(SVDRPEpgEvent.TYPE.NEXT); + result = new DateTimeType(LocalDateTime.ofInstant(entry.getEnd(), ZoneId.systemDefault()) + .atZone(timeZoneProvider.getTimeZone())); + break; + + } + updateState(channelUID, result); + + } catch (SVDRPParseResponseException e) { + logger.trace("VDR Refresh for Thing {}, ChannelUID {}, Parse Response failed. Message: {}", + this.getThing().getUID(), channelUID, e.getMessage()); + updateState(channelUID, UnDefType.UNDEF); + } catch (SVDRPConnectionException e) { + logger.debug("VDR Refresh for Thing {}, ChannelUID {}, Connection failed. Message: {}", + this.getThing().getUID(), channelUID, e.getMessage()); + } + + }); + + } catch (SVDRPConnectionException ce) { + if (thing.getStatus() == ThingStatus.ONLINE) { + // also update power channel when thing is offline before setting it offline + thing.getChannels().stream().map(c -> c.getUID()).filter(this::isLinked).forEach(channelUID -> { + if (VDRBindingConstants.CHANNEL_UID_POWER.equals(channelUID.getIdWithoutGroup())) { + updateState(channelUID, OnOffType.OFF); + } + }); + } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ce.getMessage()); + } catch (SVDRPParseResponseException se) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, se.getMessage()); + } finally { + try { + con.closeConnection(); + } catch (SVDRPException e) { + logger.trace("Error on VDR Refresh while closing SVDRP Connection for Thing : {} with message {}", + this.getThing().getUID(), e.getMessage()); + } + } + } + + /** + * Schedules the refresh thread + */ + private void scheduleRefreshThread() { + refreshThreadFuture = scheduler.scheduleWithFixedDelay(this::onVDRRefresh, 3, config.getRefresh(), + TimeUnit.SECONDS); + } + + /** + * Stops the refresh thread. + * + * @param force if set to true thread cancellation will be forced + */ + private void stopRefreshThread(boolean force) { + ScheduledFuture refreshThread = refreshThreadFuture; + if (refreshThread != null) + refreshThread.cancel(force); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRHandlerFactory.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRHandlerFactory.java new file mode 100644 index 00000000000..6ab61cac8eb --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/VDRHandlerFactory.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal; + +import static org.openhab.binding.vdr.internal.VDRBindingConstants.THING_TYPE_VDR; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.TimeZoneProvider; +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 VDRHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.vdr", service = ThingHandlerFactory.class) +public class VDRHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_VDR); + + private final TimeZoneProvider timeZoneProvider; + + @Activate + public VDRHandlerFactory(final @Reference TimeZoneProvider timeZoneProvider) { + this.timeZoneProvider = timeZoneProvider; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_VDR.equals(thingTypeUID)) { + return new VDRHandler(thing, timeZoneProvider); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPChannel.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPChannel.java new file mode 100644 index 00000000000..c352818449c --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPChannel.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SVDRPChannel} contains SVDRP Response Data for Channels + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class SVDRPChannel { + private int number; + private String name = ""; + + private SVDRPChannel() { + } + + /** + * parse object from SVDRP Client Response + * + * @param message SVDRP Client Response + * @return Channel Object + * @throws SVDRPParseResponseException thrown if response data is not parseable + */ + public static SVDRPChannel parse(String message) throws SVDRPParseResponseException { + String number = message.substring(0, message.indexOf(" ")); + String name = message.substring(message.indexOf(" ") + 1, message.length()); + SVDRPChannel channel = new SVDRPChannel(); + try { + channel.setNumber(Integer.parseInt(number)); + } catch (NumberFormatException e) { + throw new SVDRPParseResponseException(e.getMessage(), e); + } + channel.setName(name); + return channel; + } + + /** + * Get Channel Number + * + * @return Channel Number + */ + public int getNumber() { + return number; + } + + /** + * Set Channel Number + * + * @param number Channel Number + */ + public void setNumber(int number) { + this.number = number; + } + + /** + * Get Channel Name + * + * @return Channel Name + */ + public String getName() { + return name; + } + + /** + * Set Channel Name + * + * @param name Channel Name + */ + public void setName(String name) { + this.name = name; + } + + /** + * String Representation of SVDRPChannel Object + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (number >= 0) { + sb.append("Number: " + String.valueOf(number) + System.lineSeparator()); + } + sb.append("Name: " + name + System.lineSeparator()); + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPClient.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPClient.java new file mode 100644 index 00000000000..d0b79c433a9 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPClient.java @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SVDRPClient} encapsulates all calls to the SVDRP interface of a VDR + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public interface SVDRPClient { + + /** + * + * Open VDR Socket Connection + * + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public void openConnection() throws SVDRPConnectionException, SVDRPParseResponseException; + + /** + * Close VDR Socket Connection + * + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public void closeConnection() throws SVDRPConnectionException, SVDRPParseResponseException; + + /** + * Retrieve Disk Status from SVDRP Client + * + * @return SVDRP Disk Status + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public SVDRPDiskStatus getDiskStatus() throws SVDRPConnectionException, SVDRPParseResponseException; + + /** + * Retrieve EPG Event from SVDRPClient + * + * @param type Type of EPG Event (now, next) + * @return SVDRP EPG Event + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public SVDRPEpgEvent getEpgEvent(SVDRPEpgEvent.TYPE type) + throws SVDRPConnectionException, SVDRPParseResponseException; + + /** + * Retrieve current volume from SVDRP Client + * + * @return SVDRP Volume Object + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public SVDRPVolume getSVDRPVolume() throws SVDRPConnectionException, SVDRPParseResponseException; + + /** + * Set volume on SVDRP Client + * + * @param newVolume Volume in Percent + * @return SVDRP Volume Object + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public SVDRPVolume setSVDRPVolume(int newVolume) throws SVDRPConnectionException, SVDRPParseResponseException; + + /** + * Send Key command to SVDRP Client + * + * @param key Key Command to send + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public void sendSVDRPKey(String key) throws SVDRPConnectionException, SVDRPParseResponseException; + + /** + * Send Message to SVDRP Client + * + * @param message Message to send + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public void sendSVDRPMessage(String message) throws SVDRPConnectionException, SVDRPParseResponseException; + + /** + * Retrieve current Channel from SVDRP Client + * + * @return SVDRPChannel object + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public SVDRPChannel getCurrentSVDRPChannel() throws SVDRPConnectionException, SVDRPParseResponseException; + + /** + * Change current Channel on SVDRP Client + * + * @param number Channel to be set + * @return SVDRPChannel object + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public SVDRPChannel setSVDRPChannel(int number) throws SVDRPConnectionException, SVDRPParseResponseException; + + /** + * Retrieve from SVDRP Client if a recording is currently active + * + * @return is currently a recording active + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public boolean isRecordingActive() throws SVDRPConnectionException, SVDRPParseResponseException; + + /** + * Retrieve VDR Version from SVDRP Client + * + * @return VDR Version + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + public String getSVDRPVersion(); +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPClientImpl.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPClientImpl.java new file mode 100644 index 00000000000..ce633b5ebef --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPClientImpl.java @@ -0,0 +1,416 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link SVDRPClientImpl} encapsulates all calls to the SVDRP interface of a VDR + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class SVDRPClientImpl implements SVDRPClient { + + private String host; + private int port = 6419; + private String charset = "UTF-8"; + private String version = ""; + + private static final String WELCOME_MESSAGE = "([0-9]{3})([ -])(.*)"; + private static final Pattern PATTERN_WELCOME = Pattern.compile(WELCOME_MESSAGE); + + private static final int TIMEOUT_MS = 3000; + + private @Nullable Socket socket = null; + private @Nullable BufferedWriter out = null; + private @Nullable BufferedReader in = null; + + public SVDRPClientImpl(String host, int port) { + super(); + this.host = host; + this.port = port; + } + + public SVDRPClientImpl(String host, int port, String charset) { + super(); + this.host = host; + this.port = port; + this.charset = charset; + } + + /** + * + * Open VDR Socket Connection + * + * @throws IOException if an IO Error occurs + */ + @Override + public void openConnection() throws SVDRPConnectionException, SVDRPParseResponseException { + Socket localSocket = socket; + BufferedWriter localOut = out; + BufferedReader localIn = in; + + if (localSocket == null || localSocket.isClosed()) { + localSocket = new Socket(); + socket = localSocket; + } + try { + InetSocketAddress isa = new InetSocketAddress(host, port); + localSocket.connect(isa, TIMEOUT_MS); + localSocket.setSoTimeout(TIMEOUT_MS); + + localOut = new BufferedWriter(new OutputStreamWriter(localSocket.getOutputStream(), charset), 8192); + out = localOut; + localIn = new BufferedReader(new InputStreamReader(localSocket.getInputStream(), charset), 8192); + in = localIn; + + // read welcome message and init version & charset + SVDRPResponse res = null; + + res = execute(null); + + if (res.getCode() == 220) { + SVDRPWelcome welcome = SVDRPWelcome.parse(res.getMessage()); + this.charset = welcome.getCharset(); + this.version = welcome.getVersion(); + } else { + throw new SVDRPParseResponseException(res); + } + } catch (IOException e) { + // cleanup after timeout + try { + if (localOut != null) + localOut.close(); + if (localIn != null) + localIn.close(); + localSocket.close(); + } catch (IOException ex) { + } + throw new SVDRPConnectionException(e.getMessage(), e); + } + } + + /** + * Close VDR Socket Connection + * + * @throws IOException if an IO Error occurs + */ + @Override + public void closeConnection() throws SVDRPConnectionException, SVDRPParseResponseException { + Socket localSocket = socket; + BufferedWriter localOut = out; + BufferedReader localIn = in; + /* + * socket on vdr stays in FIN_WAIT2 without closing connection + */ + try { + if (localOut != null) { + localOut.write("QUIT"); + localOut.newLine(); + localOut.flush(); + localOut.close(); + } + if (localIn != null) { + localIn.close(); + } + if (localSocket != null) { + localSocket.close(); + } + } catch (IOException e) { + throw new SVDRPConnectionException(e.getMessage(), e); + } + } + + /** + * + * execute SVDRP Call + * + * @param command SVDRP command to execute + * @return response of SVDRPCall + * @throws SVDRPException exception from SVDRP call + */ + private SVDRPResponse execute(@Nullable String command) + throws SVDRPConnectionException, SVDRPParseResponseException { + BufferedWriter localOut = out; + BufferedReader localIn = in; + + StringBuilder message = new StringBuilder(); + Matcher matcher = null; + + int code; + try { + if (command != null) { + if (localOut == null) { + throw new SVDRPConnectionException("OutputStream is null!"); + } else { + localOut.write(command); + localOut.newLine(); + localOut.flush(); + } + } + + if (localIn != null) { + code = -1; + String line = null; + boolean cont = true; + while (cont && (line = localIn.readLine()) != null) { + matcher = PATTERN_WELCOME.matcher(line); + if (matcher.matches() && matcher.groupCount() > 2) { + if (code < 0) { + code = Integer.parseInt(matcher.group(1)); + } + if (" ".equals(matcher.group(2))) { + cont = false; + } + message.append(matcher.group(3)); + if (cont) { + message.append(System.lineSeparator()); + } + } else { + cont = false; + } + } + return new SVDRPResponse(code, message.toString()); + } else { + throw new SVDRPConnectionException("SVDRP Input Stream is Null"); + } + } catch (IOException ioe) { + throw new SVDRPConnectionException(ioe.getMessage(), ioe); + } catch (NumberFormatException ne) { + throw new SVDRPParseResponseException(ne.getMessage(), ne); + } + } + + /** + * Retrieve Disk Status from SVDRP Client + * + * @return SVDRP Disk Status + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + @Override + public SVDRPDiskStatus getDiskStatus() throws SVDRPConnectionException, SVDRPParseResponseException { + SVDRPResponse res = null; + + res = execute("STAT disk"); + + if (res.getCode() == 250) { + SVDRPDiskStatus status = SVDRPDiskStatus.parse(res.getMessage()); + return status; + } else { + throw new SVDRPParseResponseException(res); + } + } + + /** + * Retrieve EPG Event from SVDRPClient + * + * @param type Type of EPG Event (now, next) + * @return SVDRP EPG Event + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + @Override + public SVDRPEpgEvent getEpgEvent(SVDRPEpgEvent.TYPE type) + throws SVDRPConnectionException, SVDRPParseResponseException { + SVDRPResponse res = null; + SVDRPChannel channel = this.getCurrentSVDRPChannel(); + switch (type) { + case NOW: + res = execute(String.format("LSTE %s %s", channel.getNumber(), "now")); + break; + case NEXT: + res = execute(String.format("LSTE %s %s", channel.getNumber(), "next")); + break; + } + + if (res != null && res.getCode() == 215) { + SVDRPEpgEvent entry = SVDRPEpgEvent.parse(res.getMessage()); + return entry; + } else if (res != null) { + throw new SVDRPParseResponseException(res); + } else { + throw new SVDRPConnectionException("SVDRPResponse is Null"); + } + } + + /** + * Retrieve current volume from SVDRP Client + * + * @return SVDRP Volume Object + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + @Override + public SVDRPVolume getSVDRPVolume() throws SVDRPConnectionException, SVDRPParseResponseException { + SVDRPResponse res = null; + + res = execute("VOLU"); + + if (res.getCode() == 250) { + SVDRPVolume volume = SVDRPVolume.parse(res.getMessage()); + return volume; + } else { + throw new SVDRPParseResponseException(res); + } + } + + /** + * Set volume on SVDRP Client + * + * @param newVolume Volume in Percent + * @return SVDRP Volume Object + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + @Override + public SVDRPVolume setSVDRPVolume(int newVolume) throws SVDRPConnectionException, SVDRPParseResponseException { + SVDRPResponse res = null; + + double newVolumeDouble = newVolume * 255 / 100; + res = execute(String.format("VOLU %s", String.valueOf(Math.round(newVolumeDouble)))); + + if (res.getCode() == 250) { + SVDRPVolume volume = SVDRPVolume.parse(res.getMessage()); + return volume; + } else { + throw new SVDRPParseResponseException(res); + } + } + + /** + * Send Key command to SVDRP Client + * + * @param key Key Command to send + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + @Override + public void sendSVDRPKey(String key) throws SVDRPConnectionException, SVDRPParseResponseException { + SVDRPResponse res = null; + + res = execute(String.format("HITK %s", key)); + + if (res.getCode() != 250) { + throw new SVDRPParseResponseException(res); + } + } + + /** + * Send Message to SVDRP Client + * + * @param message Message to send + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + @Override + public void sendSVDRPMessage(String message) throws SVDRPConnectionException, SVDRPParseResponseException { + SVDRPResponse res = null; + + res = execute(String.format("MESG %s", message)); + + if (res.getCode() != 250) { + throw new SVDRPParseResponseException(res); + } + } + + /** + * Retrieve current Channel from SVDRP Client + * + * @return SVDRPChannel object + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + @Override + public SVDRPChannel getCurrentSVDRPChannel() throws SVDRPConnectionException, SVDRPParseResponseException { + SVDRPResponse res = null; + + res = execute("CHAN"); + + if (res.getCode() == 250) { + SVDRPChannel channel = SVDRPChannel.parse(res.getMessage()); + return channel; + } else { + throw new SVDRPParseResponseException(res); + } + } + + /** + * Change current Channel on SVDRP Client + * + * @param number Channel to be set + * @return SVDRPChannel object + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + @Override + public SVDRPChannel setSVDRPChannel(int number) throws SVDRPConnectionException, SVDRPParseResponseException { + SVDRPResponse res = null; + + res = execute(String.format("CHAN %s", number)); + + if (res.getCode() == 250) { + SVDRPChannel channel = SVDRPChannel.parse(res.getMessage()); + return channel; + } else { + throw new SVDRPParseResponseException(res); + } + } + + /** + * Retrieve from SVDRP Client if a recording is currently active + * + * @return is currently a recording active + * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible + * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response + */ + @Override + public boolean isRecordingActive() throws SVDRPConnectionException, SVDRPParseResponseException { + SVDRPResponse res = null; + + res = execute("LSTT"); + + if (res.getCode() == 250) { + SVDRPTimerList timers = SVDRPTimerList.parse(res.getMessage()); + return timers.isRecordingActive(); + } else if (res.getCode() == 550) { + // Error 550 is "No timers defined". Therefore there cannot be an active recording + return false; + } else { + throw new SVDRPParseResponseException(res); + } + } + + /** + * Retrieve VDR Version from SVDRP Client + * + * @return VDR Version + * @throws SVDRPException thrown if something's not OK with SVDRP call + */ + @Override + public String getSVDRPVersion() { + return version; + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPConnectionException.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPConnectionException.java new file mode 100644 index 00000000000..8e8d1c42814 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPConnectionException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link SVDRPConnectionException} is thrown when SVDRP Connection cannot be established + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class SVDRPConnectionException extends SVDRPException { + + private static final long serialVersionUID = 2825596676109860370L; + + public SVDRPConnectionException(@Nullable String message) { + super(message); + } + + public SVDRPConnectionException(@Nullable String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPDiskStatus.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPDiskStatus.java new file mode 100644 index 00000000000..aa254613e6d --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPDiskStatus.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SVDRPDiskStatus} contains SVDRP Response Data for DiskStatus queries + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class SVDRPDiskStatus { + private static final Pattern PATTERN_DISK_STATUS = Pattern.compile("([\\d]*)MB ([\\d]*)MB ([\\d]*)%"); + + private long megaBytesFree = -1; + private long megaBytesTotal = -1; + private int percentUsed = -1; + + private SVDRPDiskStatus() { + } + + /** + * parse object from SVDRP Client Response + * + * @param message SVDRP Client Response + * @return Disk Status Object + * @throws SVDRPParseResponseException thrown if response data is not parseable + */ + public static SVDRPDiskStatus parse(String message) throws SVDRPParseResponseException { + SVDRPDiskStatus status = new SVDRPDiskStatus(); + Matcher matcher = PATTERN_DISK_STATUS.matcher(message); + if (matcher.find() && matcher.groupCount() == 3) { + status.setMegaBytesTotal(Long.parseLong(matcher.group(1))); + status.setMegaBytesFree(Long.parseLong(matcher.group(2))); + status.setPercentUsed(Integer.parseInt(matcher.group(3))); + } + return status; + } + + /** + * Get Megabytes Free on Disk + * + * @return megabytes free + */ + public long getMegaBytesFree() { + return megaBytesFree; + } + + /** + * Set Megabytes Free on Disk + * + * @param megaBytesFree megabytes free + */ + public void setMegaBytesFree(long megaBytesFree) { + this.megaBytesFree = megaBytesFree; + } + + /** + * Get Megabytes Total on Disk + * + * @return megabytes total + */ + public long getMegaBytesTotal() { + return megaBytesTotal; + } + + /** + * Set Megabytes Total on Disk + * + * @param megaBytesTotal megabytes total + */ + public void setMegaBytesTotal(long megaBytesTotal) { + this.megaBytesTotal = megaBytesTotal; + } + + /** + * Get Percentage Used on Disk + * + * @return percentage used + */ + public int getPercentUsed() { + return percentUsed; + } + + /** + * Set Percentage Used on Disk + * + * @param percentUsed percentage used + */ + public void setPercentUsed(int percentUsed) { + this.percentUsed = percentUsed; + } + + /** + * String Representation of SVDRPDiskStatus Object + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (megaBytesTotal >= 0) { + sb.append("Total: " + megaBytesTotal + "MB" + System.lineSeparator()); + } + if (megaBytesFree >= 0) { + sb.append("Free: " + megaBytesFree + "MB" + System.lineSeparator()); + } + if (percentUsed >= 0) { + sb.append("Free: " + percentUsed + "%" + System.lineSeparator()); + } + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPEpgEvent.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPEpgEvent.java new file mode 100644 index 00000000000..48a346adc10 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPEpgEvent.java @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SVDRPEpgEvent} contains SVDRP Response Data for an EPG Event + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class SVDRPEpgEvent { + + public enum TYPE { + NOW, + NEXT + } + + private String title = ""; + private String subtitle = ""; + private Instant begin = Instant.now(); + private Instant end = Instant.now(); + private int duration; + + private SVDRPEpgEvent() { + } + + /** + * parse object from SVDRP Client Response + * + * @param message SVDRP Client Response + * @return SVDRPEpgEvent Object + * @throws SVDRPParseResponseException thrown if response data is not parseable + */ + public static SVDRPEpgEvent parse(String message) throws SVDRPParseResponseException { + SVDRPEpgEvent entry = new SVDRPEpgEvent(); + StringTokenizer st = new StringTokenizer(message, System.lineSeparator()); + + while (st.hasMoreTokens()) { + String line = st.nextToken(); + if (line.length() >= 1 && !line.startsWith("End")) { + switch (line.charAt(0)) { + case 'T': + entry.setTitle(line.substring(1).trim()); + break; + case 'S': + entry.setSubtitle(line.substring(1).trim()); + break; + case 'E': + StringTokenizer lt = new StringTokenizer(line.substring(1).trim(), " "); + lt.nextToken(); // event id + try { + long begin = Long.parseLong(lt.nextToken()); + entry.setBegin(Instant.ofEpochSecond(begin)); + } catch (NumberFormatException | NoSuchElementException e) { + throw new SVDRPParseResponseException("Begin: " + e.getMessage(), e); + } + try { + entry.setDuration(Integer.parseInt(lt.nextToken()) / 60); + } catch (NumberFormatException | NoSuchElementException e) { + throw new SVDRPParseResponseException("Duration: " + e.getMessage(), e); + } + entry.setEnd(entry.getBegin().plus(entry.getDuration(), ChronoUnit.MINUTES)); + default: + break; + } + } else if (!line.startsWith("End")) { + throw new SVDRPParseResponseException("EPG Event Line corrupt: " + line); + } + } + + return entry; + } + + /** + * Get Title of EPG Event + * + * @return Event Title + */ + public String getTitle() { + return title; + } + + /** + * Set Title of EPG Event + * + * @param title Event Title + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Get Subtitle of EPG Event + * + * @return Event Subtitle + */ + public String getSubtitle() { + return subtitle; + } + + /** + * Set Subtitle of EPG Event + * + * @param subtitle Event Subtitle + */ + public void setSubtitle(String subtitle) { + this.subtitle = subtitle; + } + + /** + * Get Begin of EPG Event + * + * @return Event Begin + */ + public Instant getBegin() { + return begin; + } + + /** + * Set Begin of EPG Event + * + * @param begin Event Begin + */ + public void setBegin(Instant begin) { + this.begin = begin; + } + + /** + * Get End of EPG Event + * + * @return Event End + */ + public Instant getEnd() { + return end; + } + + /** + * Set End of EPG Event + * + * @param end Event End + */ + public void setEnd(Instant end) { + this.end = end; + } + + /** + * Get Duration of EPG Event in Minutes + * + * @return Event Duration in Minutes + */ + public int getDuration() { + return duration; + } + + /** + * Set Duration of EPG Event in Minutes + * + * @param duration Event Duration in Minutes + */ + public void setDuration(int duration) { + this.duration = duration; + } + + /** + * String Representation of SVDRPDiskStatus Object + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append("Title: "); + sb.append(title); + sb.append(System.lineSeparator()); + + sb.append("Subtitle: "); + sb.append(subtitle); + sb.append(System.lineSeparator()); + + sb.append("Begin: "); + sb.append(begin); + sb.append(System.lineSeparator()); + + sb.append("End: "); + sb.append(end); + sb.append(System.lineSeparator()); + + if (duration > -1) { + sb.append("Duration: "); + sb.append(duration); + sb.append(System.lineSeparator()); + } + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPException.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPException.java new file mode 100644 index 00000000000..66a8511d804 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link SVDRPException} is thrown in case of Failure of SVDRP Handling + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public abstract class SVDRPException extends Exception { + + private static final long serialVersionUID = 3816136415994156427L; + + public SVDRPException(@Nullable String message) { + super(message); + } + + public SVDRPException(@Nullable String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPParseResponseException.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPParseResponseException.java new file mode 100644 index 00000000000..33fad17721f --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPParseResponseException.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link SVDRPParseResponseException} is thrown if a SVDRP Response cannot be parsed as expected + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class SVDRPParseResponseException extends SVDRPException { + + private static final long serialVersionUID = 631229205838438373L; + + public SVDRPParseResponseException(SVDRPResponse response) { + super(response.getMessage()); + } + + public SVDRPParseResponseException(SVDRPResponse response, Throwable cause) { + super(response.getMessage(), cause); + } + + public SVDRPParseResponseException(@Nullable String message) { + super(message); + String newMessage = message; + if (newMessage == null) { + newMessage = "Null Value on Exception Message"; + } + } + + public SVDRPParseResponseException(@Nullable String message, Throwable cause) { + super(message, cause); + String newMessage = message; + if (newMessage == null) { + newMessage = "Null Value on Exception Message"; + } + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPResponse.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPResponse.java new file mode 100644 index 00000000000..a37221dd936 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPResponse.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SVDRPResponse} represents a general Object returned by an SVDRP Client Call + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class SVDRPResponse { + private int code; + private String message; + + public SVDRPResponse(int code, String response) { + this.code = code; + this.message = response; + } + + /** + * Get Status Code of SVDRP Response + * + * @return Status Code of SVDRP Response + */ + public int getCode() { + return code; + } + + /** + * Get Message of SVDRP Response + * + * @return Message of SVDRP Response + */ + public String getMessage() { + return message; + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPTimerList.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPTimerList.java new file mode 100644 index 00000000000..243aca983e2 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPTimerList.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SVDRPTimerList} contains SVDRP Response Data for a Timer list + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class SVDRPTimerList { + + private List timers = new ArrayList(); + + /** + * parse object from SVDRP Client Response + * + * @param message SVDRP Client Response + * @return Timer List Object + * @throws SVDRPParseResponseException thrown if response data is not parseable + */ + public static SVDRPTimerList parse(String message) { + SVDRPTimerList timers = new SVDRPTimerList(); + List lines = new ArrayList(); + + StringTokenizer st = new StringTokenizer(message, System.lineSeparator()); + while (st.hasMoreTokens()) { + String timer = st.nextToken(); + lines.add(timer); + } + timers.setTimers(lines); + return timers; + } + + /** + * Is there currently an active Recording on SVDRP Client + * + * @return returns true if there is an active recording + */ + public boolean isRecordingActive() { + for (String line : timers) { + String timerContent = line.substring(line.indexOf(" ") + 1); + String timerStatus = timerContent.substring(0, timerContent.indexOf(":")); + byte b = Byte.parseByte(timerStatus); + if (((b >> 3) & 0x0001) == 1) { + return true; + } + } + return false; + } + + /** + * Set timers object of SVDRPTimerList + * + * @param timers timers to set + */ + private void setTimers(List timers) { + this.timers = timers; + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPVolume.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPVolume.java new file mode 100644 index 00000000000..7638b388487 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPVolume.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SVDRPVolume} contains SVDRP Response Data for Volume Object + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class SVDRPVolume { + + private int volume = -1; + + private SVDRPVolume() { + } + + /** + * parse object from SVDRP Client Response + * + * @param message SVDRP Client Response + * @return Volume Object + * @throws SVDRPParseResponseException thrown if response data is not parseable + */ + public static SVDRPVolume parse(String message) throws SVDRPParseResponseException { + SVDRPVolume volume = new SVDRPVolume(); + try { + String vol = message.substring(message.lastIndexOf(" ") + 1, message.length()); + if ("mute".equals(vol)) { + volume.setVolume(0); + } else { + int val = Integer.parseInt(vol); + val = val * 100 / 255; + volume.setVolume(val); + } + } catch (NumberFormatException nex) { + throw new SVDRPParseResponseException(nex.getMessage(), nex); + } catch (IndexOutOfBoundsException ie) { + throw new SVDRPParseResponseException(ie.getMessage(), ie); + } + return volume; + } + + /** + * Get Volume in Percent + * + * @param volume Volume in Percent + */ + private void setVolume(int volume) { + this.volume = volume; + } + + /** + * Set Volume in Percent + * + * @return Volume in Percent + */ + public int getVolume() { + return volume; + } + + /** + * String Representation of SVDRPDiskStatus Object + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (volume > -1) { + sb.append("Volume: "); + sb.append(String.valueOf(volume)); + } + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPWelcome.java b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPWelcome.java new file mode 100644 index 00000000000..0abdc13f490 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/java/org/openhab/binding/vdr/internal/svdrp/SVDRPWelcome.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal.svdrp; + +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SVDRPWelcome} contains SVDRP Response Data that is sent after Connection has been established + * + * @author Matthias Klocke - Initial contribution + */ +@NonNullByDefault +public class SVDRPWelcome { + + private String version = ""; + private String charset = ""; + private String dateAndTime = ""; + + private SVDRPWelcome() { + } + + /** + * Parse SVDRPResponse into SVDRPWelcome Object + * + * @param message SVDRP Client Response + * Example: VDRHOST SVDRP VideoDiskRecorder 2.4.5; Sat Jan 9 22:28:11 2021; UTF-8 + * @return Welcome Object + * @throws SVDRPParseResponseException thrown if response data is not parseable + */ + public static SVDRPWelcome parse(String message) throws SVDRPParseResponseException { + SVDRPWelcome welcome = new SVDRPWelcome(); + StringTokenizer st = new StringTokenizer(message, ";"); + try { + String hostAndVersion = st.nextToken(); + String dateAndTime = st.nextToken(); + String charset = st.nextToken(); + welcome.setCharset(charset.trim()); + welcome.setVersion(hostAndVersion.substring(hostAndVersion.lastIndexOf(" ")).trim()); + welcome.setDateAndTime(dateAndTime.trim()); + } catch (NoSuchElementException nex) { + throw new SVDRPParseResponseException(nex.getMessage(), nex); + } + return welcome; + } + + /** + * Get VDR version + * + * @return VDR version String + */ + public String getVersion() { + return version; + } + + /** + * Set VDR version + * + * @param version VDR version String + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * Get VDR Charset + * + * @return VDR charset + */ + public String getCharset() { + return charset; + } + + /** + * Set VDR Charset + * + * @param charset VDR charset + */ + public void setCharset(String charset) { + this.charset = charset; + } + + /** + * Get VDR Date and Time String + * + * @return VDR Date and Time String + */ + public String getDateAndTime() { + return dateAndTime; + } + + /** + * Set VDR Date and Time String + * + * @param dateAndTime VDR Date and Time String + */ + public void setDateAndTime(String dateAndTime) { + this.dateAndTime = dateAndTime; + } +} diff --git a/bundles/org.openhab.binding.vdr/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.vdr/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 00000000000..f97e25a9f1d --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + VDR Binding + The Video Disk Recorder (VDR) binding allows to control your own Video Disk Recorder + (https://www.tvdr.de). + + diff --git a/bundles/org.openhab.binding.vdr/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.vdr/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..7e949504f48 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,202 @@ + + + + + + VDR - The Video Disk Recorder (https://tvdr.de) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VDR Version + + + + + network-address + + Hostname or IP Address of VDR instance + + + + SVDRP Port of VDR instance + 6419 + + + + Interval in seconds the data from VDR instance is refreshed + true + 30 + + + + + + + String + + Send Message to be displayed on VDR + + + + Number + + Current Channel Number + + + + String + + Current Channel Name + + + + Switch + + ON if a recording is active + + + + Number + + Current Disk Usage in % + + + + String + + Send Key Code of Remote Control to VDR + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + Title of EPG Event + + + + String + + Sub Title of EPG Event + + + + DateTime + + Start Time of EPG Event + + + + DateTime + + End Time of EPG Event + + + + Number:Time + + Duration of EPG Event in Minutes + + + + diff --git a/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPChannelTest.java b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPChannelTest.java new file mode 100644 index 00000000000..3c4b12b3985 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPChannelTest.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.vdr.internal.svdrp.SVDRPChannel; +import org.openhab.binding.vdr.internal.svdrp.SVDRPException; +import org.openhab.binding.vdr.internal.svdrp.SVDRPParseResponseException; + +/** + * Specific unit tests to check if {@link SVDRPChannel} parses SVDRP responses correctly + * + * @author Matthias Klocke - Initial contribution + * + */ +@NonNullByDefault +public class SVDRPChannelTest { + + private final String channelResponseOk = "3 WDR HD Bielefeld"; + private final String channelResponseParseError = "250WDR HD Bielefeld"; + + @Test + public void testParseChannelData() throws SVDRPException { + SVDRPChannel channel = SVDRPChannel.parse(channelResponseOk); + assertEquals("WDR HD Bielefeld", channel.getName()); + assertEquals(3, channel.getNumber()); + } + + @Test + public void testParseExceptionChannelData() { + assertThrows(SVDRPParseResponseException.class, () -> { + SVDRPChannel.parse(channelResponseParseError); + }); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPDiskStatusTest.java b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPDiskStatusTest.java new file mode 100644 index 00000000000..695a0f43ee4 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPDiskStatusTest.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.vdr.internal.svdrp.SVDRPChannel; +import org.openhab.binding.vdr.internal.svdrp.SVDRPDiskStatus; +import org.openhab.binding.vdr.internal.svdrp.SVDRPException; +import org.openhab.binding.vdr.internal.svdrp.SVDRPParseResponseException; + +/** + * Specific unit tests to check if {@link SVDRPDiskStatus} parses SVDRP responses correctly + * + * @author Matthias Klocke - Initial contribution + * + */ +@NonNullByDefault +public class SVDRPDiskStatusTest { + + private final String diskStatusResponseOk = "411266MB 30092MB 92%"; + private final String diskStatusResponseParseError1 = "411266MB 30092MB 92%"; + private final String diskStatusResponseParseError2 = "411266MB 30092 92%"; + private final String diskStatusResponseParseError3 = "42b3MB 30092MB 92%"; + + @Test + public void testParseDiskStatus() throws SVDRPException { + SVDRPDiskStatus diskStatus = SVDRPDiskStatus.parse(diskStatusResponseOk); + assertEquals(411266, diskStatus.getMegaBytesTotal()); + assertEquals(30092, diskStatus.getMegaBytesFree()); + assertEquals(92, diskStatus.getPercentUsed()); + } + + @Test + public void testParseExceptionDiskStatus() { + assertThrows(SVDRPParseResponseException.class, () -> { + SVDRPChannel.parse(diskStatusResponseParseError1); + }); + assertThrows(SVDRPParseResponseException.class, () -> { + SVDRPChannel.parse(diskStatusResponseParseError2); + }); + assertThrows(SVDRPParseResponseException.class, () -> { + SVDRPChannel.parse(diskStatusResponseParseError3); + }); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPEpgEventTest.java b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPEpgEventTest.java new file mode 100644 index 00000000000..90715f9d268 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPEpgEventTest.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.text.ParseException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.vdr.internal.svdrp.SVDRPEpgEvent; +import org.openhab.binding.vdr.internal.svdrp.SVDRPException; +import org.openhab.binding.vdr.internal.svdrp.SVDRPParseResponseException; + +/** + * Specific unit tests to check if {@link SVDRPEpgEvent} parses SVDRP responses correctly + * + * @author Matthias Klocke - Initial contribution + * + */ +@NonNullByDefault +public class SVDRPEpgEventTest { + private final String epgResponseComplete = "C S19.2E-1-1201-28326 WDR HD Bielefeld\n" + + "E 9886 1610391600 900 4E F\n" + "T Tagesschau\n" + "S Aktuelle Nachrichten aus der Welt\n" + + "D Themen u.a.:|* Corona-Pandemie in Deutschland: Verschärfter Lockdown bundesweit in Kraft|* Entmachtung des US-Präsidenten: Demokraten planen Schritte gegen Trump|* Wintereinbruch in Bosnien-Herzegowina: Dramatische Lage der Flüchtlinge an der Grenze zu Kroatien\n" + + "G 20 80\n" + "X 2 03 deu stereo\n" + "X 2 03 deu ohne Audiodeskription\n" + + "X 3 01 deu Teletext-Untertitel\n" + "X 3 20 deu mit DVB-Untertitel\n" + "X 5 0B deu HD-Video\n" + + "V 1610391600\n" + "e\n" + "c\n" + "End of EPG data"; + private final String epgMissingSubtitle = "C S19.2E-1-1201-28326 WDR HD Bielefeld\n" + + "E 9886 1610391600 900 4E F\n" + "T Tagesschau\n" + + "D Themen u.a.:|* Corona-Pandemie in Deutschland: Verschärfter Lockdown bundesweit in Kraft|* Entmachtung des US-Präsidenten: Demokraten planen Schritte gegen Trump|* Wintereinbruch in Bosnien-Herzegowina: Dramatische Lage der Flüchtlinge an der Grenze zu Kroatien\n" + + "G 20 80\n" + "X 2 03 deu stereo\n" + "X 2 03 deu ohne Audiodeskription\n" + + "X 3 01 deu Teletext-Untertitel\n" + "X 3 20 deu mit DVB-Untertitel\n" + "X 5 0B deu HD-Video\n" + + "V 1610391600\n" + "e\n" + "c\n" + "End of EPG data"; + private final String epgParseError = "E 9999999999999999999999999"; + private final String epgCorruptDate = "C S19.2E-1-1201-28326 WDR HD Bielefeld\n" + "E 9886 2a10391600 900 4E F\n" + + "T Tagesschau\n" + + "D Themen u.a.:|* Corona-Pandemie in Deutschland: Verschärfter Lockdown bundesweit in Kraft|* Entmachtung des US-Präsidenten: Demokraten planen Schritte gegen Trump|* Wintereinbruch in Bosnien-Herzegowina: Dramatische Lage der Flüchtlinge an der Grenze zu Kroatien\n" + + "G 20 80\n" + "X 2 03 deu stereo\n" + "X 2 03 deu ohne Audiodeskription\n" + + "X 3 01 deu Teletext-Untertitel\n" + "X 3 20 deu mit DVB-Untertitel\n" + "X 5 0B deu HD-Video\n" + + "V 1610391600\n" + "e\n" + "c\n" + "End of EPG data"; + + private final String epgMissingEnd = "C S19.2E-1-1201-28326 WDR HD Bielefeld\n" + "E 9886 1610391600 900 4E F\n" + + "T Tagesschau\n" + + "D Themen u.a.:|* Corona-Pandemie in Deutschland: Verschärfter Lockdown bundesweit in Kraft|* Entmachtung des US-Präsidenten: Demokraten planen Schritte gegen Trump|* Wintereinbruch in Bosnien-Herzegowina: Dramatische Lage der Flüchtlinge an der Grenze zu Kroatien\n" + + "G 20 80\n" + "X 2 03 deu stereo\n" + "X 2 03 deu ohne Audiodeskription\n" + + "X 3 01 deu Teletext-Untertitel\n" + "X 3 20 deu mit DVB-Untertitel\n" + "X 5 0B deu HD-Video\n" + + "V 1610391600\n" + "e\n" + "c\n"; + + @Test + public void testParseEpgEventComplete() throws SVDRPException, ParseException { + SVDRPEpgEvent event = SVDRPEpgEvent.parse(epgResponseComplete); + assertEquals("Tagesschau", event.getTitle()); + assertEquals("Aktuelle Nachrichten aus der Welt", event.getSubtitle()); + assertEquals(15, event.getDuration()); + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); + assertEquals(ZonedDateTime.parse("2021-01-11 19:00:00 UTC", dtf).toInstant(), event.getBegin()); + assertEquals(ZonedDateTime.parse("2021-01-11 19:15:00 UTC", dtf).toInstant(), event.getEnd()); + } + + @Test + public void testParseEpgEventMissingSubtitle() throws SVDRPException { + SVDRPEpgEvent event = SVDRPEpgEvent.parse(epgMissingSubtitle); + assertEquals("Tagesschau", event.getTitle()); + assertEquals("", event.getSubtitle()); + } + + @Test + public void testParseEpgEventCorruptDate() { + assertThrows(SVDRPParseResponseException.class, () -> { + SVDRPEpgEvent.parse(epgCorruptDate); + }); + } + + @Test + public void testParseEpgEventMissingEnd() throws SVDRPException, ParseException { + SVDRPEpgEvent event = SVDRPEpgEvent.parse(epgMissingEnd); + assertEquals("Tagesschau", event.getTitle()); + assertEquals("", event.getSubtitle()); + assertEquals(15, event.getDuration()); + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); + assertEquals(ZonedDateTime.parse("2021-01-11 19:00:00 UTC", dtf).toInstant(), event.getBegin()); + assertEquals(ZonedDateTime.parse("2021-01-11 19:15:00 UTC", dtf).toInstant(), event.getEnd()); + } + + @Test + public void testParseExceptionVolumeData() { + assertThrows(SVDRPParseResponseException.class, () -> { + SVDRPEpgEvent.parse(epgParseError); + }); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPTimerListTest.java b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPTimerListTest.java new file mode 100644 index 00000000000..0bcc9e6ec5e --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPTimerListTest.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.vdr.internal.svdrp.SVDRPException; +import org.openhab.binding.vdr.internal.svdrp.SVDRPTimerList; + +/** + * Specific unit tests to check if {@link SVDRPTimerList} parses SVDRP responses correctly + * + * @author Matthias Klocke - Initial contribution + * + */ +@NonNullByDefault +public class SVDRPTimerListTest { + private final String timerListResponseTimerActive = "1 1:1:2021-01-12:2013:2110:50:99:Charité (1/6)~Eiserne Lunge:Test\n" + + "2 9:1:2021-01-12:2058:2200:50:99:Charité (2/6)~Blutsauger:Test"; + private final String timerListResponseTimerNotActive = "1 1:1:2021-01-12:2013:2110:50:99:Charité (1/6)~Eiserne Lunge:Test\n" + + "2 1:1:2021-01-12:2058:2200:50:99:Charité (2/6)~Blutsauger:Test"; + + @Test + public void testParseTimerList() throws SVDRPException { + SVDRPTimerList list = SVDRPTimerList.parse(timerListResponseTimerActive); + assertEquals(true, list.isRecordingActive()); + list = SVDRPTimerList.parse(timerListResponseTimerNotActive); + assertEquals(false, list.isRecordingActive()); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPVolumeTest.java b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPVolumeTest.java new file mode 100644 index 00000000000..fe1770cb7b0 --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPVolumeTest.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.vdr.internal.svdrp.SVDRPException; +import org.openhab.binding.vdr.internal.svdrp.SVDRPParseResponseException; +import org.openhab.binding.vdr.internal.svdrp.SVDRPVolume; + +/** + * Specific unit tests to check if {@link SVDRPVolume} parses SVDRP responses correctly + * + * @author Matthias Klocke - Initial contribution + * + */ +@NonNullByDefault +public class SVDRPVolumeTest { + private final String volumeResponseOk = "Audio volume is 255"; + private final String volumeResponseMute = "Audio is mute"; + private final String volumeResponseParseError1 = "Audiovolumeis255"; + private final String volumeResponseParseError2 = "Audio volume is 255x"; + + @Test + public void testParseVolumeData() throws SVDRPException { + SVDRPVolume volume = SVDRPVolume.parse(volumeResponseOk); + assertEquals(100, volume.getVolume()); + volume = SVDRPVolume.parse(volumeResponseMute); + assertEquals(0, volume.getVolume()); + } + + @Test + public void testParseExceptionVolumeData() { + assertThrows(SVDRPParseResponseException.class, () -> { + SVDRPVolume.parse(volumeResponseParseError1); + }); + assertThrows(SVDRPParseResponseException.class, () -> { + SVDRPVolume.parse(volumeResponseParseError2); + }); + } +} diff --git a/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPWelcomeTest.java b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPWelcomeTest.java new file mode 100644 index 00000000000..f293600e37a --- /dev/null +++ b/bundles/org.openhab.binding.vdr/src/test/java/org/openhab/binding/vdr/internal/SVDRPWelcomeTest.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2021 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.vdr.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.vdr.internal.svdrp.SVDRPException; +import org.openhab.binding.vdr.internal.svdrp.SVDRPParseResponseException; +import org.openhab.binding.vdr.internal.svdrp.SVDRPVolume; +import org.openhab.binding.vdr.internal.svdrp.SVDRPWelcome; + +/** + * Specific unit tests to check if {@link SVDRPWelcome} parses SVDRP responses correctly + * + * @author Matthias Klocke - Initial contribution + * + */ +@NonNullByDefault +public class SVDRPWelcomeTest { + private final String welcomeResponseOk = "srv SVDRP VideoDiskRecorder 2.5.1; Mon Jan 11 19:46:54 2021; UTF-8"; + private final String welcomeResponseParseError1 = "srv SVDRP VideoDiskRecorder 2.5.1; Mon Jan 11 19:46:54 2021 UTF-8"; + private final String welcomeResponseParseError2 = "srv SVDRP VideoDiskRecorder2.5.1; Mon Jan 11 19:46:54 2021 UTF-8"; + + @Test + public void testParseWelcomeData() throws SVDRPException { + SVDRPWelcome welcome = SVDRPWelcome.parse(welcomeResponseOk); + assertEquals("UTF-8", welcome.getCharset()); + assertEquals("2.5.1", welcome.getVersion()); + assertEquals("Mon Jan 11 19:46:54 2021", welcome.getDateAndTime()); + } + + @Test + public void testParseExceptionVolumeData() { + assertThrows(SVDRPParseResponseException.class, () -> { + SVDRPVolume.parse(welcomeResponseParseError1); + }); + assertThrows(SVDRPParseResponseException.class, () -> { + SVDRPVolume.parse(welcomeResponseParseError2); + }); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index de64d0d896d..58f7f8ead97 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -318,6 +318,7 @@ org.openhab.binding.upb org.openhab.binding.urtsi org.openhab.binding.valloxmv + org.openhab.binding.vdr org.openhab.binding.vektiva org.openhab.binding.velbus org.openhab.binding.velux