diff --git a/CODEOWNERS b/CODEOWNERS
index f6a3d79739bd6..74a066b65b725 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -95,6 +95,7 @@
/bundles/org.openhab.binding.ipp/ @peuter
/bundles/org.openhab.binding.irtrans/ @kgoderis
/bundles/org.openhab.binding.jeelink/ @vbier
+/bundles/org.openhab.binding.kaleidescape/ @mlobstein
/bundles/org.openhab.binding.keba/ @kgoderis
/bundles/org.openhab.binding.km200/ @Markinus
/bundles/org.openhab.binding.knx/ @sjka
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index d3786db8ddfd4..34ebff596168f 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -476,6 +476,11 @@
org.openhab.binding.jeelink${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.kaleidescape
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.keba
diff --git a/bundles/org.openhab.binding.kaleidescape/.classpath b/bundles/org.openhab.binding.kaleidescape/.classpath
new file mode 100644
index 0000000000000..d223a57c7967a
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/.classpath
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.kaleidescape/.project b/bundles/org.openhab.binding.kaleidescape/.project
new file mode 100644
index 0000000000000..be2900014f2b8
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.binding.kaleidescape
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.binding.kaleidescape/NOTICE b/bundles/org.openhab.binding.kaleidescape/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/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.kaleidescape/README.md b/bundles/org.openhab.binding.kaleidescape/README.md
new file mode 100644
index 0000000000000..c9e53853d17e9
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/README.md
@@ -0,0 +1,439 @@
+# Kaleidescape Binding
+
+![Kaleidescape logo](doc/Kaleidescape_Logo.png)
+
+This binding is used to control and retrieve information from a Kaleidescape movie player.
+All movie player components including the original K-Player series, M Class Players, Cinema One, Alto, and Strato are supported.
+The 4 zone audio only KMUSIC-4000 is not supported at this time.
+As there are many good control options already available for these components, this binding focuses primarily on retrieving information
+ for display purposes and to use in rules for controlling other Things such lighting, projector lens control, masking, etc.
+Basic playback transport controls are provided and any other command that is supported by the control protocol can be sent to the component through rules based commands.
+See [Kaleidescape-System-Control-Protocol-Reference-Manual.pdf](https://support.kaleidescape.com/article/Control-Protocol-Reference-Manual) for a reference of available commands.
+To simplify the design of the binding code, a different Thing instance is created for each component
+ in a multi-zone system and each Thing maintains its own socket connection to the target component.
+Overall this binding supports the majority of information and commands available in the Kaleidescape control protocol but is by no means exhaustive.
+Any feedback or suggestions for improvement are welcome.
+
+The binding supports two different kinds of connections:
+
+* direct IP connection (preferred),
+* serial connection (19200-8-N-1)
+
+## Supported Things
+
+There is exactly one supported thing type, which represents an individual Kaleidescape component.
+It has the `player` id.
+
+## Discovery
+
+Manually initiated Auto-discovery is supported if Kaleidescape components are accessible on the same IP subnet of the openHAB server.
+Since discovery involves scanning all IP addresses in the subnet range for an open socket, the discovery must be initiated by the user.
+In the Inbox, select Search For Things and then choose the Kaleidescape System Binding to initiate discovery.
+
+## Binding Configuration
+
+There are no overall binding configuration settings that need to be set.
+All settings are through thing configuration parameters.
+
+## Thing Configuration
+
+The thing has the following configuration parameters:
+
+| Parameter Label | Parameter ID | Description | Accepted values |
+|------------------------|---------------|------------------------------------------------------------------------------------|------------------------------------------------------|
+| Component type | componentType | The type of Kaleidescape component | 'Player', 'Cinema One', 'Alto', or 'Strato' |
+| Address | host | Host name or IP address of the Kaleidescape component | A host name or IP address |
+| Port | port | Communication port of the IP connection | 10000 (default - should not need to change) |
+| Serial Port | serialPort | Serial port for connecting directly a component | Serial port name (optional) |
+| Update Period | updatePeriod | Tells the component how often time status updates should be sent (see notes below) | 0 or 1 are the currently accepted values (default 0) |
+| Volume Control Enabled | volumeEnabled | Enable the volume and mute controls in the K iPad & phone apps | Boolean (default false) |
+| Initial Volume Setting | initialVolume | Initial volume level set when the binding starts up | 0 to 75 (default 25) |
+
+Some notes:
+
+* Due to a bug in the control protocol, a Strato C player will be identified as a Premiere 'Player' by the auto discovery process.
+* The thing configuration parameter 'Component type' should be manually updated to correctly identify the Strato C as a 'Strato' component.
+* The only caveat of note about this binding is the updatePeriod configuration parameter.
+* When set to the default of 0, the component only sends running time update messages sporadically (as an example: when the movie chapter changes) while content is playing.
+* In this case, the running time channels will also only sporadically update.
+* When updatePeriod is set to 1 (values greater than 1 are not yet supported by the control protocol), the component sends running time status update messages every second.
+* Be aware that this could cause performance impacts to your openHAB system.
+
+* On Linux, you may get an error stating the serial port cannot be opened when the Kaleidescape binding tries to load.
+* You can get around this by adding the `openhab` user to the `dialout` group like this: `usermod -a -G dialout openhab`.
+* Also on Linux you may have issues with the USB if using two serial USB devices e.g. Kaleidescape and RFXcom.
+* See the [general documentation about serial port configuration](/docs/administration/serial.html) for more on symlinking the USB ports.
+
+## Channels
+
+The following channels are available:
+
+| Channel ID | Item Type | Description |
+|----------------------------|-------------|------------------------------------------------------------------------------------------------------------------|
+| ui#power | Switch | Turn the zone On or Off (system standby) |
+| ui#volume | Dimmer | A virtual volume that tracks the volume in K control apps, use as a proxy to adjust a real volume item via rules |
+| ui#mute | Switch | A virtual mute switch that tracks the mute status in K control apps, use as a proxy to control a real mute item |
+| ui#control | Player | Control Movie Playback e.g. start/pause/next/previous/ffward/rewind |
+| ui#title_name | String | The title of the movie currently playing |
+| ui#play_mode | String | The current playback mode of the movie |
+| ui#play_speed | String | The speed of playback scanning |
+| ui#title_num | Number | The current movie title number that is playing |
+| ui#title_length | Number:Time | The total running time of the currently playing movie (seconds) |
+| ui#title_loc | Number:Time | The running time elapsed of the currently playing movie (seconds) |
+| ui#chapter_num | Number | The current chapter number of the movie that is playing |
+| ui#chapter_length | Number:Time | The total running time of the current chapter (seconds) |
+| ui#chapter_loc | Number:Time | The running time elapsed of the current chapter |
+| ui#movie_media_type | String | The type of media that is currently playing |
+| ui#movie_location | String | Identifies the location in the movie, ie: Main content, Intermission, or End Credits |
+| ui#aspect_ratio | String | Identifies the aspect ratio of the movie |
+| ui#video_mode | String | Raw output of video mode data from the component, format: 00:00:00 |
+| ui#video_mode_composite | String | Identifies the video mode currently active on the composite video output |
+| ui#video_mode_component | String | Identifies the video mode currently active on the component video output |
+| ui#video_mode_hdmi | String | Identifies the video mode currently active on the HDMI video output |
+| ui#video_color | String | Provides color information about the current video output (Strato Only) |
+| ui#video_color_eotf | String | Identifies the Electro-Optical Transfer Function standard of the current video output (Strato Only) |
+| ui#content_color | String | Provides color information about the currently playing content (Strato Only) |
+| ui#content_color_eotf | String | Identifies the Electro-Optical Transfer Function standard of the currently playing content (Strato Only) |
+| ui#scale_mode | String | Identifies whether the image from the player requires scaling |
+| ui#screen_mask | String | Provides aspect ratio and masking information for the current video image |
+| ui#screen_mask2 | String | Provides masking information based on aspect ratio and overscan area |
+| ui#cinemascape_mask | String | When in CinemaScape mode, provides information about the frame aspect ratio |
+| ui#cinemascape_mode | String | Identifies the CinemaScape mode currently active |
+| ui#ui_state | String | Provides information about which screen is visible in the Kaleidescape user interface |
+| ui#child_mode_state | String | Indicates if the onscreen display is displaying the child user interface |
+| ui#readiness_state | String | Indicates the system's current idle mode (Not available on Premiere system players) |
+| ui#highlighted_selection | String | Specifies the handle of the movie or album currently selected on the user interface |
+| ui#user_defined_event | String | Will contain custom event messages generated by scripts, sent from another component, or system events |
+| ui#user_input | String | Indicates if the user is being prompted for input, what type of input, and any currently entered characters |
+| ui#user_input_prompt | String | Indicates user input prompt info and properties currently shown on screen |
+| -- music channels (not available on Alto and Strato) -- |
+| music#control | Player | Control Music Playback e.g. start/pause/next/previous/ffward/rewind |
+| music#repeat | Switch | Controls repeat playback for music |
+| music#random | Switch | Controls random playback for music |
+| music#track | String | The name of the currently playing track |
+| music#artist | String | The name of the currently playing artist |
+| music#album | String | The name of the currently playing album |
+| music#play_mode | String | The current playback mode of the music |
+| music#play_speed | String | The speed of playback scanning |
+| music#track_length | Number:Time | The total running time of the current playing track (seconds) |
+| music#track_position | Number:Time | The running time elapsed of the current playing track (seconds) |
+| music#track_progress | Number | The percentage complete of the current playing track |
+| music#track_handle | String | The handle of the currently playing track |
+| music#album_handle | String | The handle of the currently playing album |
+| music#nowplay_handle | String | The handle of the current now playing list |
+| -- metadata display channels (music related channels not available on Alto and Strato) -- |
+| detail#type | String | Indicates if the currently selected item is a Movie or Album |
+| detail#title | String | The title of the selected movie |
+| detail#album_title | String | The title of the selected album |
+| detail#cover_art | Image | Cover art image of the currently selected item |
+| detail#cover_url | String | The url of the cover art |
+| detail#hires_cover_url | String | The url of the high resolution cover art |
+| detail#rating | String | The MPAA rating of the selected movie |
+| detail#year | String | The release year of the selected item |
+| detail#running_time | Number:Time | The total running time of the selected item (seconds) |
+| detail#actors | String | A list of actors appearing in the selected movie |
+| detail#artist | String | The artist of the selected album |
+| detail#directors | String | A list of directors of the selected movie |
+| detail#genres | String | A list of genres of the selected item |
+| detail#rating_reason | String | An explaination of why the selected movie received its rating |
+| detail#synopsis | String | A synopsis of the selected movie |
+| detail#review | String | A review of the selected album |
+| detail#color_description | String | Indicates if the selected movie is in Color, Black and White, etc. |
+| detail#country | String | The country that the selected movie originates from |
+| detail#aspect_ratio | String | The aspect ratio of the selected movie |
+| detail#disc_location | String | Indicates where the disc for the selected item is currently residing in the system (ie Vault, Tray, etc.) |
+
+
+## Full Example
+
+kaleidescape.things:
+
+```java
+kaleidescape:player:myzone1 "M500 Living Rm" [componentType="Player", host="192.168.1.10", updatePeriod=0, volumeEnabled=true, initialVolume=20]
+kaleidescape:player:myzone2 "My Cinema One" [componentType="Cinema One", host="192.168.1.11", updatePeriod=0, volumeEnabled=true, initialVolume=20]
+```
+
+kaleidescape.items:
+
+```java
+// Virtual switch to send a command, see sitemap and rules below
+Switch z1_GoMovieCovers "Go to Movie Covers"
+
+// Movie Channels
+Switch z1_Ui_Power "Power" { channel="kaleidescape:player:myzone1:ui#power" }
+Dimmer z1_Ui_Volume "Volume" { channel="kaleidescape:player:myzone1:ui#volume" }
+Switch z1_Ui_Mute "Mute" { channel="kaleidescape:player:myzone1:ui#mute" }
+Player z1_Ui_Control "Control" { channel="kaleidescape:player:myzone1:ui#control" }
+String z1_Ui_TitleName "Movie Title: [%s]" { channel="kaleidescape:player:myzone1:ui#title_name" }
+String z1_Ui_PlayMode "Play Mode: [%s]" { channel="kaleidescape:player:myzone1:ui#play_mode" }
+String z1_Ui_PlaySpeed "Play Speed: [%s]" { channel="kaleidescape:player:myzone1:ui#play_speed" }
+Number z1_Ui_TitleNum "Title Number: [%s]" { channel="kaleidescape:player:myzone1:ui#title_num" }
+Number:Time z1_Ui_TitleLength "Title Length: [JS(ksecondsformat.js):%s]" { channel="kaleidescape:player:myzone1:ui#title_length" }
+Number:Time z1_Ui_TitleLoc "Title Location: [JS(ksecondsformat.js):%s]" { channel="kaleidescape:player:myzone1:ui#title_loc" }
+Number z1_Ui_ChapterNum "Chapter Number: [%s]" { channel="kaleidescape:player:myzone1:ui#chapter_num" }
+Number:Time z1_Ui_ChapterLength "Chapter Length: [JS(ksecondsformat.js):%s]" { channel="kaleidescape:player:myzone1:ui#chapter_length" }
+Number:Time z1_Ui_ChapterLoc "Chapter Location: [JS(ksecondsformat.js):%s]" { channel="kaleidescape:player:myzone1:ui#chapter_loc" }
+String z1_Ui_MovieMediaType "Media Type: [%s]" { channel="kaleidescape:player:myzone1:ui#movie_media_type" }
+String z1_Ui_MovieLocation "Movie Location: [%s]" { channel="kaleidescape:player:myzone1:ui#movie_location" }
+String z1_Ui_AspectRatio "Aspect Ratio: [%s]" { channel="kaleidescape:player:myzone1:ui#aspect_ratio" }
+String z1_Ui_VideoMode "Video Mode (raw): [%s]" { channel="kaleidescape:player:myzone1:ui#video_mode" }
+String z1_Ui_VideoModeComposite "Video Mode (Composite): [%s]" { channel="kaleidescape:player:myzone1:ui#video_mode_composite" }
+String z1_Ui_VideoModeComponent "Video Mode (Component): [%s]" { channel="kaleidescape:player:myzone1:ui#video_mode_component" }
+String z1_Ui_VideoModeHdmi "Video Mode (HDMI): [%s]" { channel="kaleidescape:player:myzone1:ui#video_mode_hdmi" }
+// Video Color and Content Color only available on the Strato
+String z1_Ui_VideoColor "Video Color: [%s]" { channel="kaleidescape:player:myzone1:ui#video_color" }
+String z1_Ui_VideoColorEotf "Video Color EOTF: [%s]" { channel="kaleidescape:player:myzone1:ui#video_color_eotf" }
+String z1_Ui_ContentColor "Content Color: [%s]" { channel="kaleidescape:player:myzone1:ui#content_color" }
+String z1_Ui_ContentColorEotf "Content Color EOTF: [%s]" { channel="kaleidescape:player:myzone1:ui#content_color_eotf" }
+String z1_Ui_ScaleMode "Scale Mode: [%s]" { channel="kaleidescape:player:myzone1:ui#scale_mode" }
+String z1_Ui_ScreenMask "Screen Mask: [%s]" { channel="kaleidescape:player:myzone1:ui#screen_mask" }
+String z1_Ui_ScreenMask2 "Screen Mask 2: [%s]" { channel="kaleidescape:player:myzone1:ui#screen_mask2" }
+String z1_Ui_CinemascapeMask "CinemaScape Mask: [%s]" { channel="kaleidescape:player:myzone1:ui#cinemascape_mask" }
+String z1_Ui_CinemascapeMode "CinemaScape Mode: [%s]" { channel="kaleidescape:player:myzone1:ui#cinemascape_mode" }
+String z1_Ui_UiState "UI State: [%s]" { channel="kaleidescape:player:myzone1:ui#ui_state" }
+String z1_Ui_ChildModeState "Child Mode State: [%s]" { channel="kaleidescape:player:myzone1:ui#child_mode_state" }
+String z1_Ui_ReadinessState "Readiness State: [%s]" { channel="kaleidescape:player:myzone1:ui#readiness_state" }
+String z1_Ui_HighlightedSelection "Highlighted Selection: [%s]" { channel="kaleidescape:player:myzone1:ui#highlighted_selection" }
+String z1_Ui_UserDefinedEvent "User Defined Event: [%s]" { channel="kaleidescape:player:myzone1:ui#user_defined_event" }
+String z1_Ui_UserInput "User Input: [%s]" { channel="kaleidescape:player:myzone1:ui#user_input" }
+String z1_Ui_UserInputPrompt "User Input Prompt[%s]" { channel="kaleidescape:player:myzone1:ui#user_input_prompt" }
+
+// Music Channels (not available on Alto or Strato)
+Player z1_Music_Control "Music Control" { channel="kaleidescape:player:myzone1:music#control" }
+Switch z1_Music_Repeat "Repeat" { channel="kaleidescape:player:myzone1:music#repeat" }
+Switch z1_Music_Random "Random" { channel="kaleidescape:player:myzone1:music#random" }
+String z1_Music_Track "Track: [%s]" { channel="kaleidescape:player:myzone1:music#track" }
+String z1_Music_Artist "Artist: [%s]" { channel="kaleidescape:player:myzone1:music#artist" }
+String z1_Music_Album "Album: [%s]" { channel="kaleidescape:player:myzone1:music#album" }
+String z1_Music_PlayMode "Play Mode: [%s]" { channel="kaleidescape:player:myzone1:music#play_mode" }
+String z1_Music_PlaySpeed "Play Speed: [%s]" { channel="kaleidescape:player:myzone1:music#play_speed" }
+Number:Time z1_Music_TrackLength "Track Length: [JS(ksecondsformat.js):%s]" { channel="kaleidescape:player:myzone1:music#track_length" }
+Number:Time z1_Music_TrackPosition "Track Position: [JS(ksecondsformat.js):%s]" { channel="kaleidescape:player:myzone1:music#track_position" }
+Number z1_Music_TrackProgress "Track Progress: [%s %%]" { channel="kaleidescape:player:myzone1:music#track_progress" }
+String z1_Music_TrackHandle "Track Handle: [%s]" { channel="kaleidescape:player:myzone1:music#track_handle" }
+String z1_Music_AlbumHandle "Album Handle: [%s]" { channel="kaleidescape:player:myzone1:music#album_handle" }
+String z1_Music_NowplayHandle "Now Playing Handle: [%s]" { channel="kaleidescape:player:myzone1:music#nowplay_handle" }
+
+// Metatdata Display Channels (Album Title, Artist & Review are not available on Alto or Strato)
+String z1_Detail_Type "Metadata type: [%s]" { channel="kaleidescape:player:myzone1:detail#type" }
+String z1_Detail_Title "Title: [%s]" { channel="kaleidescape:player:myzone1:detail#title" }
+String z1_Detail_AlbumTitle "Album: [%s]" { channel="kaleidescape:player:myzone1:detail#album_title" }
+Image z1_Detail_CoverArt { channel="kaleidescape:player:myzone1:detail#cover_art" }
+String z1_Detail_CoverUrl "[%s]" { channel="kaleidescape:player:myzone1:detail#cover_url" }
+String z1_Detail_HiresCoverUrl "[%s]" { channel="kaleidescape:player:myzone1:detail#hires_cover_url" }
+String z1_Detail_Rating "Rating: [%s]" { channel="kaleidescape:player:myzone1:detail#rating" }
+String z1_Detail_Year "Year: [%s]" { channel="kaleidescape:player:myzone1:detail#year" }
+Number:Time z1_Detail_RunningTime "Running Time: [JS(ksecondsformat.js):%s]" { channel="kaleidescape:player:myzone1:detail#running_time" }
+String z1_Detail_Actors "Actors: [%s]" { channel="kaleidescape:player:myzone1:detail#actors" }
+String z1_Detail_Directors "Directors: [%s]" { channel="kaleidescape:player:myzone1:detail#directors" }
+String z1_Detail_Artist "Artist: [%s]" { channel="kaleidescape:player:myzone1:detail#artist" }
+String z1_Detail_Genres "Genres: [%s]" { channel="kaleidescape:player:myzone1:detail#genres" }
+String z1_Detail_RatingReason "Rating Reason: [%s]" { channel="kaleidescape:player:myzone1:detail#rating_reason" }
+String z1_Detail_Synopsis "Synopsis: [%s]" { channel="kaleidescape:player:myzone1:detail#synopsis" }
+String z1_Detail_Review "Review: [%s]" { channel="kaleidescape:player:myzone1:detail#review" }
+String z1_Detail_ColorDescription "Color Description: [%s]" { channel="kaleidescape:player:myzone1:detail#color_description" }
+String z1_Detail_Country "Country: [%s]" { channel="kaleidescape:player:myzone1:detail#country" }
+String z1_Detail_AspectRatio "Aspect Ratio: [%s]" { channel="kaleidescape:player:myzone1:detail#aspect_ratio" }
+String z1_Detail_DiscLocation "Disc Location: [%s]" { channel="kaleidescape:player:myzone1:detail#disc_location" }
+```
+
+ksecondsformat.js:
+
+```java
+(function(totalSeconds) {
+ if (isNaN(totalSeconds)) {
+ return '-';
+ } else {
+ hours = Math.floor(totalSeconds / 3600);
+ totalSeconds %= 3600;
+ minutes = Math.floor(totalSeconds / 60);
+ seconds = totalSeconds % 60;
+ if ( minutes < 10 ) {
+ minutes = '0' + minutes;
+ }
+ if ( seconds < 10 ) {
+ seconds = '0' + seconds;
+ }
+ return hours + ':' + minutes + ':' + seconds;
+ }
+})(input)
+```
+
+kaleidescape.sitemap:
+
+```perl
+sitemap kaleidescape label="Kaleidescape" {
+ Frame label="Zone 1" {
+ Image item=z1_Detail_CoverArt
+ Text item=z1_Detail_Title visibility=[z1_Detail_Type=="movie"] icon="video"
+ Text item=z1_Detail_Artist visibility=[z1_Detail_Type=="album"] icon="microphone"
+ Text item=z1_Detail_AlbumTitle visibility=[z1_Detail_Type=="album"] icon="soundvolume-0"
+ Text item=z1_Detail_Rating visibility=[z1_Detail_Type=="movie"] icon="none"
+ Text item=z1_Detail_Year visibility=[z1_Detail_Type=="movie", z1_Detail_Type=="album"] icon="none"
+ Text item=z1_Detail_RunningTime visibility=[z1_Detail_Type=="movie", z1_Detail_Type=="album"] icon="time"
+ Text item=z1_Detail_Actors visibility=[z1_Detail_Type=="movie"] icon="none"
+ Text item=z1_Detail_Directors visibility=[z1_Detail_Type=="movie"] icon="none"
+ Text item=z1_Detail_Genres visibility=[z1_Detail_Type=="movie", z1_Detail_Type=="album"] icon="none"
+ Text item=z1_Detail_RatingReason visibility=[z1_Detail_Type=="movie"] icon="none"
+ Text item=z1_Detail_Synopsis visibility=[z1_Detail_Type=="movie"] icon="none"
+ Text item=z1_Detail_Review visibility=[z1_Detail_Type=="album"] icon="none"
+ Text item=z1_Detail_ColorDescription visibility=[z1_Detail_Type=="movie"] icon="none"
+ Text item=z1_Detail_Country visibility=[z1_Detail_Type=="movie"] icon="none"
+ Text item=z1_Detail_AspectRatio visibility=[z1_Detail_Type=="movie"] icon="none"
+ Text item=z1_Detail_DiscLocation visibility=[z1_Detail_Type=="movie", z1_Detail_Type=="album"] icon="player"
+
+ Text label="Now Playing - Movie" icon="screen" {
+ Switch item=z1_Ui_Power
+ Slider item=z1_Ui_Volume
+ Switch item=z1_Ui_Mute
+ Default item=z1_Ui_Control
+ Switch item=z1_GoMovieCovers mappings=[ON="Movie Covers"]
+ Text item=z1_Ui_TitleName icon="video"
+ Text item=z1_Ui_PlayMode icon="player"
+ Text item=z1_Ui_PlaySpeed icon="player"
+ Text item=z1_Ui_TitleNum icon="video"
+ Text item=z1_Ui_TitleLength icon="time"
+ Text item=z1_Ui_TitleLoc icon="time"
+ Text item=z1_Ui_MovieMediaType icon="colorwheel"
+ Text item=z1_Ui_ChapterNum icon="video"
+ Text item=z1_Ui_ChapterLength icon="time"
+ Text item=z1_Ui_ChapterLoc icon="time"
+ Text item=z1_Ui_MovieLocation icon="video"
+ Text item=z1_Ui_AspectRatio icon="cinemascreen"
+ Text item=z1_Ui_VideoMode icon="screen"
+ Text item=z1_Ui_VideoModeComposite icon="screen"
+ Text item=z1_Ui_VideoModeComponent icon="screen"
+ Text item=z1_Ui_VideoModeHdmi icon="screen"
+ Text item=z1_Ui_VideoColor icon="screen"
+ Text item=z1_Ui_VideoColorEotf icon="screen"
+ Text item=z1_Ui_ContentColor icon="screen"
+ Text item=z1_Ui_ContentColorEotf icon="screen"
+ Text item=z1_Ui_ScaleMode icon="screen"
+ Text item=z1_Ui_ScreenMask icon="screen"
+ Text item=z1_Ui_ScreenMask2 icon="screen"
+ Text item=z1_Ui_CinemascapeMask icon="screen"
+ Text item=z1_Ui_CinemascapeMode icon="screen"
+ Text item=z1_Ui_UiState icon="player"
+ Text item=z1_Ui_ChildModeState icon="player"
+ Text item=z1_Ui_ReadinessState icon="switch"
+ Text item=z1_Ui_HighlightedSelection icon="zoom"
+ Text item=z1_Ui_UserDefinedEvent icon="zoom"
+ Text item=z1_Ui_UserInput icon="zoom"
+ Text item=z1_Ui_UserInputPrompt icon="zoom"
+ }
+
+ Text label="Now Playing - Music" icon="soundvolume-0" {
+ Switch item=z1_Ui_Power
+ Slider item=z1_Ui_Volume
+ Switch item=z1_Ui_Mute
+ Default item=z1_Music_Control
+ Switch item=z1_Music_Repeat
+ Switch item=z1_Music_Random
+ Text item=z1_Music_Track icon="soundvolume-0"
+ Text item=z1_Music_Artist icon="microphone"
+ Text item=z1_Music_Album icon="soundvolume-0"
+ Text item=z1_Music_PlayMode icon="player"
+ Text item=z1_Music_PlaySpeed icon="player"
+ Text item=z1_Music_TrackLength icon="time"
+ Text item=z1_Music_TrackPosition icon="time"
+ Text item=z1_Music_TrackProgress icon="time"
+ Text item=z1_Music_TrackHandle icon="zoom"
+ Text item=z1_Music_AlbumHandle icon="zoom"
+ Text item=z1_Music_NowplayHandle icon="zoom"
+ }
+ }
+}
+```
+
+kaleidescape.rules:
+
+```java
+var int lightPercent
+val kactions = getActions("kaleidescape","kaleidescape:player:myzone1")
+
+// send command to go to movie covers when button pressed
+rule "Go to Movie Covers"
+when
+ Item z1_GoMovieCovers received command
+then
+ if(null === kactions) {
+ logInfo("kactions", "Actions not found, check thing ID")
+ return
+ }
+ kactions.sendKCommand("GO_MOVIE_COVERS")
+end
+
+// send command to play a script
+rule "Play Script - Great Vistas"
+when
+ Item z1_PlayScript received command
+then
+ if(null === kactions) {
+ logInfo("kactions", "Actions not found, check thing ID")
+ return
+ }
+ kactions.sendKCommand("PLAY_SCRIPT:Great Vistas")
+end
+
+// handle a control system command sent from a script
+rule "Handle script commands"
+when
+ Item z1_Ui_UserDefinedEvent received update
+then
+ if (z1_Ui_UserDefinedEvent.state.toString == "DO_THE_NEEDFUL") {
+ logInfo("k rules", "handing the NEEDFUL script command...")
+ }
+end
+
+rule "Load selected item Metadata"
+when
+ Item z1_Ui_HighlightedSelection changed
+then
+ if(null === kactions) {
+ logInfo("kactions", "Actions not found, check thing ID")
+ return
+ }
+ kactions.sendKCommand("GET_CONTENT_DETAILS:" + z1_Ui_HighlightedSelection.state.toString + ":")
+end
+
+rule "Load Metadata for currently playing album"
+when
+ Item z1_Music_AlbumHandle changed
+then
+ if(null === kactions) {
+ logInfo("kactions", "Actions not found, check thing ID")
+ return
+ }
+ kactions.sendKCommand("GET_CONTENT_DETAILS:" + z1_Music_AlbumHandle.state.toString + ":")
+end
+
+rule "Bring up Lights when movie is over"
+when
+ Item z1_Ui_MovieLocation changed from "Main content" to "End Credits"
+then
+ // fade the lights up slowly while the credits are rolling
+ lightPercent = 0
+ while (lightPercent < 100) {
+ lightPercent = lightPercent + 5
+ logInfo("k rules", "lights at " + lightPercent.toString + " percent")
+ //myLightItem.sendCommand(lightPercent)
+ Thread::sleep(5000)
+ }
+end
+
+rule "Bring up Lights at 20 percent during intermission"
+when
+ Item z1_Ui_MovieLocation changed from "Main content" to "Intermission"
+then
+ //myLightItem.sendCommand(20)
+ logInfo("k rules", "intermission started")
+end
+
+rule "Turn lights back off when intermission over"
+when
+ Item z1_Ui_MovieLocation changed from "Intermission" to "Main content"
+then
+ //myLightItem.sendCommand(OFF)
+ logInfo("k rules", "intermission over")
+end
+```
diff --git a/bundles/org.openhab.binding.kaleidescape/doc/Kaleidescape_Logo.png b/bundles/org.openhab.binding.kaleidescape/doc/Kaleidescape_Logo.png
new file mode 100644
index 0000000000000..21143f2e7d60e
Binary files /dev/null and b/bundles/org.openhab.binding.kaleidescape/doc/Kaleidescape_Logo.png differ
diff --git a/bundles/org.openhab.binding.kaleidescape/pom.xml b/bundles/org.openhab.binding.kaleidescape/pom.xml
new file mode 100644
index 0000000000000..480593c7a99d8
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 2.5.9-SNAPSHOT
+
+
+ org.openhab.binding.kaleidescape
+
+ openHAB Add-ons :: Bundles :: Kaleidescape Binding
+
+
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/feature/feature.xml b/bundles/org.openhab.binding.kaleidescape/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..6066d53f018ea
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/feature/feature.xml
@@ -0,0 +1,10 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ openhab-transport-serial
+ mvn:org.openhab.addons.bundles/org.openhab.binding.kaleidescape/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/IKaleidescapeThingActions.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/IKaleidescapeThingActions.java
new file mode 100644
index 0000000000000..3643273d6682d
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/IKaleidescapeThingActions.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link IKaleidescapeThingActions} defines the interface for all thing actions supported by the binding.
+ * These methods, parameters, and return types are explained in {@link KaleidescapeThingActions}.
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public interface IKaleidescapeThingActions {
+
+ void sendKCommand(String kCommand);
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeBindingConstants.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeBindingConstants.java
new file mode 100644
index 0000000000000..8d6acc219b344
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeBindingConstants.java
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+
+/**
+ * The {@link KaleidescapeBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class KaleidescapeBindingConstants {
+ public static final String BINDING_ID = "kaleidescape";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_PLAYER = new ThingTypeUID(BINDING_ID, "player");
+ public static final ThingTypeUID THING_TYPE_CINEMA_ONE = new ThingTypeUID(BINDING_ID, "cinemaone");
+ public static final ThingTypeUID THING_TYPE_ALTO = new ThingTypeUID(BINDING_ID, "alto");
+ public static final ThingTypeUID THING_TYPE_STRATO = new ThingTypeUID(BINDING_ID, "strato");
+
+ public static final int DEFAULT_API_PORT = 10000;
+ public static final int DISCOVERY_THREAD_POOL_SIZE = 15;
+ public static final boolean DISCOVERY_DEFAULT_AUTO_DISCOVER = false;
+ public static final int DISCOVERY_DEFAULT_TIMEOUT_RATE_MS = 500;
+ public static final int DISCOVERY_DEFAULT_IP_TIMEOUT_RATE_MS = 750;
+
+ // List of all Channels
+ public static final String POWER = "ui#power";
+ public static final String VOLUME = "ui#volume";
+ public static final String MUTE = "ui#mute";
+ public static final String CONTROL = "ui#control";
+ public static final String TITLE_NAME = "ui#title_name";
+ public static final String PLAY_MODE = "ui#play_mode";
+ public static final String PLAY_SPEED = "ui#play_speed";
+ public static final String TITLE_NUM = "ui#title_num";
+ public static final String TITLE_LENGTH = "ui#title_length";
+ public static final String TITLE_LOC = "ui#title_loc";
+ public static final String CHAPTER_NUM = "ui#chapter_num";
+ public static final String CHAPTER_LENGTH = "ui#chapter_length";
+ public static final String CHAPTER_LOC = "ui#chapter_loc";
+ public static final String MOVIE_MEDIA_TYPE = "ui#movie_media_type";
+ public static final String MOVIE_LOCATION = "ui#movie_location";
+ public static final String ASPECT_RATIO = "ui#aspect_ratio";
+ public static final String VIDEO_MODE = "ui#video_mode";
+ public static final String VIDEO_MODE_COMPOSITE = "ui#video_mode_composite";
+ public static final String VIDEO_MODE_COMPONENT = "ui#video_mode_component";
+ public static final String VIDEO_MODE_HDMI = "ui#video_mode_hdmi";
+ public static final String VIDEO_COLOR = "ui#video_color";
+ public static final String VIDEO_COLOR_EOTF = "ui#video_color_eotf";
+ public static final String CONTENT_COLOR = "ui#content_color";
+ public static final String CONTENT_COLOR_EOTF = "ui#content_color_eotf";
+ public static final String SCALE_MODE = "ui#scale_mode";
+ public static final String SCREEN_MASK = "ui#screen_mask";
+ public static final String SCREEN_MASK2 = "ui#screen_mask2";
+ public static final String CINEMASCAPE_MASK = "ui#cinemascape_mask";
+ public static final String CINEMASCAPE_MODE = "ui#cinemascape_mode";
+ public static final String UI_STATE = "ui#ui_state";
+ public static final String CHILD_MODE_STATE = "ui#child_mode_state";
+ public static final String SYSTEM_READINESS_STATE = "ui#readiness_state";
+ public static final String HIGHLIGHTED_SELECTION = "ui#highlighted_selection";
+ public static final String USER_DEFINED_EVENT = "ui#user_defined_event";
+ public static final String USER_INPUT = "ui#user_input";
+ public static final String USER_INPUT_PROMPT = "ui#user_input_prompt";
+
+ public static final String MUSIC = "music#";
+ public static final String MUSIC_CONTROL = "music#control";
+ public static final String MUSIC_REPEAT = "music#repeat";
+ public static final String MUSIC_RANDOM = "music#random";
+ public static final String MUSIC_TRACK = "music#track";
+ public static final String MUSIC_ARTIST = "music#artist";
+ public static final String MUSIC_ALBUM = "music#album";
+ public static final String MUSIC_PLAY_MODE = "music#play_mode";
+ public static final String MUSIC_PLAY_SPEED = "music#play_speed";
+ public static final String MUSIC_TRACK_LENGTH = "music#track_length";
+ public static final String MUSIC_TRACK_POSITION = "music#track_position";
+ public static final String MUSIC_TRACK_PROGRESS = "music#track_progress";
+ public static final String MUSIC_TRACK_HANDLE = "music#track_handle";
+ public static final String MUSIC_ALBUM_HANDLE = "music#album_handle";
+ public static final String MUSIC_NOWPLAY_HANDLE = "music#nowplay_handle";
+
+ public static final String DETAIL = "detail#";
+
+ // metadata details - the values are keyed to what is sent by the component
+ // prefaced with 'detail_' when updating the channel
+ public static final String CONTENT_HANDLE = "content_handle";
+ public static final String ALBUM_CONTENT_HANDLE = "album_content_handle";
+ public static final String MOVIE = "movie";
+ public static final String ALBUM = "album";
+ public static final String DETAIL_TYPE = "type";
+ public static final String DETAIL_TITLE = "title"; // movie
+ public static final String DETAIL_ALBUM_TITLE = "album_title"; // album
+ public static final String DETAIL_COVER_ART = "cover_art"; // both
+ public static final String DETAIL_COVER_URL = "cover_url"; // both
+ public static final String DETAIL_HIRES_COVER_URL = "hires_cover_url"; // both
+ public static final String DETAIL_RATING = "rating"; // movie
+ public static final String DETAIL_YEAR = "year"; // both
+ public static final String DETAIL_RUNNING_TIME = "running_time"; // both
+ public static final String DETAIL_ACTORS = "actors"; // movie
+ public static final String DETAIL_ARTIST = "artist"; // album
+ public static final String DETAIL_DIRECTORS = "directors"; // movie
+ public static final String DETAIL_GENRES = "genres"; // both
+ public static final String DETAIL_RATING_REASON = "rating_reason"; // movie
+ public static final String DETAIL_SYNOPSIS = "synopsis"; // movie
+ public static final String DETAIL_REVIEW = "review"; // album
+ public static final String DETAIL_COLOR_DESCRIPTION = "color_description"; // movie
+ public static final String DETAIL_COUNTRY = "country"; // movie
+ public static final String DETAIL_ASPECT_RATIO = "aspect_ratio"; // movie
+ public static final String DETAIL_DISC_LOCATION = "disc_location"; // both
+
+ // make a list of all allowed metatdata channels,
+ // used to filter out what we don't want from the component
+ public static final Set METADATA_CHANNELS = new HashSet(
+ Arrays.asList(DETAIL_TITLE, DETAIL_ALBUM_TITLE, DETAIL_COVER_URL, DETAIL_HIRES_COVER_URL, DETAIL_RATING,
+ DETAIL_YEAR, DETAIL_RUNNING_TIME, DETAIL_ACTORS, DETAIL_ARTIST, DETAIL_DIRECTORS, DETAIL_GENRES,
+ DETAIL_RATING_REASON, DETAIL_SYNOPSIS, DETAIL_REVIEW, DETAIL_COLOR_DESCRIPTION, DETAIL_COUNTRY,
+ DETAIL_ASPECT_RATIO, DETAIL_DISC_LOCATION));
+
+ public static final String STANDBY_MSG = "Device is in standby";
+ public static final String PROPERTY_COMPONENT_TYPE = "Component Type";
+ public static final String PROPERTY_FRIENDLY_NAME = "Friendly Name";
+ public static final String PROPERTY_SERIAL_NUMBER = "Serial Number";
+ public static final String PROPERTY_CONTROL_PROTOCOL_ID = "Control Protocol ID";
+ public static final String PROPERTY_SYSTEM_VERSION = "System Version";
+ public static final String PROPERTY_PROTOCOL_VERSION = "Protocol Version";
+
+ public static final String GET_DEVICE_TYPE_NAME = "GET_DEVICE_TYPE_NAME";
+ public static final String GET_FRIENDLY_NAME = "GET_FRIENDLY_NAME";
+ public static final String GET_DEVICE_INFO = "GET_DEVICE_INFO";
+ public static final String GET_SYSTEM_VERSION = "GET_SYSTEM_VERSION";
+ public static final String GET_DEVICE_POWER_STATE = "GET_DEVICE_POWER_STATE";
+ public static final String GET_CINEMASCAPE_MASK = "GET_CINEMASCAPE_MASK";
+ public static final String GET_CINEMASCAPE_MODE = "GET_CINEMASCAPE_MODE";
+ public static final String GET_SCALE_MODE = "GET_SCALE_MODE";
+ public static final String GET_SCREEN_MASK = "GET_SCREEN_MASK";
+ public static final String GET_SCREEN_MASK2 = "GET_SCREEN_MASK2";
+ public static final String GET_VIDEO_MODE = "GET_VIDEO_MODE";
+ public static final String GET_UI_STATE = "GET_UI_STATE";
+ public static final String GET_HIGHLIGHTED_SELECTION = "GET_HIGHLIGHTED_SELECTION";
+ public static final String GET_CHILD_MODE_STATE = "GET_CHILD_MODE_STATE";
+ public static final String GET_MOVIE_LOCATION = "GET_MOVIE_LOCATION";
+ public static final String GET_MOVIE_MEDIA_TYPE = "GET_MOVIE_MEDIA_TYPE";
+ public static final String GET_PLAYING_TITLE_NAME = "GET_PLAYING_TITLE_NAME";
+ public static final String GET_PLAY_STATUS = "GET_PLAY_STATUS";
+ public static final String GET_MUSIC_NOW_PLAYING_STATUS = "GET_MUSIC_NOW_PLAYING_STATUS";
+ public static final String GET_MUSIC_PLAY_STATUS = "GET_MUSIC_PLAY_STATUS";
+ public static final String GET_MUSIC_TITLE = "GET_MUSIC_TITLE";
+ public static final String GET_SYSTEM_READINESS_STATE = "GET_SYSTEM_READINESS_STATE";
+ public static final String GET_VIDEO_COLOR = "GET_VIDEO_COLOR";
+ public static final String GET_CONTENT_COLOR = "GET_CONTENT_COLOR";
+ public static final String SET_STATUS_CUE_PERIOD_1 = "SET_STATUS_CUE_PERIOD:1";
+ public static final String GET_TIME = "GET_TIME";
+
+ public static final String LEAVE_STANDBY = "LEAVE_STANDBY";
+ public static final String ENTER_STANDBY = "ENTER_STANDBY";
+
+ public static final String PLAY = "PLAY";
+ public static final String PAUSE = "PAUSE";
+ public static final String NEXT = "NEXT";
+ public static final String PREVIOUS = "PREVIOUS";
+ public static final String SCAN_FORWARD = "SCAN_FORWARD";
+ public static final String SCAN_REVERSE = "SCAN_REVERSE";
+
+ public static final String MUSIC_REPEAT_ON = "MUSIC_REPEAT_ON";
+ public static final String MUSIC_REPEAT_OFF = "MUSIC_REPEAT_OFF";
+ public static final String MUSIC_RANDOM_ON = "MUSIC_RANDOM_ON";
+ public static final String MUSIC_RANDOM_OFF = "MUSIC_RANDOM_OFF";
+
+ public static final String SEND_EVENT_VOLUME_CAPABILITIES_15 = "SEND_EVENT:VOLUME_CAPABILITIES=15";
+ public static final String SEND_EVENT_VOLUME_LEVEL_EQ = "SEND_EVENT:VOLUME_LEVEL=";
+ public static final String SEND_EVENT_MUTE = "SEND_EVENT:MUTE_";
+ public static final String MUTE_ON = "ON_FB";
+ public static final String MUTE_OFF = "OFF_FB";
+
+ public static final String ONE = "1";
+ public static final String ZERO = "0";
+ public static final String EMPTY = "";
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeException.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeException.java
new file mode 100644
index 0000000000000..a52d67489537b
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeException.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link KaleidescapeException} class is used for any exception thrown by the binding
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class KaleidescapeException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public KaleidescapeException() {
+ }
+
+ public KaleidescapeException(String message, Throwable t) {
+ super(message, t);
+ }
+
+ public KaleidescapeException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeHandlerFactory.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeHandlerFactory.java
new file mode 100644
index 0000000000000..373f09a52d12d
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeHandlerFactory.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal;
+
+import static org.openhab.binding.kaleidescape.internal.KaleidescapeBindingConstants.*;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
+import org.eclipse.smarthome.io.net.http.HttpClientFactory;
+import org.eclipse.smarthome.io.transport.serial.SerialPortManager;
+import org.openhab.binding.kaleidescape.internal.handler.KaleidescapeHandler;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link KaleidescapeHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.kaleidescape", service = ThingHandlerFactory.class)
+public class KaleidescapeHandlerFactory extends BaseThingHandlerFactory {
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Collections
+ .unmodifiableSet(Stream.of(THING_TYPE_PLAYER, THING_TYPE_CINEMA_ONE, THING_TYPE_ALTO, THING_TYPE_STRATO)
+ .collect(Collectors.toSet()));
+
+ private final SerialPortManager serialPortManager;
+ private final HttpClient httpClient;
+
+ @Activate
+ public KaleidescapeHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
+ final @Reference SerialPortManager serialPortManager) {
+ this.httpClient = httpClientFactory.getCommonHttpClient();
+ this.serialPortManager = serialPortManager;
+ }
+
+ @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 (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
+ return new KaleidescapeHandler(thing, serialPortManager, httpClient);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeThingActions.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeThingActions.java
new file mode 100644
index 0000000000000..a159ea0116e8a
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/KaleidescapeThingActions.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.thing.binding.ThingActions;
+import org.eclipse.smarthome.core.thing.binding.ThingActionsScope;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.openhab.binding.kaleidescape.internal.handler.KaleidescapeHandler;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Some automation actions to be used with a {@link KaleidescapeThingActions}
+ *
+ * @author Michael Lobstein - initial contribution
+ *
+ */
+@ThingActionsScope(name = "kaleidescape")
+@NonNullByDefault
+public class KaleidescapeThingActions implements ThingActions, IKaleidescapeThingActions {
+ private final Logger logger = LoggerFactory.getLogger(KaleidescapeThingActions.class);
+
+ private @Nullable KaleidescapeHandler handler;
+
+ @RuleAction(label = "sendKCommand", description = "Action that sends raw command to the kaleidescape zone")
+ public void sendKCommand(@ActionInput(name = "sendKCommand") String kCommand) {
+ KaleidescapeHandler localHandler = handler;
+ if (localHandler != null) {
+ localHandler.handleRawCommand(kCommand);
+ logger.debug("sendKCommand called with command: {}", kCommand);
+ } else {
+ logger.warn("unable to send command, KaleidescapeHandler was null");
+ }
+ }
+
+ /** Static alias to support the old DSL rules engine and make the action available there. */
+ public static void sendKCommand(@Nullable ThingActions actions, String kCommand) throws IllegalArgumentException {
+ invokeMethodOf(actions).sendKCommand(kCommand);
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ this.handler = (KaleidescapeHandler) handler;
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return this.handler;
+ }
+
+ private static IKaleidescapeThingActions invokeMethodOf(@Nullable ThingActions actions) {
+ if (actions == null) {
+ throw new IllegalArgumentException("actions cannot be null");
+ }
+ if (actions.getClass().getName().equals(KaleidescapeThingActions.class.getName())) {
+ if (actions instanceof KaleidescapeThingActions) {
+ return (IKaleidescapeThingActions) actions;
+ } else {
+ return (IKaleidescapeThingActions) Proxy.newProxyInstance(
+ IKaleidescapeThingActions.class.getClassLoader(),
+ new Class[] { IKaleidescapeThingActions.class },
+ (Object proxy, Method method, Object[] args) -> {
+ Method m = actions.getClass().getDeclaredMethod(method.getName(),
+ method.getParameterTypes());
+ return m.invoke(actions, args);
+ });
+ }
+ }
+ throw new IllegalArgumentException("Actions is not an instance of KaleidescapeThingActions");
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeConnector.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeConnector.java
new file mode 100644
index 0000000000000..7f353afefa5dc
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeConnector.java
@@ -0,0 +1,270 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.communication;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.kaleidescape.internal.KaleidescapeBindingConstants;
+import org.openhab.binding.kaleidescape.internal.KaleidescapeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract class for communicating with the Kaleidescape component
+ *
+ * @author Laurent Garnier - Initial contribution
+ * @author Michael Lobstein - Adapted for the Kaleidescape binding
+ */
+@NonNullByDefault
+public abstract class KaleidescapeConnector {
+ private static final String SUCCESS_MSG = "01/1/000:/89";
+ private static final String BEGIN_CMD = "01/1/";
+ private static final String END_CMD = ":\r";
+
+ private final Pattern pattern = Pattern.compile("^(\\d{2})/./(\\d{3})\\:([^:^/]*)\\:(.*?)\\:/(\\d{2})$");
+
+ private final Logger logger = LoggerFactory.getLogger(KaleidescapeConnector.class);
+
+ /** The output stream */
+ protected @Nullable OutputStream dataOut;
+
+ /** The input stream */
+ protected @Nullable InputStream dataIn;
+
+ /** true if the connection is established, false if not */
+ private boolean connected;
+
+ private @Nullable Thread readerThread;
+
+ private final List listeners = new ArrayList<>();
+
+ /**
+ * Get whether the connection is established or not
+ *
+ * @return true if the connection is established
+ */
+ public boolean isConnected() {
+ return connected;
+ }
+
+ /**
+ * Set whether the connection is established or not
+ *
+ * @param connected true if the connection is established
+ */
+ protected void setConnected(boolean connected) {
+ this.connected = connected;
+ }
+
+ /**
+ * Set the thread that handles the feedback messages
+ *
+ * @param readerThread the thread
+ */
+ protected void setReaderThread(Thread readerThread) {
+ this.readerThread = readerThread;
+ }
+
+ /**
+ * Open the connection with the Kaleidescape component
+ *
+ * @throws KaleidescapeException - In case of any problem
+ */
+ public abstract void open() throws KaleidescapeException;
+
+ /**
+ * Close the connection with the Kaleidescape component
+ */
+ public abstract void close();
+
+ /**
+ * Stop the thread that handles the feedback messages and close the opened input and output streams
+ */
+ protected void cleanup() {
+ Thread readerThread = this.readerThread;
+ OutputStream dataOut = this.dataOut;
+ if (dataOut != null) {
+ try {
+ dataOut.close();
+ } catch (IOException e) {
+ logger.debug("Error closing dataOut: {}", e.getMessage());
+ }
+ this.dataOut = null;
+ }
+ InputStream dataIn = this.dataIn;
+ if (dataIn != null) {
+ try {
+ dataIn.close();
+ } catch (IOException e) {
+ logger.debug("Error closing dataIn: {}", e.getMessage());
+ }
+ this.dataIn = null;
+ }
+ if (readerThread != null) {
+ readerThread.interrupt();
+ this.readerThread = null;
+ try {
+ readerThread.join(3000);
+ } catch (InterruptedException e) {
+ logger.warn("Error joining readerThread: {}", e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
+ * actually read is returned as an integer.
+ *
+ * @param dataBuffer the buffer into which the data is read.
+ *
+ * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
+ * stream has been reached.
+ *
+ * @throws KaleidescapeException - If the input stream is null, if the first byte cannot be read for any reason
+ * other than the end of the file, if the input stream has been closed, or if some other I/O error
+ * occurs.
+ */
+ protected int readInput(byte[] dataBuffer) throws KaleidescapeException {
+ InputStream dataIn = this.dataIn;
+ if (dataIn == null) {
+ throw new KaleidescapeException("readInput failed: input stream is null");
+ }
+ try {
+ return dataIn.read(dataBuffer);
+ } catch (IOException e) {
+ throw new KaleidescapeException("readInput failed: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Ping the connection by requesting the time from the component
+ *
+ * @throws KaleidescapeException - In case of any problem
+ */
+ public void ping() throws KaleidescapeException {
+ sendCommand(KaleidescapeBindingConstants.GET_TIME);
+ }
+
+ /**
+ * Request the Kaleidescape component to execute a command
+ *
+ * @param cmd the command to execute
+ *
+ * @throws KaleidescapeException - In case of any problem
+ */
+ public void sendCommand(@Nullable String cmd) throws KaleidescapeException {
+ sendCommand(cmd, null);
+ }
+
+ /**
+ * Request the Kaleidescape component to execute a command
+ *
+ * @param cmd the command to execute
+ * @param cachedMessage an optional cached message that will immediately be sent as a KaleidescapeMessageEvent
+ *
+ * @throws KaleidescapeException - In case of any problem
+ */
+ public void sendCommand(@Nullable String cmd, @Nullable String cachedMessage) throws KaleidescapeException {
+ // if sent a cachedMessage, just send out an event with the data so KaleidescapeMessageHandler will process it
+ if (cachedMessage != null) {
+ logger.debug("Command: '{}' returning cached value: '{}'", cmd, cachedMessage);
+ // change GET_SOMETHING into SOMETHING and special case GET_PLAYING_TITLE_NAME into TITLE_NAME
+ dispatchKeyValue(cmd.replace("GET_", "").replace("PLAYING_TITLE_NAME", "TITLE_NAME"), cachedMessage, true);
+ return;
+ }
+
+ String messageStr = BEGIN_CMD + cmd + END_CMD;
+
+ logger.debug("Send command {}", messageStr);
+
+ OutputStream dataOut = this.dataOut;
+ if (dataOut == null) {
+ throw new KaleidescapeException("Send command \"" + messageStr + "\" failed: output stream is null");
+ }
+ try {
+ dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
+ dataOut.flush();
+ } catch (IOException e) {
+ throw new KaleidescapeException("Send command \"" + cmd + "\" failed: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Add a listener to the list of listeners to be notified with events
+ *
+ * @param listener the listener
+ */
+ public void addEventListener(KaleidescapeMessageEventListener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Remove a listener from the list of listeners to be notified with events
+ *
+ * @param listener the listener
+ */
+ public void removeEventListener(KaleidescapeMessageEventListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Analyze an incoming message and dispatch corresponding event (key, value) to the event listeners
+ *
+ * @param incomingMessage the received message
+ */
+ public void handleIncomingMessage(byte[] incomingMessage) {
+ String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
+
+ // ignore empty success messages
+ if (!SUCCESS_MSG.equals(message)) {
+ logger.debug("handleIncomingMessage: {}", message);
+
+ // Kaleidescape message ie: 01/!/000:TITLE_NAME:Office Space:/79
+ // or: 01/!/000:PLAY_STATUS:2:0:01:07124:00138:001:00311:00138:/27
+ // or: 01/1/000:TIME:2020:04:27:11:38:52:CDT:/84
+ // g1=zoneid, g2=sequence, g3=message name, g4=message, g5=checksum
+ // pattern : "^(\\d{2})/./(\\d{3})\\:([^:^/]*)\\:(.*?)\\:/(\\d{2})$");
+
+ Matcher matcher = pattern.matcher(message);
+ if (matcher.find()) {
+ dispatchKeyValue(matcher.group(3), matcher.group(4), false);
+ } else {
+ logger.debug("no match on message: {}", message);
+ if (message.contains(KaleidescapeBindingConstants.STANDBY_MSG)) {
+ dispatchKeyValue(KaleidescapeBindingConstants.STANDBY_MSG, "", false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Dispatch an event (key, value, isCached) to the event listeners
+ *
+ * @param key the key
+ * @param value the value
+ * @param isCached indicates if this event was generated from a cached value
+ */
+ private void dispatchKeyValue(String key, String value, boolean isCached) {
+ KaleidescapeMessageEvent event = new KaleidescapeMessageEvent(this, key, value, isCached);
+ listeners.forEach(l -> l.onNewMessageEvent(event));
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeDefaultConnector.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeDefaultConnector.java
new file mode 100644
index 0000000000000..f833ad9ebda3d
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeDefaultConnector.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.communication;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.kaleidescape.internal.KaleidescapeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class to create a default Kaleidescape before initialization is complete.
+ *
+ * @author Laurent Garnier - Initial contribution
+ * @author Michael Lobstein - Adapted for the Kaleidescape binding
+ */
+@NonNullByDefault
+public class KaleidescapeDefaultConnector extends KaleidescapeConnector {
+ private final Logger logger = LoggerFactory.getLogger(KaleidescapeDefaultConnector.class);
+
+ @Override
+ public void open() throws KaleidescapeException {
+ logger.warn("Kaleidescape binding incorrectly configured. Please configure for IP or serial connection");
+ setConnected(false);
+ }
+
+ @Override
+ public void close() {
+ setConnected(false);
+ }
+
+ @Override
+ public void sendCommand(@Nullable String value) {
+ logger.warn("Kaleidescape binding incorrectly configured. Please configure for IP or serial connection");
+ setConnected(false);
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeFormatter.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeFormatter.java
new file mode 100644
index 0000000000000..ebc4f987173fc
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeFormatter.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.communication;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link KaleidescapeFormatter} is a utility class with formatting methods for Kaleidescape strings
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class KaleidescapeFormatter {
+ public static String formatString(String input) {
+ if (!input.equals("")) {
+ // convert || back to :
+ input = input.replace("||", ":");
+
+ // if input does not have any escaped characters, bypass all the replace()'s
+ if (input.contains("\\")) {
+ // fix escaped :
+ input = input.replace("\\:", ":");
+
+ // fix escaped /
+ input = input.replace("\\/", "/");
+
+ // convert \r into comma space
+ input = input.replace("\\r", ", ");
+
+ // convert \d146 from review text into apostrophe
+ input = input.replace("\\d146", "'");
+ // convert \d147 & \d148 from review text into double quote
+ input = input.replace("\\d147", "\"");
+ input = input.replace("\\d148", "\"");
+
+ // fix the encoding for k mangled extended ascii characters (chars coming in as \dnnn)
+ // I.e. characters with accent, umlaut, etc., they need to be restored to the correct character
+ // example: Noel (with umlaut 'o') comes in as N\d246el
+ input = input.replaceAll("(?i)\\\\d([0-9]{3})", "\\$1;"); // first convert to html escaped codes
+ // then convert with unescapeHtml, not sure how to do this without the Apache libraries :(
+ return StringEscapeUtils.unescapeHtml(input);
+ }
+ }
+ return input;
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeIpConnector.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeIpConnector.java
new file mode 100644
index 0000000000000..e9dd33ca8a28f
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeIpConnector.java
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.communication;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.kaleidescape.internal.KaleidescapeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class for communicating with the Kaleidescape component through a IP connection or a serial over IP connection
+ *
+ * @author Laurent Garnier - Initial contribution
+ * @author Michael Lobstein - Adapted for the Kaleidescape binding
+ */
+@NonNullByDefault
+public class KaleidescapeIpConnector extends KaleidescapeConnector {
+ private final Logger logger = LoggerFactory.getLogger(KaleidescapeIpConnector.class);
+
+ private final @Nullable String address;
+ private final int port;
+ private final String uid;
+
+ private @Nullable Socket clientSocket;
+
+ /**
+ * Constructor
+ *
+ * @param address the IP address of the Kaleidescape component
+ * @param port the TCP port to be used
+ */
+ public KaleidescapeIpConnector(@Nullable String address, int port, String uid) {
+ this.address = address;
+ this.port = port;
+ this.uid = uid;
+ }
+
+ @Override
+ public synchronized void open() throws KaleidescapeException {
+ logger.debug("Opening IP connection on IP {} port {}", this.address, this.port);
+ try {
+ Socket clientSocket = new Socket(this.address, this.port);
+ clientSocket.setSoTimeout(100);
+
+ dataOut = new DataOutputStream(clientSocket.getOutputStream());
+ dataIn = new DataInputStream(clientSocket.getInputStream());
+
+ Thread thread = new KaleidescapeReaderThread(this, this.uid, this.address + "." + this.port);
+ setReaderThread(thread);
+ thread.start();
+
+ this.clientSocket = clientSocket;
+
+ setConnected(true);
+
+ logger.debug("IP connection opened");
+ } catch (IOException | SecurityException | IllegalArgumentException e) {
+ setConnected(false);
+ throw new KaleidescapeException("Opening IP connection failed: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ logger.debug("Closing IP connection");
+ super.cleanup();
+ Socket clientSocket = this.clientSocket;
+ if (clientSocket != null) {
+ try {
+ clientSocket.close();
+ } catch (IOException e) {
+ }
+ this.clientSocket = null;
+ }
+ setConnected(false);
+ logger.debug("IP connection closed");
+ }
+
+ /**
+ * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
+ * actually read is returned as an integer.
+ * In case of socket timeout, the returned value is 0.
+ *
+ * @param dataBuffer the buffer into which the data is read.
+ *
+ * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
+ * stream has been reached.
+ *
+ * @throws KaleidescapeException - If the input stream is null, if the first byte cannot be read for any reason
+ * other than the end of the file, if the input stream has been closed, or if some other I/O error
+ * occurs.
+ */
+ @Override
+ protected int readInput(byte[] dataBuffer) throws KaleidescapeException {
+ InputStream dataIn = this.dataIn;
+ if (dataIn == null) {
+ throw new KaleidescapeException("readInput failed: input stream is null");
+ }
+ try {
+ return dataIn.read(dataBuffer);
+ } catch (SocketTimeoutException e) {
+ return 0;
+ } catch (IOException e) {
+ throw new KaleidescapeException("readInput failed: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeMessageEvent.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeMessageEvent.java
new file mode 100644
index 0000000000000..b681bcae0fc93
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeMessageEvent.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.communication;
+
+import java.util.EventObject;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * KaleidescapeMessageEvent used to notify changes coming from messages received from the Kaleidescape component
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class KaleidescapeMessageEvent extends EventObject {
+ private static final long serialVersionUID = 1L;
+ private final String key;
+ private final String value;
+ private final boolean isCached;
+
+ public KaleidescapeMessageEvent(Object source, String key, String value, boolean isCached) {
+ super(source);
+ this.key = key;
+ this.value = value;
+ this.isCached = isCached;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public boolean isCached() {
+ return isCached;
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeMessageEventListener.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeMessageEventListener.java
new file mode 100644
index 0000000000000..7c832ffe2c0ae
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeMessageEventListener.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.communication;
+
+import java.util.EventListener;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Kaleidescape Event Listener interface. Handles incoming Kaleidescape message events
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public interface KaleidescapeMessageEventListener extends EventListener {
+ /**
+ * Event handler method for incoming Kaleidescape message events
+ *
+ * @param event the KaleidescapeMessageEvent object
+ */
+ void onNewMessageEvent(KaleidescapeMessageEvent event);
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeReaderThread.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeReaderThread.java
new file mode 100644
index 0000000000000..ea4f332297f9a
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeReaderThread.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.communication;
+
+import java.util.Arrays;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.kaleidescape.internal.KaleidescapeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A class that reads messages from the Kaleidescape component in a dedicated thread
+ *
+ * @author Laurent Garnier - Initial contribution
+ * @author Michael Lobstein - Adapted for the Kaleidescape binding
+ */
+@NonNullByDefault
+public class KaleidescapeReaderThread extends Thread {
+ private static final int READ_BUFFER_SIZE = 16;
+ private static final int SIZE = 512;
+ private static final char TERM_CHAR = '\r';
+
+ private final Logger logger = LoggerFactory.getLogger(KaleidescapeReaderThread.class);
+
+ private KaleidescapeConnector connector;
+
+ /**
+ * Constructor
+ *
+ * @param connector the object that should handle the received message
+ * @param uid the thing uid string
+ * @param connectionId a string that uniquely identifies the particular connection
+ */
+ public KaleidescapeReaderThread(KaleidescapeConnector connector, String uid, String connectionId) {
+ super("OH-binding-" + uid + "-" + connectionId);
+ this.connector = connector;
+ setDaemon(true);
+ }
+
+ @Override
+ public void run() {
+ logger.debug("Data listener started");
+
+ byte[] readDataBuffer = new byte[READ_BUFFER_SIZE];
+ byte[] dataBuffer = new byte[SIZE];
+ int index = 0;
+
+ try {
+ while (!Thread.interrupted()) {
+ int len = connector.readInput(readDataBuffer);
+ if (len > 0) {
+ for (int i = 0; i < len; i++) {
+
+ if (index < SIZE) {
+ dataBuffer[index++] = readDataBuffer[i];
+ }
+ if (readDataBuffer[i] == TERM_CHAR) {
+ if (index >= SIZE) {
+ dataBuffer[index - 1] = (byte) TERM_CHAR;
+ }
+ byte[] msg = Arrays.copyOf(dataBuffer, index);
+ connector.handleIncomingMessage(msg);
+ index = 0;
+ }
+
+ }
+ }
+ }
+ } catch (KaleidescapeException e) {
+ logger.debug("Reading failed: {}", e.getMessage(), e);
+ }
+
+ logger.debug("Data listener stopped");
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeSerialConnector.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeSerialConnector.java
new file mode 100644
index 0000000000000..a81bec39f618f
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeSerialConnector.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.communication;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.io.transport.serial.PortInUseException;
+import org.eclipse.smarthome.io.transport.serial.SerialPort;
+import org.eclipse.smarthome.io.transport.serial.SerialPortIdentifier;
+import org.eclipse.smarthome.io.transport.serial.SerialPortManager;
+import org.eclipse.smarthome.io.transport.serial.UnsupportedCommOperationException;
+import org.openhab.binding.kaleidescape.internal.KaleidescapeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class for communicating with the Kaleidescape component through a serial connection
+ *
+ * @author Laurent Garnier - Initial contribution
+ * @author Michael Lobstein - Adapted for the Kaleidescape binding
+ */
+@NonNullByDefault
+public class KaleidescapeSerialConnector extends KaleidescapeConnector {
+ private final Logger logger = LoggerFactory.getLogger(KaleidescapeSerialConnector.class);
+
+ private final String serialPortName;
+ private final SerialPortManager serialPortManager;
+ private final String uid;
+
+ private @Nullable SerialPort serialPort;
+
+ /**
+ * Constructor
+ *
+ * @param serialPortManager the serial port manager
+ * @param serialPortName the serial port name to be used
+ * @param uid the thing uid string
+ */
+ public KaleidescapeSerialConnector(SerialPortManager serialPortManager, String serialPortName, String uid) {
+ this.serialPortManager = serialPortManager;
+ this.serialPortName = serialPortName;
+ this.uid = uid;
+ }
+
+ @Override
+ public synchronized void open() throws KaleidescapeException {
+ logger.debug("Opening serial connection on port {}", serialPortName);
+ try {
+ SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
+ if (portIdentifier == null) {
+ setConnected(false);
+ throw new KaleidescapeException("Opening serial connection failed: No Such Port");
+ }
+
+ SerialPort commPort = portIdentifier.open(this.getClass().getName(), 2000);
+
+ commPort.setSerialPortParams(19200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
+ commPort.enableReceiveThreshold(1);
+ commPort.enableReceiveTimeout(100);
+ commPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
+
+ InputStream dataIn = commPort.getInputStream();
+ OutputStream dataOut = commPort.getOutputStream();
+
+ if (dataOut != null) {
+ dataOut.flush();
+ }
+ if (dataIn != null && dataIn.markSupported()) {
+ try {
+ dataIn.reset();
+ } catch (IOException e) {
+ logger.debug("Caught IOException at dataIn.reset(): {}", e.getMessage());
+ }
+ }
+
+ Thread thread = new KaleidescapeReaderThread(this, this.uid, this.serialPortName);
+ setReaderThread(thread);
+ thread.start();
+
+ this.serialPort = commPort;
+ this.dataIn = dataIn;
+ this.dataOut = dataOut;
+
+ setConnected(true);
+
+ logger.debug("Serial connection opened");
+ } catch (PortInUseException e) {
+ setConnected(false);
+ throw new KaleidescapeException("Opening serial connection failed: Port in Use Exception", e);
+ } catch (UnsupportedCommOperationException e) {
+ setConnected(false);
+ throw new KaleidescapeException("Opening serial connection failed: Unsupported Comm Operation Exception",
+ e);
+ } catch (UnsupportedEncodingException e) {
+ setConnected(false);
+ throw new KaleidescapeException("Opening serial connection failed: Unsupported Encoding Exception", e);
+ } catch (IOException e) {
+ setConnected(false);
+ throw new KaleidescapeException("Opening serial connection failed: IO Exception", e);
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ logger.debug("Closing serial connection");
+ SerialPort serialPort = this.serialPort;
+ if (serialPort != null) {
+ serialPort.removeEventListener();
+ }
+ super.cleanup();
+ if (serialPort != null) {
+ serialPort.close();
+ this.serialPort = null;
+ }
+ setConnected(false);
+ logger.debug("Serial connection closed");
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeStatusCodes.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeStatusCodes.java
new file mode 100644
index 0000000000000..9fce88d4b8b7d
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/communication/KaleidescapeStatusCodes.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.communication;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Provides mapping of various Kaleidescape status codes to plain language meanings
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class KaleidescapeStatusCodes {
+ private static final String UNUSED = "unused";
+ private static final String UNKNOWN = "unknown";
+ private static final String RESERVED = "reserved";
+
+ // map to lookup play mode
+ public static final Map PLAY_MODE = new HashMap<>();
+ static {
+ PLAY_MODE.put("0", "Nothing playing");
+ PLAY_MODE.put("1", "Paused");
+ PLAY_MODE.put("2", "Playing");
+ PLAY_MODE.put("3", UNUSED);
+ PLAY_MODE.put("4", "Forward scan");
+ PLAY_MODE.put("5", UNUSED);
+ PLAY_MODE.put("6", "Reverse scan");
+ }
+
+ // map to lookup media type
+ public static final Map MEDIA_TYPE = new HashMap<>();
+ static {
+ MEDIA_TYPE.put("00", "Nothing playing");
+ MEDIA_TYPE.put("01", "DVD");
+ MEDIA_TYPE.put("02", "Video stream");
+ MEDIA_TYPE.put("03", "Blu-ray Disc");
+ }
+
+ // map to lookup movie location
+ public static final Map MOVIE_LOCATION = new HashMap<>();
+ static {
+ MOVIE_LOCATION.put("00", UNKNOWN);
+ MOVIE_LOCATION.put("01", UNUSED);
+ MOVIE_LOCATION.put("02", UNUSED);
+ MOVIE_LOCATION.put("03", "Main content");
+ MOVIE_LOCATION.put("04", "Intermission");
+ MOVIE_LOCATION.put("05", "End Credits");
+ MOVIE_LOCATION.put("06", "DVD/Blu-ray Disc Menu");
+ }
+
+ // map to lookup aspect ratio
+ public static final Map ASPECT_RATIO = new HashMap<>();
+ static {
+ ASPECT_RATIO.put("00", UNKNOWN);
+ ASPECT_RATIO.put("01", "1.33");
+ ASPECT_RATIO.put("02", "1.66");
+ ASPECT_RATIO.put("03", "1.78");
+ ASPECT_RATIO.put("04", "1.85");
+ ASPECT_RATIO.put("05", "2.35");
+ }
+
+ public static final Map VIDEO_MODE = new HashMap<>();
+
+ static {
+ VIDEO_MODE.put("00", "No output");
+ VIDEO_MODE.put("01", "480i60 4:3");
+ VIDEO_MODE.put("02", "480i60 16:9");
+ VIDEO_MODE.put("03", "480p60 4:3");
+ VIDEO_MODE.put("04", "480p60 16:9");
+ VIDEO_MODE.put("05", "576i50 4:3");
+ VIDEO_MODE.put("06", "576i50 16:9");
+ VIDEO_MODE.put("07", "576p50 4:3");
+ VIDEO_MODE.put("08", "576p50 16:9");
+ VIDEO_MODE.put("09", "720p60 NTSC HD");
+ VIDEO_MODE.put("10", "720p50 PAL HD");
+ VIDEO_MODE.put("11", "1080i60 16:9");
+ VIDEO_MODE.put("12", "1080i50 16:9");
+ VIDEO_MODE.put("13", "1080p60 16:9");
+ VIDEO_MODE.put("14", "1080p50 16:9");
+ VIDEO_MODE.put("15", RESERVED);
+ VIDEO_MODE.put("16", RESERVED);
+ VIDEO_MODE.put("17", "1080p24 16:9");
+ VIDEO_MODE.put("18", RESERVED);
+ VIDEO_MODE.put("19", "480i60 64:27");
+ VIDEO_MODE.put("20", "576i50 64:27");
+ VIDEO_MODE.put("21", "1080i60 64:27");
+ VIDEO_MODE.put("22", "1080i50 64:27");
+ VIDEO_MODE.put("23", "1080p60 64:27");
+ VIDEO_MODE.put("24", "1080p50 64:27");
+ VIDEO_MODE.put("25", "1080p24 64:27");
+ VIDEO_MODE.put("26", "1080p24 64:27");
+ VIDEO_MODE.put("27", "3840x 2160p24 16:9");
+ VIDEO_MODE.put("28", "3840x 2160p24 64:27");
+ VIDEO_MODE.put("29", "3840x 2160p30 16:9");
+ VIDEO_MODE.put("30", "3840x 2160p30 64:27");
+ VIDEO_MODE.put("31", "3840x 2160p60 16:9");
+ VIDEO_MODE.put("32", "3840x 2160p60 64:27");
+ VIDEO_MODE.put("33", "3840x 2160p25 16:9");
+ VIDEO_MODE.put("34", "3840x 2160p25 64:27");
+ VIDEO_MODE.put("35", "3840x 2160p50 16:9");
+ VIDEO_MODE.put("36", "3840x 2160p50 64:27");
+ VIDEO_MODE.put("37", "3840x 2160p24 16:9");
+ VIDEO_MODE.put("38", "3840x 2160p24 64:27");
+ }
+
+ // map to lookup eotf
+ public static final Map EOTF = new HashMap<>();
+ static {
+ EOTF.put("00", UNKNOWN);
+ EOTF.put("01", "SDR");
+ EOTF.put("02", "HDR");
+ EOTF.put("03", "SMTPE ST 2048");
+ }
+
+ // map to lookup readiness state
+ public static final Map READINESS_STATE = new HashMap<>();
+ static {
+ READINESS_STATE.put("0", "system is ready");
+ READINESS_STATE.put("1", "system is becoming ready");
+ READINESS_STATE.put("2", "system is idle");
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/configuration/KaleidescapeThingConfiguration.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/configuration/KaleidescapeThingConfiguration.java
new file mode 100644
index 0000000000000..a3ccd015a07bf
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/configuration/KaleidescapeThingConfiguration.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.configuration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link KaleidescapeThingConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class KaleidescapeThingConfiguration {
+ public @Nullable String serialPort;
+ public @Nullable String host;
+ public @Nullable Integer port;
+ public @Nullable Integer updatePeriod;
+ public boolean volumeEnabled;
+ public Integer initialVolume = 0;
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryJob.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryJob.java
new file mode 100644
index 0000000000000..6bc321e5d7310
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryJob.java
@@ -0,0 +1,209 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.discovery;
+
+import static org.openhab.binding.kaleidescape.internal.KaleidescapeBindingConstants.*;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link KaleidescapeDiscoveryJob} class allow manual discovery of
+ * Kaleidescape components for a single IP address. This is used
+ * for threading to make discovery faster.
+ *
+ * @author Chris Graham - Initial contribution
+ * @author Michael Lobstein - Adapted for the Kaleidescape binding
+ *
+ */
+@NonNullByDefault
+public class KaleidescapeDiscoveryJob implements Runnable {
+ private final Logger logger = LoggerFactory.getLogger(KaleidescapeDiscoveryJob.class);
+
+ // Component Types
+ private static final String PLAYER = "Player";
+ private static final String CINEMA_ONE = "Cinema One";
+ private static final String ALTO = "Alto";
+ private static final String STRATO = "Strato";
+ private static final String STRATO_S = "Strato S";
+ private static final String DISC_VAULT = "Disc Vault";
+
+ private static final Set ALLOWED_DEVICES = new HashSet(
+ Arrays.asList(PLAYER, CINEMA_ONE, ALTO, STRATO, STRATO_S, DISC_VAULT));
+
+ private KaleidescapeDiscoveryService discoveryClass;
+
+ private ThingTypeUID thingTypeUid = THING_TYPE_PLAYER;
+ private String ipAddress = EMPTY;
+ private String friendlyName = EMPTY;
+ private String serialNumber = EMPTY;
+
+ public KaleidescapeDiscoveryJob(KaleidescapeDiscoveryService service, String ip) {
+ this.discoveryClass = service;
+ this.ipAddress = ip;
+ }
+
+ @Override
+ public void run() {
+ if (hasKaleidescapeDevice(this.ipAddress)) {
+ discoveryClass.submitDiscoveryResults(this.thingTypeUid, this.ipAddress, this.friendlyName,
+ this.serialNumber);
+ }
+ }
+
+ /**
+ * Determines if a Kaleidescape component with a movie player zone is available at a given IP address.
+ *
+ * @param ip IP address of the Kaleidescape component as a string.
+ * @return True if a component is found, false if not.
+ */
+ private boolean hasKaleidescapeDevice(String ip) {
+ try {
+ InetAddress address = InetAddress.getByName(ip);
+
+ if (isKaleidescapeDevice(address, DEFAULT_API_PORT)) {
+ return true;
+ } else {
+ logger.debug("No Kaleidescape component found at IP address ({})", ip);
+ return false;
+ }
+ } catch (UnknownHostException e) {
+ logger.debug("Unknown host: {} - {}", ip, e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Tries to establish a connection to a hostname and port and then interrogate the component
+ *
+ * @param host Hostname or IP address to connect to.
+ * @param port Port to attempt to connect to.
+ * @return True if the component found is one the binding supports
+ */
+ private boolean isKaleidescapeDevice(InetAddress host, int port) {
+ try (Socket socket = new Socket()) {
+ socket.connect(new InetSocketAddress(host, port), DISCOVERY_DEFAULT_IP_TIMEOUT_RATE_MS);
+
+ OutputStream output = socket.getOutputStream();
+ PrintWriter writer = new PrintWriter(output, true);
+
+ // query the component to see if it has video zones, the device type, friendly name, and serial number
+ writer.println("01/1/GET_NUM_ZONES:");
+ writer.println("01/1/GET_DEVICE_TYPE_NAME:");
+ writer.println("01/1/GET_FRIENDLY_NAME:");
+ writer.println("01/1/GET_DEVICE_INFO:");
+
+ InputStream input = socket.getInputStream();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input));
+
+ String componentType = EMPTY;
+ String line;
+ String videoZone = null;
+ String audioZone = null;
+ int lineCount = 0;
+
+ while ((line = reader.readLine()) != null) {
+ String[] strArr = line.split(":");
+
+ if (strArr.length >= 4) {
+ switch (strArr[1]) {
+ case "NUM_ZONES":
+ videoZone = strArr[2];
+ audioZone = strArr[3];
+ break;
+ case "DEVICE_TYPE_NAME":
+ componentType = strArr[2];
+ break;
+ case "FRIENDLY_NAME":
+ friendlyName = strArr[2];
+ break;
+ case "DEVICE_INFO":
+ serialNumber = strArr[3].trim(); // take off leading zeros
+ break;
+ }
+ } else {
+ logger.debug("isKaleidescapeDevice() - Unable to process line: {}", line);
+ }
+
+ lineCount++;
+
+ // stop after reading four lines
+ if (lineCount > 3) {
+ break;
+ }
+ }
+
+ // see if we have a video zone
+ if ("01".equals(videoZone)) {
+ // now check if we are one of the allowed types
+ if (ALLOWED_DEVICES.contains(componentType)) {
+ if (STRATO_S.equals(componentType) || STRATO.equals(componentType)) {
+ thingTypeUid = THING_TYPE_STRATO;
+ return true;
+ }
+
+ // A 'Player' without an audio zone is really a Strato C
+ // does not work yet, Strato C erroneously reports "01" for audio zones
+ // so we are unable to differentiate a Strato C from a Premiere player
+ if ("00".equals(audioZone) && PLAYER.equals(componentType)) {
+ thingTypeUid = THING_TYPE_STRATO;
+ return true;
+ }
+
+ // Alto
+ if (ALTO.equals(componentType)) {
+ thingTypeUid = THING_TYPE_ALTO;
+ return true;
+ }
+
+ // Cinema One
+ if (CINEMA_ONE.equals(componentType)) {
+ thingTypeUid = THING_TYPE_CINEMA_ONE;
+ return true;
+ }
+
+ // A Disc Vault with a video zone (the M700 vault), just call it a THING_TYPE_PLAYER
+ if (DISC_VAULT.equals(componentType)) {
+ thingTypeUid = THING_TYPE_PLAYER;
+ return true;
+ }
+
+ // default returns THING_TYPE_PLAYER
+ return true;
+ }
+ }
+ } catch (IOException e) {
+ logger.debug("isKaleidescapeDevice() IOException: {}", e.getMessage());
+ return false;
+ }
+
+ return false;
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryService.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryService.java
new file mode 100644
index 0000000000000..a7b5258c1ec4f
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryService.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.discovery;
+
+import static org.openhab.binding.kaleidescape.internal.KaleidescapeBindingConstants.*;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.commons.net.util.SubnetUtils;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService;
+import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
+import org.eclipse.smarthome.config.discovery.DiscoveryService;
+import org.eclipse.smarthome.core.common.NamedThreadFactory;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link KaleidescapeDiscoveryService} class allow manual discovery of Kaleidescape components.
+ *
+ * @author Chris Graham - Initial contribution
+ * @author Michael Lobstein - Adapted for the Kaleidescape binding
+ *
+ */
+@NonNullByDefault
+@Component(service = DiscoveryService.class, configurationPid = "discovery.kaleidescape")
+public class KaleidescapeDiscoveryService extends AbstractDiscoveryService {
+ private final Logger logger = LoggerFactory.getLogger(KaleidescapeDiscoveryService.class);
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Collections
+ .unmodifiableSet(Stream.of(THING_TYPE_PLAYER, THING_TYPE_CINEMA_ONE, THING_TYPE_ALTO, THING_TYPE_STRATO)
+ .collect(Collectors.toSet()));
+
+ @Activate
+ public KaleidescapeDiscoveryService() {
+ super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_DEFAULT_TIMEOUT_RATE_MS, DISCOVERY_DEFAULT_AUTO_DISCOVER);
+ }
+
+ @Override
+ public Set getSupportedThingTypes() {
+ return SUPPORTED_THING_TYPES_UIDS;
+ }
+
+ @Override
+ protected void startScan() {
+ logger.debug("Starting discovery of Kaleidescape components.");
+
+ try {
+ List ipList = getIpAddressScanList();
+
+ ExecutorService discoverySearchPool = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE,
+ new NamedThreadFactory("OH-binding-discovery.kaleidescape", true));
+
+ for (String ip : ipList) {
+ discoverySearchPool.execute(new KaleidescapeDiscoveryJob(this, ip));
+ }
+
+ discoverySearchPool.shutdown();
+ } catch (Exception exp) {
+ logger.debug("Kaleidescape discovery service encountered an error while scanning for components: {}",
+ exp.getMessage());
+ }
+
+ logger.debug("Completed discovery of Kaleidescape components.");
+ }
+
+ /**
+ * Create a new Thing with an IP address and Component type given. Uses default port.
+ *
+ * @param thingTypeUid ThingTypeUID of detected Kaleidescape component.
+ * @param ip IP address of the Kaleidescape component as a string.
+ * @param friendlyName Name of Kaleidescape component as a string.
+ * @param serialNumber Serial Number of Kaleidescape component as a string.
+ */
+ public void submitDiscoveryResults(ThingTypeUID thingTypeUid, String ip, String friendlyName, String serialNumber) {
+ ThingUID uid = new ThingUID(thingTypeUid, serialNumber);
+
+ HashMap properties = new HashMap<>();
+
+ properties.put("host", ip);
+ properties.put("port", DEFAULT_API_PORT);
+
+ thingDiscovered(DiscoveryResultBuilder.create(uid).withProperties(properties).withRepresentationProperty("host")
+ .withLabel(friendlyName).build());
+ }
+
+ /**
+ * Provide a string list of all the IP addresses associated with the network interfaces on
+ * this machine.
+ *
+ * @return String list of IP addresses.
+ * @throws UnknownHostException
+ * @throws SocketException
+ */
+ private List getIpAddressScanList() throws UnknownHostException, SocketException {
+ List results = new ArrayList<>();
+
+ InetAddress localHost = InetAddress.getLocalHost();
+ NetworkInterface networkInterface = NetworkInterface.getByInetAddress(localHost);
+
+ for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) {
+ InetAddress ipAddress = address.getAddress();
+
+ String cidrSubnet = ipAddress.getHostAddress() + "/" + address.getNetworkPrefixLength();
+
+ /* Apache Subnet Utils only supports IP v4 for creating string list of IP's */
+ if (ipAddress instanceof Inet4Address) {
+ logger.debug("Found interface IPv4 address to scan: {}", cidrSubnet);
+
+ SubnetUtils utils = new SubnetUtils(cidrSubnet);
+
+ results.addAll(Arrays.asList(utils.getInfo().getAllAddresses())); // not sure how to do this without the
+ // Apache libraries
+ } else if (ipAddress instanceof Inet6Address) {
+ logger.debug("Found interface IPv6 address to scan: {}, ignoring", cidrSubnet);
+ } else {
+ logger.debug("Found interface unknown IP type address to scan: {}", cidrSubnet);
+ }
+ }
+
+ return results;
+ }
+}
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/handler/KaleidescapeHandler.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/handler/KaleidescapeHandler.java
new file mode 100644
index 0000000000000..eaf3b2357e0dd
--- /dev/null
+++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/handler/KaleidescapeHandler.java
@@ -0,0 +1,585 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.kaleidescape.internal.handler;
+
+import static org.openhab.binding.kaleidescape.internal.KaleidescapeBindingConstants.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Time;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.smarthome.core.library.types.NextPreviousType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.eclipse.smarthome.core.library.types.PlayPauseType;
+import org.eclipse.smarthome.core.library.types.RewindFastforwardType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.library.unit.SmartHomeUnits;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingStatus;
+import org.eclipse.smarthome.core.thing.ThingStatusDetail;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerService;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.RefreshType;
+import org.eclipse.smarthome.core.types.State;
+import org.eclipse.smarthome.io.transport.serial.SerialPortManager;
+import org.openhab.binding.kaleidescape.internal.KaleidescapeException;
+import org.openhab.binding.kaleidescape.internal.KaleidescapeThingActions;
+import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeConnector;
+import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeDefaultConnector;
+import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeIpConnector;
+import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeMessageEvent;
+import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeMessageEventListener;
+import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeSerialConnector;
+import org.openhab.binding.kaleidescape.internal.configuration.KaleidescapeThingConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link KaleidescapeHandler} is responsible for handling commands, which are sent to one of the channels.
+ *
+ * Based on the Rotel binding by Laurent Garnier
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class KaleidescapeHandler extends BaseThingHandler implements KaleidescapeMessageEventListener {
+ private static final long RECON_POLLING_INTERVAL_S = 60;
+ private static final long POLLING_INTERVAL_S = 20;
+
+ private final Logger logger = LoggerFactory.getLogger(KaleidescapeHandler.class);
+ private final SerialPortManager serialPortManager;
+ private final Map cache = new HashMap();
+
+ protected final HttpClient httpClient;
+ protected final Unit