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.bundles org.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