diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index a546e9c9cbed4..1a42f08a9c504 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1801,6 +1801,11 @@ org.openhab.binding.sonos ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.sony + ${project.version} + org.openhab.addons.bundles org.openhab.binding.sonyaudio diff --git a/bundles/org.openhab.binding.sony/NOTICE b/bundles/org.openhab.binding.sony/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.sony/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.sony/README-ALL.md b/bundles/org.openhab.binding.sony/README-ALL.md new file mode 100644 index 0000000000000..7923366fa420f --- /dev/null +++ b/bundles/org.openhab.binding.sony/README-ALL.md @@ -0,0 +1,273 @@ +# Sony Binding + +This binding is for Sony IP based product line including TVs, BDVs, AVRs, Blurays, Soundbars and Wireless Speakers. + +## Supported Things + +The following are the services that are available from different Sony devices. +Please note they are not exclusive of each other (many services are offered on a single device and offer different capabilities). +Feel free to mix and match as you see fit. + +### Scalar (also known as the REST API) + +The Scalar API is Sony's next generation API for discovery and control of the device. +This service has been implemented in most of the Sony products and has the same (and more) capabilities of all the other services combined. +If your device supports a Scalar thing, you should probably use it versus any of the other services. +The only downside is that it's a bit 'heavier' (depending on the device - will likely issue more calls) and is a bit more complicated to use (many, many channels are produced). + +This service dynamically generates the channels based on the device. + +For specifics - see [Scalar Details](#scalar-details) section below + +### Simple IP + +The Simple IP protocol is a simplified version of IRCC and appears to be only supported on some models of Bravia TVs. +You must enable "Simple IP Control" on the devices (generally under `Settings->Network->Home Network->IP Control->Simple IP Control`) but once enabled - does not need any authentication. +The Simple IP control provides direct access to commonly used functions (channels, inputs, volume, etc) and provides full two-way communications (as things change on the device, openHAB will be notified immediately). + +For specifics - see [Simple IP](README-SimpleIp.md) + +### IRCC + +Many Sony products (TVs, AV systems, disc players) provided an IRCC service that provides minimal control to the device and some minimal feedback (via polling) depending on the version. +From my research, their appears to be 5 versions of this service: + +1. Not Specified - implemented on TVs and provides ONLY a command interface (i.e. sending of commands). +No feedback from the device is possible. +No status is available. +2. 1.0 - ??? +3. 1.1 - ??? +4. 1.2 - ??? +5. 1.3 - implemented on blurays. + +Provides a command interface, text field entry and status feedback (including disc information). +The status feedback is provided via polling of the device. + +Please note that the IRCC service is fully undocumented and much of the work that has gone into this service is based on observations. + +If you have a device that is reporting one of the "???" versions above, please post on the forum and I can give you directions on how we can document (and fix any issues) with those versions. + +Please note that Sony has begun transitioning many of their products over to the Scalar API and the latest firmware updates have begun to disable this service. + +For specifics - see [IRCC](README-IRCC.md) + +### DIAL + +The DIAL (DIscovery And Launch) allows you to discover the various applications available on the device and manage those applications (mainly to start or stop them). +This will apply to many of the smart tvs and bluray devices. +Generally you need to authenticate with IRCC before being able to use DIAL. +A channel will be created for each application (at startup only) and you can send ON to that channel to start the application and OFF to exit back to the main menu. + +For specifics - see [DIAL](README-README-DIAL.md.md) + +## Bluray Players + +Please note that somy Bluray players have only a limited, partial implementation of SCALAR. If you have a bluray player and scalar seems limited, you should try the DIAL/IRCC services as well. + +## Application status + +Sony has 'broken' the API that determines which application is currently running regardless if you use DIAL or Scalar services. +The API that determines whether an application is currently running ALWAYS returns 'stopped' (regardless if it's running or not). +Because of that - you cannnot rely on the application status and there is NO CURRENT WAY to determine if any application is running. + +Both DIAL/Scalar will continue to check the status in case Sony fixes this in some later date - but as of this writing - there is NO WAY to determine application status. + +## Device setup + +To enable automation of your device may require changes on the device. +This section mainly applies to TVs as the other devices generally are setup correctly. +Unfortunately the location of the settings generally depend on the device and the firmware that is installed. +I'll mention the most common area for each below but do remember that it may differ on your device. + +### Turn the device ON!!! + +When a sony device is off, there are a number of 'things' that get turned off as well. +You best action would be to turn the device ON when you are trying to set it up and bring it online for the first time. + +1. IRCC/Scalar on Blurays will not be auto discovered if the device is off (however DIAL will be discovered). +Both these services are turned off when the device is turned off. +2. Audio service on certain devices will either be turned off or limited in scope. + +If the audio service is off, you will either see no audio channels (volume, etc) or will be missing audio channels (like headphone volume for Bravias) + +### Wireless Interface + +If you are using the wireless interface on the device, you will *likely* lose the ability to power on the device with any of the services. +Most sony devices will power down the wireless port when turning off or going into standby - making communication to that device impossible (and thus trying to power on the device impossible). +As of the when this was written, there is no known way to change this behaviour through device options or setup. + + +### Wake on LAN + +To enable the device to wakeup based on network activity (WOL), go to `Settings->Network->Remote Start` and set to "ON". +This setting will cause the device to use more power (as it has to keep the ethernet port on always) but will generally allow you to turn on the device at any time. + +Note: this will **likely** not work if your device is connected wirelessly and generally only affects physical ethernet ports. + +### Enabling Remote Device Control + +To enable openHAB to control your device, you'll need to set the device to allow remote control. +Go to `Settings->Network->Home network setup->Renderer->Render Function` and set it to "Enabled". + +### Setting up the Authentication Mode + +There are three major ways to authenticate to a Sony Device: + +1. None - No authentication is needed (and openHAB should simply connect and work) +2. Normal - when openHAB registers with a device, a code is displayed on the device that needs to be entered into openHAB +3. Preshared - a predetermined key that is entered into openHAB + +You can select your authentication mode by going to `Settings->Network->Home network setup->IP Control->Authentication` and selecting your mode. +I highly recommend the use of "Normal" mode. + +Please note that their is a rare fourth option - some AVRs need to be put into a pairing mode prior to openHAB authentication. +This pairing mode acts similar to the "Normal" mode in that a code will be displayed on the AVR screen to be entered into openHAB. + +Also note that generally AVRs/SoundBars/Wireless speakers need no authentication at all and will automatically come online. + +See the authentication section below to understand how to use authenticate openHAB to the device. + +## Discovery + +This binding does attempt to discover Sony devices via UPNP. +Although this binding attempts to wake Sony devices (via wake on lan), some devices do not support WOL nor broadcast when they are turned off or sleeping. +If your devices does not automatically discovered, please turn the device on first and try again. +You may also need to turn on broadcasting via `Settings->Network->Remote Start` (on) - this setting has a side effect of turning on UPNP discovery. + +### Enabling/Disabling services + +By default, only the scalar service is enabled for discovery. +You can change the defaults by setting the following in the `conf/services/runtime.cfg` file: + +``` +discovery.sony-simpleip:background=false +discovery.sony-dial:background=false +discovery.sony-ircc:background=false +discovery.sony-scalar:background=true +``` + +## Authentication + +#### Normal Key + +A code request will request a code from the device and the device will respond by displaying new code on the screen. +Write this number down and then update the binding configuration with this code (FYI - you only have a limited time to do this - usually 30 or 60 seconds before that code expires). +Once you update the access code in the configuration, the binding will restart and a success message should appear on the device. + +Specifically you should: + +1. Update the "accessCode" configuration with the value "RQST". +The binding will then reload and send a request to the device. +2. The device will display a new code on the screen (and a countdown to expiration). +3. Update the "accessCode" configuration with the value shown on the screen. +The binding will then reload and ask the device to authorize with that code. +4. If successful, the device will show a success message and the binding should go online. +5. If unsuccessful, the code may have expired - start back at step 1. + +If the device was auto-discovered, the "RQST" will automatically be entered once you approve the device (then you have 30-60 seconds to enter the code displayed on the screen in the PaperUI `Configuration->Things->the device->configuration->Access Code`). +If the code expired before you had a chance, simply double click on the "RQST" and press the OK button - that will force the binding to request a new code (and alternative if that doesn't work is to switch it to "1111" and then back to "RQST"). + +If you are manually setting up the configuration, saving the file will trigger the above process. + +#### Pre Shared Key + +A pre-shared key is a key that you have set on the device prior to discovery (generally `Settings->Network Control->IP Control->Pre Shared Key`). +If you have set this on the device and then set the appropriate accessCode in the configuration, no additional authentication is required and the binding should be able to connect to the device. + +## Deactivation + +If you have used the Normal Key authentication and wish to deactivate the addon from the TV (to either cleanup when uninstalling or to simply restart an new authentication process): + +1. Go to `Settings->Network->Remote device settings->Deregister remote device` +2. Find and highlight the `openHAB (MediaRemote:00-11-22-33-44-55)` entry. +3. Select the `Deregister` button + +If you have used a preshared key - simply choose a new key (this may affect other devices however since a preshared key is global). + +## Thing Configuration + +### IP Address Configuration + +Any service can be setup by using just an IP address (or host name) - example: `192.168.1.104` in the deviceAddress field in configuration. +However, doing this will make certain assumptions about the device (ie path to services, port numbers, etc) that may not be correct for your device (should work for about 95% of the devices however). + +If you plan on setting your device up in a .things file, I recommend autodiscovering it first and copy the URL to your things file. + +There is one situation where you MUST use an IP address - if your device switches discovery ports commonly - then you must use a static IP address/host name. + +### Common Configuration Options + +The following is a list of common configuration options for all services + +| Name | Required | Default | Description | +| ------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------- | +| deviceAddress | Yes (1) | None | The path to the descriptor file or the IP address/host name of the device | +| deviceMacAddress | No (2) | eth0 | The device MAC address to use for wake on lan (WOL). | +| refresh | No (3) | 30 | The time, in seconds, to refresh some state from the device (only if the device supports retrieval of status) | +| checkStatusPolling | No | 30 | The time, in seconds, to check the device status device | +| retryPolling | No | 10 | The time, in seconds, to retry connecting to the device | + +1. See IP Address Configuration above +2. Only specify if the device support wake on lan (WOL) +3. Only specify if the device provides status information. + +Set to negative to disable (-1). + +```refresh``` is the time between checking the state of the device. +This will query the device for it's current state (example: volume level, current input, etc) and update all associated channels. +This is necessary if there are changes made by the device itself or if something else affects the device state outside of openHAB (such as using a remote). + +```checkStatusPolling``` is the time between checking if we still have a valid connection to the device. +If a connection attempt cannot be made, the thing will be updated to OFFLINE and will start a reconnection attempt (see ```retryPolling```). + +```retryPolling``` is the time between re-connection attempts. +If the thing goes OFFLINE (for any non-configuration error), reconnection attempts will be made. +Once the connection is successful, the thing will go ONLINE. + +### Ignore these configuration options + +The following 'configuration' options (as specified in the config XMLs) should **NEVER** be set as they are only set by the discovery process. + +| Name | Description | +| ------------------------- | --------------------------------------------- | +| discoveredMacAddress | Don't set this - set deviceMacAddress instead | +| discoveredCommandsMapFile | Don't set this - set commandMapFile instead | +| discoveredModelName | Don't set this - set modelName instead | + +## Advanced Users Only + +The following information is for more advanced users... + +### Low power devices (PIs, etc) + +This addon will try to only query information for the device to fulfill the information for channels you have linked. +However, if you've linked a great deal of channels (causing alot of requests to the device) and are running openHAB on a low power device - the polling time should be adjusted upwards to reduce the load on the PI. + +### Separating the sony logging into its own file + +To separate all the sony logging information into a separate file, please edit the file `userdata/etc/log4j2.xml` as follows: + +1. Add an logger appender definition (including the log file name) +2. Add a logger definition referencing the appender defined in step 1 + +Example for logging all `INFO` logs into a separate file `sony.log` under the standard log folder: + +``` + +... + + + + + + + + + + +... + + + + +``` diff --git a/bundles/org.openhab.binding.sony/README-DIAL.md b/bundles/org.openhab.binding.sony/README-DIAL.md new file mode 100644 index 0000000000000..f28f88badef53 --- /dev/null +++ b/bundles/org.openhab.binding.sony/README-DIAL.md @@ -0,0 +1,88 @@ +# DIAL + +The DIAL (DIscovery And Launch) allows you to discover the various applications available on the device and manage those applications (mainly to start or stop them). +This will apply to many of the smart tvs and bluray devices. +Generally you need to authenticate with IRCC before being able to use DIAL. +A channel will be created for each application (at startup only) and you can send ON to that channel to start the application and OFF to exit back to the main menu. + +## Application status + +Sony has 'broken' the API that determines which application is currently running regardless if you use DIAL or Scalar services. +The API that determines whether an application is currently running ALWAYS returns 'stopped' (regardless if it's running or not). +Because of that - you cannnot rely on the application status and there is NO CURRENT WAY to determine if any application is running. + +Both DIAL/Scalar will continue to check the status in case Sony fixes this in some later date - but as of this writing - there is NO WAY to determine application status. + + +## Authentication + +The DIAL service itself, generally, doesn't support authentication and relies on the authentication from IRCC. + +Feel free to try to authentication as outlined in the main [README](README.md). + +However, if that doesn't work, authenticate via the IRCC service. +Once the IRCC thing is online, the DIAL thing should come online as well and you may delete the IRCC thing (if you are not using it) + +## Thing Configuration + +The configuration for the DIAL Service Thing: + +| Name | Required | Default | Description | +| ---------- | -------- | ------- | ------------------------------ | +| accessCode | No | RQST | The access code for the device | + +## Channels + +The DIAL service will interactively create channels (based on what applications are installed on the device). +The channels will be: + +| Channel Type ID | Read/Write | Item Type | Description | +| --------------- | ---------- | --------- | ----------------------------------- | +| state-{id} | R (1) | Switch | Whether the app is running or not | +| icon-{id} | R | Image | The icon related to the application | + +1. Please note that at the time of this writing, Sony broke the application status and this channel will not correctly reflect what is running + +The {id} is the unique identifier that the device has assigned to the application. + +Example: On my bluray device, "Netflix" is identified as "com.sony.iptv.type.NRDP". +The channels would then be: + +1. "state-com.sony.iptv.type.NRDP" with a label of "Netflix" +2. "icon-com.sony.iptv.type.NRDP" with a label of "Netflix Icon" + +To identify all of the channel ids - look into the log file. +This service (on startup) will provide a logging message like: +```Creating channel 'Netflix' with an id of 'com.sony.iptv.type.NRDP' `` + +Note: if you install a new application on the device, this binding will need to be restarted to pickup on and create a channel for the new application. + +## Full Example + +*Really recommended to autodiscover rather than manually setup thing file* + +dial.Things: + +``` +Thing sony:dial:home [ deviceAddress="http://192.168.1.71:50201/dial.xml", deviceMacAddress="aa:bb:cc:dd:ee:ff", refresh=-1 ] +``` + +dial.items: + +``` +Switch DIAL_Netflix "Netflix [%s]" { channel="sony:dial:home:state-com.sony.iptv.type.NRDP" } +Image DIAL_NetflixIcon "Icon" { channel="sony:dial:home:icon-com.sony.iptv.type.NRDP" } +``` + + +dial.sitemap + +``` +sitemap demo label="Main Menu" +{ + Frame label="DIAL" { + Switch item=DIAL_Netflix + ImageItem item=DIAL_NetflixIcon + } +} +``` diff --git a/bundles/org.openhab.binding.sony/README-IRCC.md b/bundles/org.openhab.binding.sony/README-IRCC.md new file mode 100644 index 0000000000000..5acc2d83a3043 --- /dev/null +++ b/bundles/org.openhab.binding.sony/README-IRCC.md @@ -0,0 +1,231 @@ +# IRCC + +IRCC (otherwise know as IRCC-IP - InfraRed Compatible Control over Internet Protocol) will allow you to send IR commands to the device over IP. + +Many Sony products (TVs, AV systems, disc players) provided an IRCC service that provides minimal control to the device and some minimal feedback (via polling) depending on the version. +From my research, their appears to be 5 versions of this service: + +1) Not Specified - implemented on TVs and provides ONLY a command interface (i.e. sending of commands). +No feedback from the device is possible. +No status is available. +2) 1.0 - ??? +3) 1.1 - ??? +4) 1.2 - ??? +5) 1.3 - implemented on blurays. +Provides a command interface, text field entry and status feedback (including disc information). +The status feedback is provided via polling of the device. + +Please note that the IRCC service is fully undocumented and much of the work that has gone into this service is based on observations. + +If you have a device that is reporting one of the "???" versions above, please post on the forum and I can give you directions on how we can document (and fix any issues) with those versions. + +Please note that Sony has begun transitioning many of it's products over the the Scalar API and the latest firmware updates have begun to disable this service. + +### Power ON notes + +The biggest issue with IRCC devices is powering on. +Some devices support wake on lan (WOL) and this binding will attempt to wake the device. +Some devices support an IRCC power on command (rare) and this binding will attempt to use it. +However, some devices won't even send out a discovery packet nor respond to IRCC descriptor requests until the device is on. +If your device falls into that category, the device must be ON to auto-discover it and it must be ON when the binding starts up. +If it's not ON during discovery, the device simply won't be found. +If it's not ON during startup, the binding will remain OFFLINE and attempt to reconnect every few seconds (configuration option). +Likewise if it's turned OFF, the binding will go OFFLINE and will attempt retries until it's turned back on. + +Please note that if you device supports WOL, the device will be woken when the openHAB comes online since the binding will attempt to read the IRCC descriptor from it. + +The "power" channel will: + +1) ON - attempt to wake the device via WOL (if supported) or attempt to send the power ON IRCC command (if supported). +If the device supports neither, then the "ON" side of the channel will not work and you'll need to rely on some other means to turn the device on +2) OFF - will turn the device off via the power IRCC command. +The binding will attempt to use the discrete POWER OFF command first and if not supported, the POWER toggle command will be used instead (assuming the device is on). + +Please note that the initial/current status of the "power" channel is dependent on the IRCC version. +Version 1.3 will detect and generally be correct. +Version 1.0-1.2 is unknown. +Version "Not Specified" cannot determine the status. + +### Power OFF notes + +Powering OFF is generally supported on all IRCC devices either through a discrete "Power Off" IRCC command or via the "Power" toggle IRCC command. + +## Authentication + +IRCC can be authenticated via normal keys or preshared keys as documented in the main [README](README.md). + +## Thing Configuration + +The configuration for the IRCC thing (in addition to the common parameters) + +| Name | Required | Default | Description | +| --------------- | -------- | ------- | ----------------------------------------------------------------------------- | +| accessCode | No | RQST | The access code for the device | +| commandsMapFile | No (1) | None | The commands map file that translates words to the underlying protocol string | + +1. See transformations below + + +## Transformations + +These services use a commands map file that will convert a word (specified in the command channel) to the underlying command to send to the device. +This file will appear in your openHAB ```conf/transformation``` directory. + +When the device is ONLINE, the commandsMapFile configuration property has been set and the resulting file doesn't exist, the binding will write out the commands supported by the device to that file. +If discovery of the commands is not possible, a default set of commands will be written out which may or may not be correct for the device. +I highly recommend having the binding do this rather than creating the file from scratch. + +When the device is auto discovered, the commandsMapFile will be set to "ircc-{thingid}.map" (example: "ircc-ace2a0229f7a.map"). +You may want to change that in the things configuration, post-discovery, to something more reasonable. + +The format of the file will be: ```{word}={protocol}:{cmd}``` + +1. The word can be anything (in any language) and is the value send to the command channel. +2. The protocol can either be "ircc" for an IRCC command or "url" for a web request command. +3. The cmd is a URL Encoded value that will be sent to the device (or used as an HTTP GET if the "url" protocol). + +An example from a Sony BluRay player (that was discovered by the binding): + +``` +... +Stop=ircc:AAAAAwAAHFoAAAAYAw%3D%3D +SubTitle=ircc:AAAAAwAAHFoAAABjAw%3D%3D +TopMenu=ircc:AAAAAwAAHFoAAAAsAw%3D%3D +Up=ircc:AAAAAwAAHFoAAAA5Aw%3D%3D +Yellow=ircc:AAAAAwAAHFoAAABpAw%3D%3D +ZoomIn=url:http%3A%2F%2F192.168.1.2%3A50002%2FsetBrowse%3Faction%3DzoomIn +ZoomOut=url:http%3A%2F%2F192.168.1.2%3A50002%2FsetBrowse%3Faction%3DzoomOut +... +``` + +Please note that you can recreate the .map file by simply deleting it from ```conf/transformation``` and restarting openHAB. + +## Channels + +The channels supported depend on the version of the IRCC service. + +| Channel Group ID | Channel Type ID | Read/Write | Version | Item Type | Description | +| ---------------- | --------------- | ---------- | ------- | --------- | ------------------------------------------------------- | +| primary | power | R | any | Switch | Whether the device is powered on or not | +| primary | command | W | any | String | The IRCC command to execute (see transformations above) | +| primary | contenturl | R | 1.3 | String | The URL displayed in the device's browser | +| primary | textfield | R | 1.3 | String | The contents of the text field | +| primary | intext | R | 1.3 | Switch | Whether a text field has focus | +| primary | inbrowser | R | 1.3 | Switch | Whether viewing the device's browser or not | +| primary | isviewing | R | 1.3 | Switch | Whether viewing content or not | +| viewing | id | R | 1.3 | String | The identifier of what is being viewed | +| viewing | source | R | 1.3 | String | The source being viewed | +| viewing | zone2source | R | 1.3 | String | The source being viewed in zone 2 | +| viewing | title | R | 1.3 | String | The title of the source being viewed | +| viewing | duration | R | 1.3 | Number | The duration (in seconds) of what is being viewed | +| content | id | R | 1.3 | String | The identifier of the content | +| content | title | R | 1.3 | String | The title of the content | +| content | class | R | 1.3 | String | The class of the content (video, etc) | +| content | source | R | 1.3 | String | The source of the content (DVD, etc) | +| content | mediatype | R | 1.3 | String | The media type of the content (DVD, USB, etc) | +| content | mediasource | R | 1.3 | String | The media format of the content (VIDEO, etc) | +| content | edition | R | 1.3 | String | The edition of the content | +| content | description | R | 1.3 | String | The description of the content | +| content | genre | R | 1.3 | String | The genre of the content | +| content | duration | R | 1.3 | Number | The duration (in seconds) of the content | +| content | rating | R | 1.3 | String | The rating of the content (R, PG, etc) | +| content | daterelease | R | 1.3 | DateTime | The release date of the content | +| content | director | R | 1.3 | String | The director(s) of the content | +| content | producer | R | 1.3 | String | The producer(s) of the content | +| content | screenwriter | R | 1.3 | String | The screen writer(s) of the content | +| content | image | R | 1.3 | Image | The content image | + +Notes: + +1. *The power switch is simply a toggle to issue power on/off IRCC commands and will NOT reflect the current power state of the device* +2. Version 1.3 is what I had to test with - channels may work with lower versions but I can't confirm until someone has a device with those versions. +3. "inbrowser" will become true for certain apps as well (not sure why unless the use a browser under the scenes?) +4. "viewingXXX" will only be populated (with the exception of "viewingtitle") when "isviewing" is true and is only set when actually viewing a disc (not an app or browser) +5. "contenttitle" will also represent the browser title when viewing a webpage in the browser +6. "contentXXX" will be available if a disc is inserted (whether you are viewing it or not). +7. Setting the "contenturl" will start the browser on the device and set the url to the content. +Please note that the url MUST begin with "http://" or "https://" for this to work. + +## Full Examples + +*Really recommended to autodiscover rather than manually setup thing file* + +ircc.Things: + +``` +Thing sony:ircc:home [ deviceAddress="http://192.168.1.72:20970/sony/webapi/ssdp/dd.xml", deviceAddressAddress="aa:bb:cc:dd:ee:ff", commandsMapFile="ircccodes.map", accessCode="1111", refresh=-1 ] +``` + +ircc.items: + +``` +String IRCC_Version "Version [%s]" { channel="sony:ircc:home:primary#version" } +Switch IRCC_Power "Power [%s]" { channel="sony:ircc:home:primary#power" } +String IRCC_Command "Command [%s]" { channel="sony:ircc:home:primary#command" } +Switch IRCC_InBrowser "In browser [%s]" { channel="sony:ircc:home:primary#inbrowser" } +String IRCC_ContentURL "URL [%s]" { channel="sony:ircc:home:primary#contenturl" } +Switch IRCC_InText "In text [%s]" { channel="sony:ircc:home:primary#intext" } +String IRCC_Text "Text [%s]" { channel="sony:ircc:home:primary#textfield" } + +Switch IRCC_IsViewing "Is Viewing [%s]" { channel="sony:ircc:home:primary#isviewing" } +String IRCC_ViewingId "ViewingID [%s]" { channel="sony:ircc:home:viewing#id" } +String IRCC_ViewingSource "Viewing Source [%s]" { channel="sony:ircc:home:viewing#source" } +String IRCC_ViewingClass "Viewing Class [%s]" { channel="sony:ircc:home:viewing#class" } +String IRCC_ViewingTitle "Viewing Title [%s]" { channel="sony:ircc:home:viewing#title" } +Number IRCC_ViewingDuration "Viewing Duration [%s] seconds" { channel="sony:ircc:home:viewing#duration" } + +String IRCC_ContentId "ContentID [%s]" { channel="sony:ircc:home:content#id" } +String IRCC_ContentTitle "Content Title [%s]" { channel="sony:ircc:home:content#title" } +String IRCC_ContentClass "Content Class [%s]" { channel="sony:ircc:home:content#class" } +String IRCC_ContentSource "Content Source [%s]" { channel="sony:ircc:home:content#source" } +String IRCC_ContentMediaType "Content Media Type [%s]" { channel="sony:ircc:home:content#mediatype" } +String IRCC_ContentMediaFormat "Content Media Format [%s]" { channel="sony:ircc:home:content#mediaformat" } +String IRCC_ContentEdition "Content Edition [%s]" { channel="sony:ircc:home:content#edition" } +String IRCC_ContentDescription "Content Description [%s]" { channel="sony:ircc:home:content#description" } +String IRCC_ContentGenre "Content Genre [%s]" { channel="sony:ircc:home:content#genre" } +Number IRCC_ContentDuration "Content Duration [%s] seconds" { channel="sony:ircc:home:content#duration" } +String IRCC_ContentRating "Content Rating [%s]" { channel="sony:ircc:home:content#rating" } +DateTime IRCC_ContentDateRelease "Content Date Released [%F]" { channel="sony:ircc:home:content#daterelease" } +String IRCC_ContentDirector "Content Director(s) [%s]" { channel="sony:ircc:home:content#director" } +String IRCC_ContentProducer "Content Producer(s) [%s]" { channel="sony:ircc:home:content#producer" } +String IRCC_ContentScreenWriter "Content Screen Writer(s) [%s]" { channel="sony:ircc:home:content#screenwriter" } +Image IRCC_ContentImage "Content Image" { channel="sony:ircc:home:content#image" } +``` + +ircc.sitemap + +``` +sitemap demo label="Main Menu" +{ + Frame label="IRCC" { + Text item=IRCC_Version + Switch item=IRCC_Power + Text item=IRCC_InBrowser + Text item=IRCC_ContentURL + Text item=IRCC_InText + Text item=IRCC_Text + Text item=IRCC_IsViewing + Text item=IRCC_ViewingId + Text item=IRCC_ViewingSource + Text item=IRCC_ViewingClass + Text item=IRCC_ViewingTitle + Text item=IRCC_ViewingDuration + Text item=IRCC_ContentId + Text item=IRCC_ContentTitle + Text item=IRCC_ContentClass + Text item=IRCC_ContentSource + Text item=IRCC_ContentMediaType + Text item=IRCC_ContentMediaFormat + Text item=IRCC_ContentEdition + Text item=IRCC_ContentDescription + Text item=IRCC_ContentGenre + Text item=IRCC_ContentDuration + Text item=IRCC_ContentRating + Text item=IRCC_ContentDateRelease + Text item=IRCC_ContentDirector + Text item=IRCC_ContentProducer + Text item=IRCC_ContentScreenWriter + ImageItem item=IRCC_ContentImage + } +} +``` diff --git a/bundles/org.openhab.binding.sony/README-SCALAR.md b/bundles/org.openhab.binding.sony/README-SCALAR.md new file mode 100644 index 0000000000000..9ef05c0f51b77 --- /dev/null +++ b/bundles/org.openhab.binding.sony/README-SCALAR.md @@ -0,0 +1,841 @@ +# SCALAR + +SCALAR (also known as REST-API) is Sony's next generation API for discovery and control of the device. +This service has been implemented in most of the Sony products and has the same (and more) capabilities of all the other services combined. +If your device supports a Scalar thing, you should probably use it versus any of the other services. +The only downside is that it's a bit 'heavier' (depending on the device - will likely issue more calls) and is a bit more complicated to use (many, many channels are produced). + +This service dynamically generates the channels based on the device. + +## Application status + +Sony has 'broken' the API that determines which application is currently running regardless if you use DIAL or Scalar services. +The API that determines whether an application is currently running ALWAYS returns 'stopped' (regardless if it's running or not). +Because of that - you cannnot rely on the application status and there is NO CURRENT WAY to determine if any application is running. + +Both DIAL/Scalar will continue to check the status in case Sony fixes this in some later date - but as of this writing - there is NO WAY to determine application status. + +## Authentication + +Scalar can be authenticated via normal keys or preshared keys as documented in the main [README](README.md). + +## Thing Configuration + +The configuration for the Scalar thing (in addition to the common parameters) + +| Name | Required | Default | Description | +| --------------- | -------- | ------- | ----------------------------------------------------------------------------- | +| accessCode | No | RQST | The access code for the device | +| irccUrl | No (1) | None | The URL/Hostname for the IRCC service | +| commandsMapFile | No (2) | None | The commands map file that translates words to the underlying protocol string | +| modelName | No (3) | None | The model name of the device | + +1. See IP Address Configuration above +2. See transformations below +3. Only specify this if the model name is not automatically detected + +## Transformations + +These services use a commands map file that will convert a word (specified in the command channel) to the underlying command to send to the device. +This file will appear in your openHAB `conf/transformation` directory. + +When the device is ONLINE, the commandsMapFile configuration property has been set and the resulting file doesn't exist, the binding will write out the commands supported by the device to that file. +If discovery of the commands is not possible, a default set of commands will be written out which may or may not be correct for the device. +I highly recommend having the binding do this rather than creating the file from scratch. + +When the device is auto discovered, the commandsMapFile will be set to "scalar-{thingid}.map" (example: "scalar-ace2a0229f7a.map"). +You may want to change that in the things configuration, post-discovery, to something more reasonable. + +The format of the file will be: `{word}={cmd}` + +1. The word can be anything (in any language) and is the value send to the command channel. +2. The cmd is a URL Encoded value that will be sent to the device. + +An example from a Sony BluRay player (that was discovered by the binding): + +``` +... +Stop=AAAAAwAAHFoAAAAYAw%3D%3D +SubTitle=AAAAAwAAHFoAAABjAw%3D%3D +TopMenu=AAAAAwAAHFoAAAAsAw%3D%3D +Up=AAAAAwAAHFoAAAA5Aw%3D%3D +Yellow=AAAAAwAAHFoAAABpAw%3D%3D +... +``` + +Please note that you can recreate the .map file by simply deleting it from `conf/transformation` and restarting openHAB. + +## HDMI/CEC/ARC (AVRs, SoundBars) + +One of the issues with HDMI/CEC/ARC is that you can only increment or decrement the sound level by 1 if you are using HDMI/CEC/ARC to connect a soundbar or AVR. +If you set a volume to a specific level, the sound will only ever go up or down by a single value due to HDMI/CEC protocols. +To overcome this, the addon will (if configured - see below) issue a series of increment/decrement commands to reach a target level. +Example: if the processing is configured (see below), your current sound level is 10 and you set the sound level to 15 - the system will issue 5 increment commands to bring the soundbar/AVR up to 15. + +### Configuration options + +Edit the `conf/services/runtime.cfg` and add any (or all) of the following values: +1. `sony.things:audio-enablecec` +2. `sony.things:audio-forcecec` +3. `sony.things:audio-cecdelay` + +#### audio-enablecec + +This enables the HDMI/CEC processing. + +Set this value to `true` to allow the system to convert a set volume into a series of increments if HDMI/CEC is detected (see `audio-forcecec`). + +The default value is `false` + +#### audio-forcecec + +This set's whether to force HDMI/CEC processing (`true`) or attempt to detect whether HDMI/CEC processing should be used (`false`). + +You will want to set this value to `true` in the following cases: + +1. A soundbar/AVR is always used (in other words, you will NEVER use the TV speakers). +This will turn of auto-detection and issue less commands since it assumes HDMI/CEC processing. + +2. The soundbar/AVR is not correctly detected on the HDMI/CEC. +If you set the volume and the volume only goes up or down a single increment, then HDMI/CEC detection didn't work and this overrides that detection. + +The default value is `false` + +#### audio-cecdelay + +This is the delay (in ms) between increment/decrement requests. +Depending on your device, you may need to modify the delay to either improve responsiveness (by setting a lower delay if your device handles it properly) or fix missed messages (by setting a higher delay if your device is slower to respond). + +The default value is `250` (250ms); + +#### WARNING - Sony devices (soundbars, AVRs) + +Do ***NOT*** enable this if your soundbar/AVR is a sony device. +Sony has special processing when the HDMI/CEC device is a Sony device and this logic will simply not work. +You will need to connect to the device (using this binding) directly to change the volume **or** use the IRCC channel to increment/decrement the volume. + +Any setting of the volume or incrementing/decrementing the volume will set the TV speaker volume (which is not active) and will ***NOT*** be passed to the soundbar/AVR. + +#### Example + +``` +sony.things:audio-enablecec=true +sony.things:audio-forcecec=true +sony.things:audio-cecdelay=100 +``` + +This will enable special HDMI/CEC processing, force the ARC processing (ie disabling detection) and provide a 100ms delay between commands. + +## Channels + +The scalar service will dynamically generate the channels supported by your device. +The following table(s) will provide all the various channels supported by this addon - not all channels will be applicable for your device. +Example: Bravia TVs will support input channels but no terminal channels. +Likewise, even if you device supports a specific channel doesn't necessarily mean that channel will receive any data from the device (example: pl_durationsec may be supported by a TV but will ONLY be active if you are playing specific media and if that media's metadata is in a format the TV supports) + +### Channel ID Format + +Scalar uses the following format for channels: + +`{serviceId}#{channelName}[-{sonyid}]` + +1. `serviceId` is the service identifier (see sections below) +2. `channelName` is the name of the channel (listed in each section) +3. `sonyid` is an OPTIONAL additional Sony identifier if there are multiple channel names. +The ID can itself have multiple dashes to further denote uniqueness. + +Example: + +The current from the system service would have a channel id of `system#currenttime` + +The speaker volume from the audo service would have a channel id of `audio#volume-speaker` (the headphone volume would be `audio#volume-headphone`). + +A TV with multiple HDMI ports may have a status channel like +`avContent#in_status-extInput-hdmi1` or `avContent#in_status-extInput-hdmi2` . + +### General Settings + +A number of channels below will be marked as a General Setting (Item Type: GeneralSetting). +This was Sony's way of describing a setting in a general way. +A general setting will have one or more channels decribing the setting and may be a combination of different types of channels (dimmers, switches, numbers). + +Example: a general setting my be a "Custom Equalizer" and would actually have dimmer channels describing each band (treble, base, etc) + +The following channels may be described (one or more and in any combination): + +| Channel Type ID | Read/Write | Item Type | Description | +| --------------- | ---------- | --------- | -------------------------------------------------------------------------- | +| {name}-{id} | R | Number | A general setting representing a number (may have min/max assigned) | +| {name}-{id} | R | Switch | A general setting representing an on/off | +| {name}-{id} | R | String | A general setting representing a string (usually an enumeration of values) | +| {name}-{id} | R | Dimmer | A general setting representing a dimmer (may have min/max/step assigned) | + +The {name} represents the name of the channel name (described above) and would have `-{id}` appended to it to represent the unique setting id given by the setting. + +Example: on an AVR - a subwoofer would have a general setting for the subwoofer level. +This would be create a channel called `audio#speakersetting-subwooferlevel` (where `audio#speakersetting` is the name of the channel and the general setting id is `-subwooferlevel`). + +### Application Control Channels (service id of "appControl") + +The following channels are for the application control service. +The application control service provides management of applications that are installed on the device. + +| Channel Type ID | Read/Write | Item Type | Description | +| --------------- | ---------- | --------- | ----------------------------------- | +| appstatus-{id} | R (1) | String | The application status (start/stop) | +| apptitle-{id} | R | String | The application title | +| appicon-{id} | R | Image | The application icon | +| appdata-{id} | R | String | The application data | + +1. Please note that at the time of this writing, Sony broke the application status and this channel will not correctly reflect what is running + +The {id} is the unique identifier of the application and is simply the application stripped of any illegal (for a channel UID) characters and made lowercase. +Example: `Amazon Video` would have an id of `amazonvideo`. +So you'd have `appstatus-amazonvideo`, `apptitle-amazonvideo`, etc. + +### Audio Channels (service ID of "audio") + +The following channels are for the audio service. The audio service provides management of audio functions on the device. + +| Channel Type ID | Read/Write | Item Type | Description | +| --------------- | ---------- | -------------- | ----------------------------- | +| volume-{id} | R (1) | Dimmer | The volume from 0% to 100% | +| mute-{id} | R | Switch | Whether the volume is muted | +| soundsetting | RW | GeneralSetting | The setting for the sound | +| speakersetting | RW | GeneralSetting | The setting for the speaker | +| customequalizer | RW | GeneralSetting | The setting for the equalizer | + +1. Volume will be scaled to the device's range + +The {id} is the unique id of the volume from the device. +For TVs, there generally be a 'speaker' or 'main' and a 'headphone'. +For a multi-zone AVR, there will be a volume/mute for each zone (extoutput-zone1, extoutput-zone2, etc). + +Example: sending `.2` to the volume-headphone will put the headphone's volume to 20% of the device capability. + +### Audio/Video Content Channels (service ID of "avContent") + +The following channels are for the audio/visual content service. +The AV content service allows management of all audio/visual functions on the device including what is currently playing. + +#### General information + +The following channels are general information/settings for the device. + +| Channel Type ID | Read/Write | Item Type | Description | +| ---------------- | ---------- | -------------- | ------------------------------- | +| schemes | R (1) | String | Comma separated list of schemes | +| sources | R (2) | String | Comma separated list of source | +| bluetoothsetting | RW | GeneralSetting | The bluetooth settings | +| playbackmode | RW | GeneralSetting | The playback modes | + +1. Scheme are the high level schemes supported by the device. +Examples would be `radio`, `tv`, `dlna` and would also include schems for the input and outputs like `extInput` and `extOutput`. +2. Sources would contain the subcategories of each scheme in the format of `{scheme}:{source}`. +Examples would be `radio:fm`, `tv:analog`, `tv:atsct`, `extInput:hdmi` and `extInput:component` + +You can use the sources then to query information about each source via the `avContent:cn_parenturi` (see the Using Content section below). + +#### Parental Ratings + +The following channels reflect the parental ratings of the content being played. + +| Channel Type ID | Read/Write | Item Type | Description | +| ---------------------------- | ---------- | --------- | ---------------------------------- | +| pr_ratingtypeage | R | Number | The minimum age of the rating type | +| pr_ratingtypesony | R | String | Sony's rating type | +| pr_ratingcountry | R | String | The country of the rating system | +| pr_ratingcustomtypetv | R | String | The TV designated rating | +| pr_ratingcustomtypempaa | R | String | The MPAA designated rating | +| pr_ratingcustomtypecaenglish | R | String | The english designated rating | +| pr_ratingcustomtypecafrench | R | String | The french designated rating | +| pr_unratedlock | R | Switch | Whether unrated can be shown | + +#### Now Playing + +The following channels reflect information about what is currently playing. +Please note that which channels are being updated depends on the source of the information and the format it is in (ie album name will only appear if the source is some type of song and the metadata for the song is in a format sony can parse). + +Please note the following acyronyms are used: + +| Name | Description | +| ---- | -------------------------- | +| BIVL | Sony Bravia Internet Link | +| DAB | Digital Audio Broadcasting | + +| Channel Type ID | Read/Write | Item Type | Description | +| -------------------- | ---------- | ----------------- | --------------------------------- | +| pl_albumname | R | String | Album name | +| pl_applicationname | R | String | Application name | +| pl_artist | R | String | Artist | +| pl_audiochannel | R | String | Audio channel | +| pl_audiocodec | R | String | Audio codec | +| pl_audiofrequency | R | String | Audio frequency | +| pl_bivlassetid | R | String | BIVL asset id | +| pl_bivlprovider | R | String | BIVL provider | +| pl_bivlserviceid | R | String | BIVL service id | +| pl_broadcastfreq | R | Number:Frequency | Broadcasting frequency | +| pl_broadcastfreqband | R | String | Broadcasting frequency band | +| pl_channelname | R | String | Channel name | +| pl_chaptercount | R | Number | Chapter count | +| pl_chapterindex | R | Number | Chapter index | +| pl_cmd | RW (1) | String | Playing command | +| pl_contentkind | R | String | Content kind | +| pl_dabcomponentlabel | R | String | DAB component label | +| pl_dabdynamiclabel | R | String | DAB dynamic label | +| pl_dabensemblelabel | R | String | DAB ensemble label | +| pl_dabservicelabel | R | String | DAB service label | +| pl_dispnum | R | String | Display number | +| pl_durationmsec | R | Number:DataAmount | Duration | +| pl_durationsec | R | Number:DataAmount | Duration | +| pl_fileno | R | String | File number | +| pl_genre | R | String | Genre | +| pl_index | R | Number | Index number | +| pl_is3d | R | String | 3D setting | +| pl_mediatype | R | String | Media type | +| pl_originaldispnum | R | String | Original display number | +| pl_output | R | String | Output | +| pl_parentindex | R | Number | Parent index | +| pl_parenturi | R | String | Parent URI | +| pl_path | R | String | Path to content | +| pl_playlistname | R | String | Playlist name | +| pl_playspeed | R | String | Playing speed | +| pl_playstepspeed | R | Number | Playing step speed | +| pl_podcastname | R | String | Podcast name | +| pl_positionmsec | R | Number:DataAmount | Position | +| pl_positionsec | R | Number:DataAmount | Position | +| pl_presetid | RW (1) | Number | Preset identifier | +| pl_programnum | R | Number | Program number | +| pl_programtitle | R | String | Program title | +| pl_repeattype | R | String | Repeat type | +| pl_service | R | String | Service identifier | +| pl_source | R | String | Source | +| pl_sourcelabel | R | String | Source label | +| pl_startdatetime | R | String | Start date/time | +| pl_state | R | String | Current state | +| pl_statesupplement | R | String | Supplemental information to state | +| pl_subtitleindex | R | Number | Subtitle index | +| pl_title | R | String | Title | +| pl_totalcount | R | Number | Total count | +| pl_tripletstr | R | String | Triplet string | +| pl_uri | R | String | URI | +| pl_videocodec | R | String | Video codec | + +1. The playing command supports the following: + +| Command | Description | +| --------- | ------------------------------------------ | +| play | Continue playing content | +| pause | Pause content | +| stop | Stop playing content | +| next | Play next content | +| prev | Play previous content | +| fwd | Fast forward content | +| bwd | Rewind content | +| fwdseek | Seek forward (radio only) | +| bwdseek | Seek backward (radio only) | +| setpreset | Set current as preset (set as pl_presetid) | +| getpreset | Get preset (as set in pl_presetid) | + +fwd/bwd on a radio will move the frequency manually one step forward/backward + +#### TV/Radio Preset channels + +Since changing TV and Radio stations are a frequent occurance, the addon will define a `ps_channel-xxxx` channel for each TV and/or radio source. +This is a helper channel to select any scanned (for TV) or preset (for radio) channels. +If the UI you are using supports state, then a button will be created to be able to quickly select the value for the channel + +| Channel Type ID | Read/Write | Item Type | Description | +| ---------------- | ---------- | --------- | ------------------------- | +| ps_channel-{src} | RW | String | The preset for the source | + +`{src}` will be the source portion of the source URI. +Example: if you have a source (from the `sources` channel) of `tv:atsct`, a `ps_channel-atsct` will be created with options for each digital (ATSCT) channel defined. + +Example: for the XBR-43X830C (TV) - I'd have a channel called `ps_channel-atsct`. +If I send `5.2` to that channel, the TV would switch station 5.2 (and on the next polling, the associated playing and content channels would be updated to reflect that change). + +##### Configurable TV presets +To ease the selection of a large number of TV preset channels, the channels for selection can be filtered and sorted by use of a configuration file. +This feature can be enabled by setting the (advanced) thing configuration `Enable Configurable Presets` and saving the new configuration. + +If this featured is enabled, then for each (non-empty) TV source a csv file in the folder `/userdata/config/sony/presets` will be generated after each thing restart. +The name of the file is a concatenation of the channel type id and the thing id (e.g. `ps_channel-dvbs_d8d43c4d563d.csv`). + +The generated file contains a list of all TV channels of the given source. The last column named `Rank` determines +which TV channels should be added to the preset list and in which order. Only the value of this column should be edited as follows: + +- `rank < 0`: TV channel is excluded +- `rank = 0`: TV channel is added to end of preset list +- `rank > 0`: TV channel is added in the order of the rank value + +Channels with same rank value are naturally ordered in the preset list. Initially, the rank value is 0 for all channels. + +A refresh of the preset after a change of the csv file can be triggered by selecting the pseudo channel `--REFRESH--` as state option from a UI component that is linked to the according preset channel. + + +#### Device Inputs + +The following will be a list of all inputs defined on the device. +Please note that these will only appear on devices that are single output zone devices (TVs, blurays, etc). +Please see terminal status for multizone devices. +Please note that dynamic inputs (such as a USB) will not be listed here unless the USB was plugged in when the thing was created. + +| Channel Type ID | Read/Write | Item Type | Description | +| ------------------- | ---------- | --------- | ----------------------------------------- | +| in_uri-{inp} | R (1) | String | The URI of the input | +| in_title-{inp} | R (2) | String | The title of the input (hdmi, etc) | +| in_label-{inp} | R (2) | String | The label of the input | +| in_icon-{inp} | R (3) | String | The icon meta data representing the input | +| in_connection-{inp} | R (4) | Switch | Whether something is connected | +| in_status-{inp} | R (5) | String | The status of the input | + +1. Each input on the system will be assigned this set of channels. +If you have for inputs (HDMI1-HDMI4), you'd have four sets of these channels each named for the input (`in_uri-hdmi1`, `in_uri-hdmi2`, etc). +Note: if your device only has a single input - the channel will be named `in_uri-main`. +2. The title is the official title of the input (hdmi1, hdmi2, etc) and the label is text that you have specified on the device for the input (maybe DVD, Media, etc). +3. This is meta data for the icon. +Example: this channel will contain `meta:hdmi` for an HDMI icon or `meta:video` for the Video input icon. +4. The connection will tell you if something is connected to that input. +5. The status will tell you the status (the actual text depends on the device). +Generally, if this is blank - the input is not selected. +If not blank, the input is selected. +If the input has some type of content, the status will be 'true' or 'active'. +If the input is selected but the feeding device is off (or is not outputting any content), the status generally will be 'false' or 'inactive'. + +#### Terminal Sources + +The following will be a list of all inputs and outputs defined on the device (may be virtual inputs/outputs as well such as bluetooth). +If the device is a single output zone device, there will be a single device with an id of "main" that describes the output (inputs will be defined in the `in_` channels). +If the device is a multi-zone devices (AVRS), then the terminals will include all inputs and outputs. + +Please note that dynamic inputs (such as a USB) will not be listed here unless the USB was plugged in when the thing was created. + +| Channel Type ID | Read/Write | Item Type | Description | +| ------------------ | ---------- | --------- | ----------------------------------------- | +| tm_uri-{id} | R (1) | String | The URI of the terminal | +| tm_title-{id} | R (2) | String | The title of the terminal | +| tm_label-{id} | R (2) | String | The label of the terminal | +| tm_icon-{id} | R | Image | The icon representing the terminal | +| tm_connection-{id} | R (3) | String | The connection status of the terminal | +| tm_active-{id} | RW (4) | Switch | Whether the terminal is active | +| tm_source-{id} | RW (5) | String | The source URI connected to this terminal | + +1. Each terminal (input or output) will be assigned this set of channels. +If you have for inputs (HDMI1-HDMI4), you'd have four sets of these channels each named for the input (`tm_uri-hdmi1`, `tm_uri-hdmi2`, etc). +Note: if your device only has a single input or a single output - the channel will be named `tm_uri-main`. +2. The title is the official title of the terminal (hdmi1, hdmi2, etc) and the label is text that you have specified on the device for the input (maybe DVD, Media, etc). +3. The connection will tell you if something is connected to that terminal. +Unlike `in_connection`, this is an open ended string from sony that potentially gives you more information than a switch. +Generally the value will always be "connected" on AVRs +4. Specifies whether the terminal is active or not. +Active, generally, means either the terminal is selected (in the case of inputs) or is powered (in the case of outputs). +Setting active on an output will power on the terminal +5. This will be the source identifier of the source that is connected to this terminal if that terminal is an output. +Setting it on an output terminal will switch the terminal to that input source. + +Please note that some terminal sources **cannot** be activated via the tm_active (sending "ON" to the tm_active channel will do nothing). +Sources that fall into this category are generally content based sources - like USB storage, BluRay/DVD storage, etc. +To activate these, you just select the content (as described in the next section) for the terminal source to become active. + +#### Content + +This set of channels allows you to browse through some type of content (USB, TV channels, radio stations, etc). +Available starting points will be found in the `sources` channel described above. +Please use the `isbrowesable` channel to determine if that source is a browesable source (for some reasons DLNA is not a browseable source as they want you to use a DLNA service to browse it instead). + +Please note that which channels are being updated depends on the source of the information and the format it is in (ie album name will only appear if the content is some type of song and the metadata for the song is in a format sony can parse). + +Please note the following acyronyms are used: + +| Name | Description | +| ---- | -------------------------- | +| BIVL | Sony Bravia Internet Link | +| DAB | Digital Audio Broadcasting | +| EPG | Electronic Program Guide | + +| Channel Type ID | Read/Write | Item Type | Description | +| --------------------------- | ---------- | ----------------- | -------------------------------- | +| cn_albumname | R | String | Album name | +| cn_applicationname | R | String | Application name | +| cn_artist | R | String | Artist | +| cn_audiochannel | R | String | Audio channel | +| cn_audiocodec | R | String | Audio codec | +| cn_audiofrequency | R | String | Audio frequency | +| cn_bivlserviceid | R | String | BIVL service id | +| cn_bivleassetid | R | String | BIVL asset id | +| cn_bivlprovider | R | String | BIVL provider | +| cn_broadcastfreq | R | Number:Frequency | Broadcast frequency | +| cn_broadcastfreqband | R | String | Broadcase frequency band | +| cn_channelname | R | String | Channel name | +| cn_channelsurfingvisibility | RW (4) | String | Visibility setting for surfing | +| cn_chaptercount | R | Number | Chapter count | +| cn_chapterindex | R | Number | Chapter index | +| cn_childcount | R (1) | Number | Count of children | +| cn_clipcount | R | Number | Clip count | +| cn_cmd | RW (2) | String | Content command | +| cn_contentkind | R | String | Content kind | +| cn_contenttype | R | String | Content type | +| cn_createdtime | R | String | Content created date/time | +| cn_dabcomponentlabel | R | String | DAB component label | +| cn_dabdynamiclabel | R | String | DAB dynamic label | +| cn_dabensemblelabel | R | String | DAB ensemble label | +| cn_dabservicelabel | R | String | DAB service label | +| cn_description | R | String | Content description | +| cn_directremotenum | R | Number | Direct remote number | +| cn_dispnum | R | String | Display number | +| cn_durationmsec | R | Number:DataAmount | Duration | +| cn_durationsec | R | Number:DataAmount | Duration | +| cn_epgvisibility | RW (4) | String | Visibility setting for EPG | +| cn_eventid | R | String | Event identifier | +| cn_fileno | R | String | File number | +| cn_filesizebyte | R | Number:DataAmount | File size | +| cn_folderno | R | String | Folder number | +| cn_genre | R | String | Genre | +| cn_globalplaybackcount | R | Number | Global playback count | +| cn_hasresume | R (3) | String | Can resume | +| cn_idx | RW (1) | Number | Content index number | +| cn_is3d | R (3) | String | 3D setting | +| cn_is4k | R (3) | String | 4K setting | +| cn_isalreadyplayed | R (3) | String | Already played setting | +| cn_isautodelete | R (3) | String | Auto delete setting | +| cn_isbrowsable | R (3) | String | Whether browsesable or not | +| cn_isnew | R (3) | String | Whether new or not | +| cn_isplayable | R (3) | String | Whether playable or not | +| cn_isplaylist | R (3) | String | Whether a playlist or not | +| cn_isprotected | RW (3) | String | Whether protected or not | +| cn_issoundphoto | R (3) | String | Whether a sound photo or not | +| cn_mediatype | R | String | Media type | +| cn_originaldispnum | R | String | Original display number | +| cn_output | R | String | Possible output | +| cn_parentalcountry | R | String | Parental rating country | +| cn_parentalrating | R | String | Parental rating | +| cn_parentalsystem | R | String | Parental rating system | +| cn_parentindex | R | Number | Parent index | +| cn_parenturi | RW (1) | String | Parent content URI | +| cn_path | R | String | Path to content | +| cn_playlistname | R | String | Playlist name | +| cn_podcastname | R | String | PODcast name | +| cn_productid | R | String | Product identifier | +| cn_programmediatype | R | String | Program media type | +| cn_programnum | R | Number | Program number | +| cn_programservicetype | R | String | Program serivce type | +| cn_programtitle | R | String | Program title | +| cn_remoteplaytype | R | String | Remote play type | +| cn_repeattype | R | String | Repeat type | +| cn_service | R | String | Service type | +| cn_sizemb | R | Number:DataAmount | Size | +| cn_source | R | String | Source | +| cn_sourcelabel | R | String | Source label | +| cn_startdatetime | R | String | Start date/time | +| cn_state | R | String | Current state | +| cn_statesupplement | R | String | Supplementl information to state | +| cn_storageuri | R | String | Storage URI | +| cn_subtitlelanguage | R | String | Subtitle language | +| cn_subtitletitle | R | String | Subtitle title | +| cn_synccontentpriority | R | String | Synchronized content priority | +| cn_title | R | String | Content title | +| cn_totalcount | R | Number | Total count | +| cn_tripletstr | R | String | Triplet string | +| cn_uri | R (1) | String | Content URI | +| cn_usercontentflag | R | Switch | User content flag | +| cn_videocodec | R | String | Video codec | +| cn_visibility | RW (4) | String | General visibility setting | + +1. Please refer to "Using Content" section for more information +2. The only command supported is "select" to start playing the content +3. Generally these flags contain either "true" or "false". +They are not switches since sony defined the item as strings and there may potentially be other values I'm unaware of. +4. You can set the visibility of the content to the various guides available and this generally only applies to TV/Radio stations (where you can 'surf' through them) + +### Browser Channels (service ID of "browser") + +The following list the channels for the browser service. +The browser service allows management of a browser on the device. + +| Channel Type ID | Read/Write | Item Type | Description | +| --------------- | ---------- | --------- | ------------------------------- | +| browsercontrol | RW | String | The browser command | +| texturl | RW | String | The URL in the browser | +| texttitle | R | String | The title of the current page | +| texttype | R | String | The type of the current page | +| textfavicon | R | Image | The favicon of the current page | + +The browsercontrol allows you to send a command to the browser such as 'start' (or 'activate') or 'stop' to start/stop the browser. +Send a URL to the texturl to have the browser go to that URL (please note that you can generally just send the URL and the browser will automatically activate). + +### Illumination (service ID of "illumination") + +The following channels are for the illumination service. The illumination service provides management of illumination functions on the device. + +| Channel Type ID | Read/Write | Item Type | Description | +| -------------------- | ---------- | -------------- | -------------------------------- | +| illuminationsettings | RW | GeneralSetting | The setting for the illumination | + +### CEC Channels (service ID of "cec") + +The following list the channels for the HDMI CEC (consumer electronics control) service. +The CEC service allows management of the HDMI CEC settings. + +| Channel Type ID | Read/Write | Item Type | Description | +| ---------------------- | ---------- | --------- | ------------------------------------------------------------- | +| controlmode | RW | Switch | Whether CEC is active or not | +| mhlautoinputchangemode | RW | Switch | Whether the device will automatically change to the MHL input | +| mhlpowerfeedmode | RW | Switch | Whether the device will power MHL device | +| poweroffsyncmode | RW | Switch | Whether the device will turn off with CEC | +| poweronsyncmode | RW | Switch | Whether the device will power on with CEC | + +### System Channels (service ID of "system") + +The following list the channels for the system service. +The system service allows management of general system settings. + +| Channel Type ID | Read/Write | Item Type | Description | +| ------------------------------- | ---------- | ----------------- | ----------------------------------------------------- | +| powerstatus | RW (1) | Switch | The current power status (see notes) | +| currenttime | R | DateTime | Current time of the device (format depends on device) | +| ledindicatorstatus | RW (2) | String | LED indicator status | +| powersavingsmode | RW (3) | String | The power savings mode | +| wolmode | RW | Switch | Whether WOL is enabled | +| language | RW | String | The langauge used | +| reboot | RW (4) | Switch | Whether to reboot the device | +| syscmd | RW (5) | String | The IRCC command to send | +| postalcode | RW | String | The postal code of the device | +| devicemiscsettings | RW | GeneralSetting | Misc device settings (timezones, auot update, etc) | +| powersettings | RW | GeneralSetting | The power settings (wol, standby, etc) | +| sleepsettings | RW | GeneralSetting | The sleep timer settings | +| wutangsettings | RW | GeneralSetting | The wutang settings (google cast settings) | +| st_devicename-{src} | R (6) | String | The storage device name | +| st_error-{src} | R (6) | String | Any storage errors | +| st_filesystem-{src} | R (6) | String | The storage file system | +| st_finalizestatus-{src} | R (6) | String | The storage finalization status | +| st_format-{src} | R (6) | String | The storage format | +| st_formatstatus-{src} | R (6) | String | The storage format status | +| st_formattable-{src} | R (6) | String | The storage formattable status | +| st_formatting-{src} | R (6) | String | Whether the storage is formatting | +| st_freecapacitymb-{src} | R (6) | Number:DataAmount | The storage free space | +| st_hasnonstandarddata-{src} | R (6) | String | Whether the storage has non-standard data | +| st_hasunsupportedcontents-{src} | R (6) | String | Whether the storage has unsupported contents | +| st_isavailable-{src} | R (6) | String | Whether the storage is available | +| st_islocked-{src} | R (6) | String | Whether the storage is locked | +| st_ismanagementinfofull-{src} | R (6) | String | Whether the storage management info is full | +| st_isprotected-{src} | R (6) | String | Whether the storage is protected | +| st_isregistered-{src} | R (6) | String | Whether the storage is registered | +| st_isselfrecorded-{src} | R (6) | String | Whether the storage is self recorded | +| st_issqvsupported-{src} | R (6) | String | Whether the storage is SQV (standard quality voice) | +| st_lun-{src} | R (6) | Number | The storage LUN (logical unit number) | +| st_mounted-{src} | R (6) | String | The storage mount status | +| st_permission-{src} | R (6) | String | The storage permission | +| st_position-{src} | R (6) | String | The storage position (front, back, internal, etc) | +| st_protocol-{src} | R (6) | String | The storage protocol | +| st_registrationdate-{src} | R (6) | String | The storage registration date | +| st_systemareacapacitymb-{src} | R (6) | Number:DataAmount | The storage system capacity | +| st_timesectofinalize-{src} | R (6) | Number:Time | The time to finalize | +| st_timesectogetcontents-{src} | R (6) | Number:Time | The time to get contents | +| st_type-{src} | R (6) | String | The storage type | +| st_uri-{src} | R (6) | String | The storage URI | +| st_usbdevicetype-{src} | R (6) | String | The storage USB device type | +| st_volumelabel-{src} | R (6) | String | The storage label | +| st_wholeCapacityMB-{src} | R (6) | Number:DataAmount | The storage whole capacity | + +1. The power status may not be accurate on startup. Some devices will report ON when, in fact, they are off. +2. Sets the LED status - generally "Off", "Low" or "High" (there may be others specific to your device like "AutoBrightnessAdjust") +3. Sets the power savings mode - generally "Off", "Low" or "High" (there may be others specific to your device) +4. Sending 'on' to this channel will reboot the device +5. Sends an IRCC command to the device. +This can either be the raw IRCC command (AAAAAwAAHFoAAAAYAw==) or can be a name (`Home`) that is transformed by the transformation file +6. These channels will be repeated by every storage source (ie source for a scheme of ```storage```). +Example: if you have a ```USB1``` and ```CD``` storage sources, you'd have a ```st_uri-usb1``` and a ```st_uri-cd``` channel. +Please note that, on many devices, the storage information is not reliable and a bit quirky (the st_mounted status shows unmounted even though the storage is mounted). +However, the st_mounted will reliably change when a source is physically mounted/unmounted from the unit. +Just the initial status will likely be incorrect. + +### Video Channels (service ID of "video") + +The following list the channels for the video service. +The video service allows management of the video quality itself. + +| Channel Type ID | Read/Write | Item Type | Description | +| ---------------------- | ---------- | -------------- | --------------------------------------------------- | +| picturequalitysettings | RW | GeneralSetting | The settings for picture quality | + +If supported, contains about 20 different quality settings (dimming, mode, color, hdr mode, etc) + +### Video Screen Channels (service ID of "videoScreen") + +The following list the channels for the video screen service. +The video service allows management of the video (screen) itself. + +| Channel Type ID | Read/Write | Item Type | Description | +| ---------------------- | ---------- | -------------- | --------------------------------------------------- | +| audiosource | RW | String | The audio source of the screen (speaker, headphone) | +| bannermode | RW | String | The banner mode (demo) | +| multiscreenmode | RW | String | The multiscreen mode (pip) | +| pipsubscreenposition | RW | String | The pip screen position | +| scenesetting | RW | String | The sceen settings | + +The values of these channels are pretty much unknown and you'll need to experiment with your device if you wish to use them. + +## Using Content + +Many devices support content - whether the content is a list of TV stations, a list of radio stations, a DVD/bluray of chapters, a USB of music. +Most content can be represented in a folder structure (which is how Sony represents it). +Every resource is represented by a URI (ex: tv:atsct?dispNum=4.1&trip=1793.25.3&srvName=UNC-TV represents a URI to a digital TV (ATSCT) station 4.1 called UNC-TV). + +To use the content involves the following channels: + +1. cn_parenturi +2. cn_childcount +3. cn_idx +4. cn_uri +5. cn_cmd + +The parenturi is the 'folder' you are looking at. +Example: `tv:atsct` would be the parent of all digital ATSCT channels +The childcount would be how many child items are in parenturi. +Example: if I had 20 ATSCT stations, the count would be `20` +The idx (0-based) represents the current child within the folder. +Example: if idx is `3` - the current content is the 4th child in the parent uri +The uri represents the uri to the current child within the folder. +Example: if the 4th child is UNC-TV, the uri would be `tv:atsct?dispNum=4.1&trip=1793.25.3&srvName=UNC-TV` +The cmd can then be used to issue a command for the current child. +Example: sending `select` to the cmd would select the resource in uri (ie would change the channel to UNC-TV) + +### Specific example XBR-43X830C (US Bravia TV) + +This example will use the PaperUI with the following channels linked: + +1. cn_parenturi (as described above) +2. cn_childcount (as described above) +3. cn_idx (as described above) +4. cn_uri (as described above) +5. cn_cmd (as described above) +6. cn_title (the current child's title) +7. sources (see next statement) + +First you need to find the highest level parent URI your device supports. +The simpliest method would be to link the `sources` channel and view it. +The sources channel will provide a comma delimited list of the high level URIs. +Please note that a USB source may not be present immediately after plugging a USB into the device (the source will appear after the next polling refresh). + +From the X830C - the sources are `tv:analog,extInput:widi,extInput:cec,tv:atsct,extInput:component,extInput:composite,extInput:hdmi` + +To view all the digital channels for ATSCT, I double click on the cn_parenturi, type in `tv:atsct` and press the checkmark. + +After a second or so, the cn_count was updated to `29` (saying I have 29 channels), the cn_idx was updated to `0` (defaults back to 0 on a parent uri change), the cn_uri was updated to `tv:atsct?dispNum=4.1&trip=1793.25.3&srvName=UNC-TV` and the cn_title was updated to `UNC-TV`. + +If I double check on the cn_idx, update it to `5` and press the check mark... + +After a second or so, the cn_uri was updated to `tv:atsct?dispNum=5.2&trip=1851.48.4&srvName=WRAL DT` and the cn_title was updated to `WRAL DT`. + +To have the TV change to that channel - I find the cn_cmd and press the `Select` button (which is the same as sending `select` to the cn_cmd channel) and the TV switches to digital station 5.2 (WRAL DT) + +You can use the same general technique to navigate USB folder structure + +## Full Examples + +_Really recommended to autodiscover rather than manually setup thing file_ + +The scalar service will dynamically generate the channels supported by your device. +Since potentially hundreds of channels can be created, the following is a small example to help get you started. + +scalar.Things: + +``` +Thing sony:scalar:home [ deviceAddress="http://192.168.1.123:52323/dmr.xml", deviceMacAddress="aa:bb:cc:dd:ee:ff", refresh=-1 ] +``` + +scalar.items: + +``` +String Scalar_ParentUri "Parent URI" { channel="sony:scalar:home:avContent#cn_parenturi" } +Number Scalar_ChildCount "Child Count" { channel="sony:scalar:home:avContent#cn_childcount" } +Number Scalar_ContentIdx "Content Index" { channel="sony:scalar:home:avContent#cn_idx" } +String Scalar_ContentUri "Content URI" { channel="sony:scalar:home:avContent#cn_uri" } +String Scalar_SelectContent "Select Content URI" { channel="sony:scalar:home:avContent#cn_cmd" } +``` + +## Advanced Users Only + +The following information is for more advanced users... + +### Local Information + +**THIS IS NOT RECOMMENDED TO USE BUT I'M DOCUMENTING THIS AS A LAST RESORT** + +One of the issues with the dynamically create channels is that some channels are not detected if the device is not on. +A common example would be a Bravia TV. +If you start openHAB and the thing goes online without the TV being one, there is a high likelyhood that the `audio#volume-xxx` channels will not be defined. + +An attempt to 'fix' this situation is to create a thing type for the TV (that predefines the channels needed) and then to use that local thing type as the source of channels. + +This addon defines the following directories: + +1. `userdata/sony/definition/capabilities` which will contain the capabilities of the TV after it's gone online +2. `userdata/sony/definition/types` which will define a thing type of the TV after it's gone online +3. `userdata/sony/db/local/types` which will be the source of local thing types + +Essentially - when the TV goes online - this addon will: + +1. Query the device capabilities and write them to the `userdata/sony/definition/capabilities/{modelname}.json` if that file doesn't already exist +2. If a file `userdata/sony/db/local/types/{modelname}.json` exists, constructs the thing and thing type from this file +3. If the file doesn't exist, dynamically determine the channels for the device and write a thing type defintion to `userdata/sony/definition/types/{modelname}.json` if it doesn't already exist. + +Now - to solve the problem of the non detected channels - you can follow this procedure: + +1. Stop openHAB +2. Delete `{modelname}.json` from `userdata/sony/definition/types` if it exists. +3. Turn the device on and make sure it's at the home screen +4. Start openHAB and wait for the thing to go online +5. Verify that `{modelname}.json` in `userdata/sony/definition/types` exists. +6. Copy the `{modelname}.json` in `userdata/sony/db/local/types` directory +7. Wait a minute or so and the thing type of your thing will change to this file + +You may disable the local thing definition by editing the `conf/services/runtime.cfg` and including the following: + +``` +sony.sources:local=false +``` + +Setting the value to anything but 'false' will result in that provider being activated. +Please note that disabling will disable the ability to use custom thing types for your devices. + +### Sony Support pages + +This addon provides some support pages that can be useful to you. +If you go to "{ip}:{port}/sony", you can access the page (where IP/port is the IP/Port address that openHAB is listening on for it's web interface). + +#### Sony API/Definition Explorer + +The first page is the API/Definition explorer. +This will allow you to load your device file and explore the API on your own. +Please ignore the "Rest API, Merge File and Save" functionality as it only applies to myself (to manage devices). + +To use this page: + +1. To explore your device, press the "Load File" button and navigate to your device file (in `userdata/sony/definition/capabilities` folder as described above) and open your device JSON file. +This will load all the capabilities into the right side. +2. Click on one of the capabiliites and the left side will be loaded with details of the call +3. Change any parameters you want to press the execute button +4. Results appear in the bottom window. + +Example of seeing the digital channels on a TV: + +1. Click on the `avContent/getSchemeList` capability and press execute (there are no parameters for this call) +2. The results will be something like `[[{"scheme":"tv"},{"scheme":"extInput"}]]`. +3. Click on the `avContent/getSourceList` capability, change the paramater from `{"scheme":"string"}` to `{"scheme":"tv"}` (as shown in the getSchemeList call) and then press execute +4. The results will be something like `[[{"source":"tv:analog"},{"source":"tv:atsct"}]]` +5. Copy the source you want to explore. +For this example I copied `{"source":"tv:atsct"}` (digital ATSCT source) +6. Click on the `avContent/getContentList` (highest version) capability +7. Replace the parameter with what you copied. +For our example - `{"source":"tv:atsct"}` and press execute +8. The results will be something like `[[{"uri":"tv:atsct?dispNum=4.1&trip=1793.25.3&srvName=UNC-TV","title":"UNC-TV","index":0,"dispNum":"4.1","originalDispNum":"4.1","tripletStr":"1793.25.3","programNum":3,"programMediaType":"tv","visibility":"visible"},...`. +You'll have a single line for each digital (ATSCT) TV station that has been scanned on the set. +9. Copy the URI portion that you want to tune to. +For our example - `tv:atsct?dispNum=4.1&trip=1793.25.3&srvName=UNC-TV` +10. Click on the `avContent/playContent` and replace the `{"uri":"string"}` with `{"uri":"tv:atsct?dispNum=4.1&trip=1793.25.3&srvName=UNC-TV"}` and press Execute +11. The TV should switch to that channel. +For our example - it will switch to station 4.1 (UNC-TV). + +Feel free to explore other APIs diff --git a/bundles/org.openhab.binding.sony/README-SimpleIp.md b/bundles/org.openhab.binding.sony/README-SimpleIp.md new file mode 100644 index 0000000000000..6063691cd9f0b --- /dev/null +++ b/bundles/org.openhab.binding.sony/README-SimpleIp.md @@ -0,0 +1,120 @@ +# Simple IP + +The Simple IP protocol is a simplified version of IRCC and appears to be only supported on some models of Bravia TVs. You must enable "Simple IP Control" on the devices (generally under ```Settings->Network->Home Network->IP Control->Simple IP Control```) but once enabled - does not need any authentication. The Simple IP control provides direct access to commonly used functions (channels, inputs, volume, etc) and provides full two-way communications (as things change on the device, openHAB will be notified immediately). + +## Authentication + +Simple IP needs no additional authentication and should automatically come online if the configuration is correct (and the TV has been setup correctly). + +## Thing Configuration + +The configuration for the Simple IP Service Thing: + +| Name | Required | Default | Description | +| --------------- | -------- | ------- | ----------------------------------------------------------------------------- | +| commandsMapFile | No (1) | None | The commands map file that translates words to the underlying protocol string | +| netInterface | No (2) | eth0 | The network interface the is using (eth0 for wired, wlan0 for wireless). | + +1. See transformations below +2. The netInterface is ONLY required if you wish to retrieve the broadcast address or mac address + +## Transformations + +The Simple IP service requires a commands map file that will convert a word (specified in the command channel) to the underlying command to send to the device. This file will appear in your openHAB ```conf/transformation``` directory. + +When the Simple IP device is ONLINE, the commandsMapFile configuration property has been set and the resulting file doesn't exist, the binding will write out the commands that have been documented so far. I highly recommend having the binding do this rather than creating the file from scratch. Please note that the end of the file you will see gaps in the numbers - I believe those are dependent upon the TV's configuration (# of hdmi ports, etc). Feel free to play with those missing numbers and if you figure out what they do - post a note to the forum and I'll document them. + +When the Simple IP device is auto discovered, the commandsMapFile will be set to "simpleip-{thingid}.map". You may want to change that, post-discovery, to something more reasonable. + +The format of the file will be: +```{word}={cmd}``` + +1. The word can be anything (in any language) and is the value send to the command channel. +2. The cmd is an integer representing the ir command to execute. + +An example from a Sony Bravia XBR-43X830C (that was discovered by the binding): + +``` +... +Input=1 +Guide=2 +EPG=3 +Favorites=4 +Display=5 +Home=6 +... +``` + +Please note that you can recreate the .map file by simply deleting it from ```conf/transformation``` and restarting openHAB. + +## Channels + +All devices support the following channels (non exhaustive): + +| Channel Type ID | Read/Write | Item Type | Description | +| ----------------- | ---------- | --------- | --------------------------------------------------------------- | +| ir | W | String | The ir codes to send (see transformations above) | +| power | R | Switch | Whether device is powered on | +| volume | R | Dimmer | The volume for the device | +| audiomute | R | Switch | Whether the audio is muted | +| channel | R | String | The channel in the form of "x.x" ("50.1") or "x" ("13") | +| tripletchannel | R | String | The triplet channel in the form of "x.x.x" ("32736.32736.1024") | +| inputsource | R | String | The input source ("antenna"). See note 1 below | +| input | R | String | The input in the form of "xxxxyyyy" ("HDMI1"). See note 2 below | +| picturemute | R | Switch | Whether the picture is shown or not (muted) | +| togglepicturemute | W | Switch | Toggles the picture mute | +| pip | R | Switch | Enables or disabled picture-in-picture | +| togglepip | W | Switch | Toggles the picture-in-picture enabling | +| togglepipposition | W | Switch | Toggles the picture-in-picture position | + +1. The text of the input source is specific to the TV. The documentation lists as valid dvbt, dvbc, dvbs, isdbt, isdbbs, isdbcs, antenna, cable, isdbgt. However, "atsct" seems to be supported as well and others may be valid. +2. The input can be either "TV" or "xxxxyyyy" where xxxx is the port name and yyyy is the port number. Valid port names (case insensitive) are "hdmi", "scart", "composite", "component", "screen mirroring", and "pc rgb input". The port number is dependent on how many ports the device supports. Example: the X830 supports 4 hdmi ports - so "hdmi1", "hdmi2", "hdmi3" and "hdmi4" are all valid. + +## Full Example + +simpleip.Things: + +``` +sony:simpleip:home [ deviceAddress="192.168.1.72", commandsMapFile="braviaircodes.map", netInterface="eth0" ] +``` + +simpleip.items: + +``` +String Bravia_IR "IR [%s]" { channel="sony:simpleip:home:ir" } +Switch Bravia_Power "Power [%s]" { channel="sony:simpleip:home:power" } +Dimmer Bravia_Volume "Volume [%s]" { channel="sony:simpleip:home:volume" } +Switch Bravia_AudioMute "Audio Mute [%s]" { channel="sony:simpleip:home:audiomute" } +String Bravia_Channel "Channel [%s]" { channel="sony:simpleip:home:channel" } +String Bravia_TripletChannel "Triplet Channel [%s]" { channel="sony:simpleip:home:tripletchannel" } +String Bravia_InputSource "Input Source [%s]" { channel="sony:simpleip:home:inputsource" } +String Bravia_Input "Input [%s]" { channel="sony:simpleip:home:input" } +Switch Bravia_PictureMute "Picture Mute [%s]" { channel="sony:simpleip:home:picturemute" } +Switch Bravia_TogglePictureMute "Toggle Picture Mute [%s]" { channel="sony:simpleip:home:togglepicturemute", autoupdate="false" } +Switch Bravia_Pip "PIP [%s]" { channel="sony:simpleip:home:pip" } +Switch Bravia_TogglePip "Toggle PIP [%s]" { channel="sony:simpleip:home:togglepip", autoupdate="false" } +Switch Bravia_TogglePipPosition "Toggle PIP Position [%s]" { channel="sony:simpleip:home:togglepipposition", autoupdate="false" } +``` + +simpleip.sitemap: + +``` +sitemap demo label="Main Menu" +{ + Frame label="Sony Bravia" { + Selection item=Bravia_IR mappings=[Channel-Up="Channel Up",Channel-Down="Channel Down",Left="Left"] + Switch item=Bravia_Power + Slider item=Bravia_Volume + Switch item=Bravia_AudioMute + Selection item=Bravia_Channel mappings=[4.1="ABC(1)", 5.1="NBC(1)", 5.2="NBC(2)", 13="CBS", 50.1="WRAL(1)", 50.2="WRAL(2)"] + Text item=Bravia_TripletChannel + Selection item=Bravia_InputSource mappings=[atsct="ATSCT", dvbt="DVBT", dvbc="DVBC", dvbs="DVBS", isdbt="ISDBT", isdbbs="ISDBBS", isdbcs="ISDBCS", antenna="Antenna", cable="Cable", isdbgt="ISDBGT"] + Selection item=Bravia_Input mappings=[TV="TV", HDMI1="HDMI1", HDMI2="HDMI2"] + Switch item=Bravia_PictureMute + Switch item=Bravia_TogglePictureMute mappings=[ON="Toggle"] + Switch item=Bravia_Pip + Switch item=Bravia_TogglePip mappings=[ON="Toggle"] + Switch item=Bravia_TogglePipPosition mappings=[ON="Toggle"] + } +} +``` diff --git a/bundles/org.openhab.binding.sony/README.md b/bundles/org.openhab.binding.sony/README.md new file mode 100644 index 0000000000000..594507902a883 --- /dev/null +++ b/bundles/org.openhab.binding.sony/README.md @@ -0,0 +1,273 @@ +# Sony Binding + +This binding is for Sony IP based product line including TVs, BDVs, AVRs, Blurays, Soundbars and Wireless Speakers. + +## Supported Things + +The following are the services that are available from different Sony devices. +Please note they are not exclusive of each other (many services are offered on a single device and offer different capabilities). +Feel free to mix and match as you see fit. + +### Scalar (also known as the REST API) + +The Scalar API is Sony's next generation API for discovery and control of the device. +This service has been implemented in most of the Sony products and has the same (and more) capabilities of all the other services combined. +If your device supports a Scalar thing, you should probably use it versus any of the other services. +The only downside is that it's a bit 'heavier' (depending on the device - will likely issue more calls) and is a bit more complicated to use (many, many channels are produced). + +This service dynamically generates the channels based on the device. + +For specifics - see [Scalar](README-SCALAR.md) + +### Simple IP + +The Simple IP protocol is a simplified version of IRCC and appears to be only supported on some models of Bravia TVs. +You must enable "Simple IP Control" on the devices (generally under `Settings->Network->Home Network->IP Control->Simple IP Control`) but once enabled - does not need any authentication. +The Simple IP control provides direct access to commonly used functions (channels, inputs, volume, etc) and provides full two-way communications (as things change on the device, openHAB will be notified immediately). + +For specifics - see [Simple IP](README-SimpleIp.md) + +### IRCC + +Many Sony products (TVs, AV systems, disc players) provided an IRCC service that provides minimal control to the device and some minimal feedback (via polling) depending on the version. +From my research, their appears to be 5 versions of this service: + +1. Not Specified - implemented on TVs and provides ONLY a command interface (i.e. sending of commands). +No feedback from the device is possible. +No status is available. +2. 1.0 - ??? +3. 1.1 - ??? +4. 1.2 - ??? +5. 1.3 - implemented on blurays. + +Provides a command interface, text field entry and status feedback (including disc information). +The status feedback is provided via polling of the device. + +Please note that the IRCC service is fully undocumented and much of the work that has gone into this service is based on observations. + +If you have a device that is reporting one of the "???" versions above, please post on the forum and I can give you directions on how we can document (and fix any issues) with those versions. + +Please note that Sony has begun transitioning many of their products over to the Scalar API and the latest firmware updates have begun to disable this service. + +For specifics - see [IRCC](README-IRCC.md) + +### DIAL + +The DIAL (DIscovery And Launch) allows you to discover the various applications available on the device and manage those applications (mainly to start or stop them). +This will apply to many of the smart tvs and bluray devices. +Generally you need to authenticate with IRCC before being able to use DIAL. +A channel will be created for each application (at startup only) and you can send ON to that channel to start the application and OFF to exit back to the main menu. + +For specifics - see [DIAL](README-README-DIAL.md.md) + +## Bluray Players + +Please note that somy Bluray players have only a limited, partial implementation of SCALAR. If you have a bluray player and scalar seems limited, you should try the DIAL/IRCC services as well. + +## Application status + +Sony has 'broken' the API that determines which application is currently running regardless if you use DIAL or Scalar services. +The API that determines whether an application is currently running ALWAYS returns 'stopped' (regardless if it's running or not). +Because of that - you cannnot rely on the application status and there is NO CURRENT WAY to determine if any application is running. + +Both DIAL/Scalar will continue to check the status in case Sony fixes this in some later date - but as of this writing - there is NO WAY to determine application status. + +## Device setup + +To enable automation of your device may require changes on the device. +This section mainly applies to TVs as the other devices generally are setup correctly. +Unfortunately the location of the settings generally depend on the device and the firmware that is installed. +I'll mention the most common area for each below but do remember that it may differ on your device. + +### Turn the device ON!!! + +When a sony device is off, there are a number of 'things' that get turned off as well. +You best action would be to turn the device ON when you are trying to set it up and bring it online for the first time. + +1. IRCC/Scalar on Blurays will not be auto discovered if the device is off (however DIAL will be discovered). +Both these services are turned off when the device is turned off. +2. Audio service on certain devices will either be turned off or limited in scope. + +If the audio service is off, you will either see no audio channels (volume, etc) or will be missing audio channels (like headphone volume for Bravias) + +### Wireless Interface + +If you are using the wireless interface on the device, you will *likely* lose the ability to power on the device with any of the services. +Most sony devices will power down the wireless port when turning off or going into standby - making communication to that device impossible (and thus trying to power on the device impossible). +As of the when this was written, there is no known way to change this behaviour through device options or setup. + + +### Wake on LAN + +To enable the device to wakeup based on network activity (WOL), go to `Settings->Network->Remote Start` and set to "ON". +This setting will cause the device to use more power (as it has to keep the ethernet port on always) but will generally allow you to turn on the device at any time. + +Note: this will **likely** not work if your device is connected wirelessly and generally only affects physical ethernet ports. + +### Enabling Remote Device Control + +To enable openHAB to control your device, you'll need to set the device to allow remote control. +Go to `Settings->Network->Home network setup->Renderer->Render Function` and set it to "Enabled". + +### Setting up the Authentication Mode + +There are three major ways to authenticate to a Sony Device: + +1. None - No authentication is needed (and openHAB should simply connect and work) +2. Normal - when openHAB registers with a device, a code is displayed on the device that needs to be entered into openHAB +3. Preshared - a predetermined key that is entered into openHAB + +You can select your authentication mode by going to `Settings->Network->Home network setup->IP Control->Authentication` and selecting your mode. +I highly recommend the use of "Normal" mode. + +Please note that their is a rare fourth option - some AVRs need to be put into a pairing mode prior to openHAB authentication. +This pairing mode acts similar to the "Normal" mode in that a code will be displayed on the AVR screen to be entered into openHAB. + +Also note that generally AVRs/SoundBars/Wireless speakers need no authentication at all and will automatically come online. + +See the authentication section below to understand how to use authenticate openHAB to the device. + +## Discovery + +This binding does attempt to discover Sony devices via UPNP. +Although this binding attempts to wake Sony devices (via wake on lan), some devices do not support WOL nor broadcast when they are turned off or sleeping. +If your devices does not automatically discovered, please turn the device on first and try again. +You may also need to turn on broadcasting via `Settings->Network->Remote Start` (on) - this setting has a side effect of turning on UPNP discovery. + +### Enabling/Disabling services + +By default, only the scalar service is enabled for discovery. +You can change the defaults by setting the following in the `conf/services/runtime.cfg` file: + +``` +discovery.sony-simpleip:background=false +discovery.sony-dial:background=false +discovery.sony-ircc:background=false +discovery.sony-scalar:background=true +``` + +## Authentication + +#### Normal Key + +A code request will request a code from the device and the device will respond by displaying new code on the screen. +Write this number down and then update the binding configuration with this code (FYI - you only have a limited time to do this - usually 30 or 60 seconds before that code expires). +Once you update the access code in the configuration, the binding will restart and a success message should appear on the device. + +Specifically you should: + +1. Update the "accessCode" configuration with the value "RQST". +The binding will then reload and send a request to the device. +2. The device will display a new code on the screen (and a countdown to expiration). +3. Update the "accessCode" configuration with the value shown on the screen. +The binding will then reload and ask the device to authorize with that code. +4. If successful, the device will show a success message and the binding should go online. +5. If unsuccessful, the code may have expired - start back at step 1. + +If the device was auto-discovered, the "RQST" will automatically be entered once you approve the device (then you have 30-60 seconds to enter the code displayed on the screen in the PaperUI `Configuration->Things->the device->configuration->Access Code`). +If the code expired before you had a chance, simply double click on the "RQST" and press the OK button - that will force the binding to request a new code (and alternative if that doesn't work is to switch it to "1111" and then back to "RQST"). + +If you are manually setting up the configuration, saving the file will trigger the above process. + +#### Pre Shared Key + +A pre-shared key is a key that you have set on the device prior to discovery (generally `Settings->Network Control->IP Control->Pre Shared Key`). +If you have set this on the device and then set the appropriate accessCode in the configuration, no additional authentication is required and the binding should be able to connect to the device. + +## Deactivation + +If you have used the Normal Key authentication and wish to deactivate the addon from the TV (to either cleanup when uninstalling or to simply restart an new authentication process): + +1. Go to `Settings->Network->Remote device settings->Deregister remote device` +2. Find and highlight the `openHAB (MediaRemote:00-11-22-33-44-55)` entry. +3. Select the `Deregister` button + +If you have used a preshared key - simply choose a new key (this may affect other devices however since a preshared key is global). + +## Thing Configuration + +### IP Address Configuration + +Any service can be setup by using just an IP address (or host name) - example: `192.168.1.104` in the deviceAddress field in configuration. +However, doing this will make certain assumptions about the device (ie path to services, port numbers, etc) that may not be correct for your device (should work for about 95% of the devices however). + +If you plan on setting your device up in a .things file, I recommend autodiscovering it first and copy the URL to your things file. + +There is one situation where you MUST use an IP address - if your device switches discovery ports commonly - then you must use a static IP address/host name. + +### Common Configuration Options + +The following is a list of common configuration options for all services + +| Name | Required | Default | Description | +| ------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------- | +| deviceAddress | Yes (1) | None | The path to the descriptor file or the IP address/host name of the device | +| deviceMacAddress | No (2) | eth0 | The device MAC address to use for wake on lan (WOL). | +| refresh | No (3) | 30 | The time, in seconds, to refresh some state from the device (only if the device supports retrieval of status) | +| checkStatusPolling | No | 30 | The time, in seconds, to check the device status device | +| retryPolling | No | 10 | The time, in seconds, to retry connecting to the device | + +1. See IP Address Configuration above +2. Only specify if the device support wake on lan (WOL) +3. Only specify if the device provides status information. + +Set to negative to disable (-1). + +```refresh``` is the time between checking the state of the device. +This will query the device for it's current state (example: volume level, current input, etc) and update all associated channels. +This is necessary if there are changes made by the device itself or if something else affects the device state outside of openHAB (such as using a remote). + +```checkStatusPolling``` is the time between checking if we still have a valid connection to the device. +If a connection attempt cannot be made, the thing will be updated to OFFLINE and will start a reconnection attempt (see ```retryPolling```). + +```retryPolling``` is the time between re-connection attempts. +If the thing goes OFFLINE (for any non-configuration error), reconnection attempts will be made. +Once the connection is successful, the thing will go ONLINE. + +### Ignore these configuration options + +The following 'configuration' options (as specified in the config XMLs) should **NEVER** be set as they are only set by the discovery process. + +| Name | Description | +| ------------------------- | --------------------------------------------- | +| discoveredMacAddress | Don't set this - set deviceMacAddress instead | +| discoveredCommandsMapFile | Don't set this - set commandMapFile instead | +| discoveredModelName | Don't set this - set modelName instead | + +## Advanced Users Only + +The following information is for more advanced users... + +### Low power devices (PIs, etc) + +This addon will try to only query information for the device to fulfill the information for channels you have linked. +However, if you've linked a great deal of channels (causing alot of requests to the device) and are running openHAB on a low power device - the polling time should be adjusted upwards to reduce the load on the PI. + +### Separating the sony logging into its own file + +To separate all the sony logging information into a separate file, please edit the file `userdata/etc/log4j2.xml` as follows: + +1. Add an logger appender definition (including the log file name) +2. Add a logger definition referencing the appender defined in step 1 + +Example for logging all `INFO` logs into a separate file `sony.log` under the standard log folder: + +``` + +... + + + + + + + + + + +... + + + + +``` diff --git a/bundles/org.openhab.binding.sony/pom.xml b/bundles/org.openhab.binding.sony/pom.xml new file mode 100644 index 0000000000000..35fa3dbdfbdc1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/pom.xml @@ -0,0 +1,55 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.3.0-SNAPSHOT + + + org.openhab.binding.sony + + openHAB Add-ons :: Bundles :: Sony Binding + + + + org.openhab.addons.bundles + org.openhab.transform.map + ${project.version} + provided + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + add-source + + generate-sources + + + src/3rdparty/java + + + + + + + com.mycila + license-maven-plugin + +
${project.basedir}/header.txt
+
+
+
+
+ +
diff --git a/bundles/org.openhab.binding.sony/src/3rdparty/java/org/glassfish/jersey/filter/LoggingFilter.java b/bundles/org.openhab.binding.sony/src/3rdparty/java/org/glassfish/jersey/filter/LoggingFilter.java new file mode 100644 index 0000000000000..25de021ee69f5 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/3rdparty/java/org/glassfish/jersey/filter/LoggingFilter.java @@ -0,0 +1,360 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2011-2015 Oracle and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * http://glassfish.java.net/public/CDDL+GPL_1_1.html + * or packager/legal/LICENSE.txt. See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at packager/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * Oracle designates this particular file as subject to the "Classpath" + * exception as provided by Oracle in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.glassfish.jersey.filter; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Logger; + +import javax.annotation.Priority; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.container.PreMatching; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.WriterInterceptor; +import javax.ws.rs.ext.WriterInterceptorContext; + +/** + * Universal logging filter. + *

+ * Can be used on client or server side. Has the highest priority. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + * @author Martin Matula + */ +@PreMatching +@Priority(Integer.MIN_VALUE) +public final class LoggingFilter implements ContainerRequestFilter, ClientRequestFilter, ContainerResponseFilter, + ClientResponseFilter, WriterInterceptor { + + public static final Charset UTF8 = Charset.forName("UTF-8"); + + private static final Logger LOGGER = Logger.getLogger(LoggingFilter.class.getName()); + private static final String NOTIFICATION_PREFIX = "* "; + private static final String REQUEST_PREFIX = "> "; + private static final String RESPONSE_PREFIX = "< "; + private static final String ENTITY_LOGGER_PROPERTY = LoggingFilter.class.getName() + ".entityLogger"; + private static final String LOGGING_ID_PROPERTY = LoggingFilter.class.getName() + ".id"; + + private static final Comparator>> COMPARATOR = new Comparator>>() { + + @Override + public int compare(final Map.Entry> o1, final Map.Entry> o2) { + return o1.getKey().compareToIgnoreCase(o2.getKey()); + } + }; + + private static final int DEFAULT_MAX_ENTITY_SIZE = 8 * 1024; + + // + private final Logger logger; + private final AtomicLong _id = new AtomicLong(0); + private final boolean printEntity; + private final int maxEntitySize; + + /** + * Create a logging filter logging the request and response to a default JDK + * logger, named as the fully qualified class name of this class. Entity + * logging is turned off by default. + */ + public LoggingFilter() { + this(LOGGER, false); + } + + /** + * Create a logging filter with custom logger and custom settings of entity + * logging. + * + * @param logger the logger to log requests and responses. + * @param printEntity if true, entity will be logged as well up to the default maxEntitySize, which is 8KB + */ + public LoggingFilter(final Logger logger, final boolean printEntity) { + this.logger = logger; + this.printEntity = printEntity; + this.maxEntitySize = DEFAULT_MAX_ENTITY_SIZE; + } + + /** + * Creates a logging filter with custom logger and entity logging turned on, but potentially limiting the size + * of entity to be buffered and logged. + * + * @param logger the logger to log requests and responses. + * @param maxEntitySize maximum number of entity bytes to be logged (and buffered) - if the entity is larger, + * logging filter will print (and buffer in memory) only the specified number of bytes + * and print "...more..." string at the end. Negative values are interpreted as zero. + */ + public LoggingFilter(final Logger logger, final int maxEntitySize) { + this.logger = logger; + this.printEntity = true; + this.maxEntitySize = Math.max(0, maxEntitySize); + } + + private void log(final StringBuilder b) { + if (logger != null) { + logger.info(b.toString()); + } + } + + private StringBuilder prefixId(final StringBuilder b, final long id) { + b.append(Long.toString(id)).append(" "); + return b; + } + + private void printRequestLine(final StringBuilder b, final String note, final long id, final String method, + final URI uri) { + prefixId(b, id).append(NOTIFICATION_PREFIX).append(note).append(" on thread ") + .append(Thread.currentThread().getName()).append("\n"); + prefixId(b, id).append(REQUEST_PREFIX).append(method).append(" ").append(uri.toASCIIString()).append("\n"); + } + + private void printResponseLine(final StringBuilder b, final String note, final long id, final int status) { + prefixId(b, id).append(NOTIFICATION_PREFIX).append(note).append(" on thread ") + .append(Thread.currentThread().getName()).append("\n"); + prefixId(b, id).append(RESPONSE_PREFIX).append(Integer.toString(status)).append("\n"); + } + + private void printPrefixedHeaders(final StringBuilder b, final long id, final String prefix, + final MultivaluedMap headers) { + for (final Map.Entry> headerEntry : getSortedHeaders(headers.entrySet())) { + final List val = headerEntry.getValue(); + final String header = headerEntry.getKey(); + + if (val.size() == 1) { + prefixId(b, id).append(prefix).append(header).append(": ").append(val.get(0)).append("\n"); + } else { + final StringBuilder sb = new StringBuilder(); + boolean add = false; + for (final Object s : val) { + if (add) { + sb.append(','); + } + add = true; + sb.append(s); + } + prefixId(b, id).append(prefix).append(header).append(": ").append(sb.toString()).append("\n"); + } + } + } + + private Set>> getSortedHeaders(final Set>> headers) { + final TreeSet>> sortedHeaders = new TreeSet>>( + COMPARATOR); + sortedHeaders.addAll(headers); + return sortedHeaders; + } + + private InputStream logInboundEntity(final StringBuilder b, InputStream stream, final Charset charset) + throws IOException { + if (!stream.markSupported()) { + stream = new BufferedInputStream(stream); + } + stream.mark(maxEntitySize + 1); + final byte[] entity = new byte[maxEntitySize + 1]; + final int entitySize = stream.read(entity); + try { + b.append(new String(entity, 0, Math.min(entitySize, maxEntitySize), charset)); + if (entitySize > maxEntitySize) { + b.append("...more..."); + } + } + catch (Exception ex) {}; + b.append('\n'); + stream.reset(); + return stream; + } + + @Override + public void filter(final ClientRequestContext context) throws IOException { + final long id = _id.incrementAndGet(); + context.setProperty(LOGGING_ID_PROPERTY, id); + + final StringBuilder b = new StringBuilder(); + + printRequestLine(b, "Sending client request", id, context.getMethod(), context.getUri()); + printPrefixedHeaders(b, id, REQUEST_PREFIX, context.getStringHeaders()); + + if (printEntity && context.hasEntity()) { + final OutputStream stream = new LoggingStream(b, context.getEntityStream()); + context.setEntityStream(stream); + context.setProperty(ENTITY_LOGGER_PROPERTY, stream); + // not calling log(b) here - it will be called by the interceptor + } else { + log(b); + } + } + + @Override + public void filter(final ClientRequestContext requestContext, final ClientResponseContext responseContext) + throws IOException { + final Object requestId = requestContext.getProperty(LOGGING_ID_PROPERTY); + final long id = requestId != null ? (Long) requestId : _id.incrementAndGet(); + + final StringBuilder b = new StringBuilder(); + + printResponseLine(b, "Client response received", id, responseContext.getStatus()); + printPrefixedHeaders(b, id, RESPONSE_PREFIX, responseContext.getHeaders()); + + if (printEntity && responseContext.hasEntity()) { + responseContext.setEntityStream( + logInboundEntity(b, responseContext.getEntityStream(), getCharset(responseContext.getMediaType()))); + } + + log(b); + } + + @Override + public void filter(final ContainerRequestContext context) throws IOException { + final long id = _id.incrementAndGet(); + context.setProperty(LOGGING_ID_PROPERTY, id); + + final StringBuilder b = new StringBuilder(); + + printRequestLine(b, "Server has received a request", id, context.getMethod(), + context.getUriInfo().getRequestUri()); + printPrefixedHeaders(b, id, REQUEST_PREFIX, context.getHeaders()); + + if (printEntity && context.hasEntity()) { + context.setEntityStream(logInboundEntity(b, context.getEntityStream(), getCharset(context.getMediaType()))); + } + + log(b); + } + + @Override + public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext) + throws IOException { + final Object requestId = requestContext.getProperty(LOGGING_ID_PROPERTY); + final long id = requestId != null ? (Long) requestId : _id.incrementAndGet(); + + final StringBuilder b = new StringBuilder(); + + printResponseLine(b, "Server responded with a response", id, responseContext.getStatus()); + printPrefixedHeaders(b, id, RESPONSE_PREFIX, responseContext.getStringHeaders()); + + if (printEntity && responseContext.hasEntity()) { + final OutputStream stream = new LoggingStream(b, responseContext.getEntityStream()); + responseContext.setEntityStream(stream); + requestContext.setProperty(ENTITY_LOGGER_PROPERTY, stream); + // not calling log(b) here - it will be called by the interceptor + } else { + log(b); + } + } + + @Override + public void aroundWriteTo(final WriterInterceptorContext writerInterceptorContext) + throws IOException, WebApplicationException { + final LoggingStream stream = (LoggingStream) writerInterceptorContext.getProperty(ENTITY_LOGGER_PROPERTY); + writerInterceptorContext.proceed(); + if (stream != null) { + log(stream.getStringBuilder(getCharset(writerInterceptorContext.getMediaType()))); + } + } + + private class LoggingStream extends FilterOutputStream { + + private final StringBuilder b; + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + LoggingStream(final StringBuilder b, final OutputStream inner) { + super(inner); + + this.b = b; + } + + StringBuilder getStringBuilder(final Charset charset) { + // write entity to the builder + final byte[] entity = baos.toByteArray(); + + b.append(new String(entity, 0, Math.min(entity.length, maxEntitySize), charset)); + if (entity.length > maxEntitySize) { + b.append("...more..."); + } + b.append('\n'); + + return b; + } + + @Override + public void write(final int i) throws IOException { + if (baos.size() <= maxEntitySize) { + baos.write(i); + } + out.write(i); + } + } + + /** + * Get the character set from a media type. + *

+ * The character set is obtained from the media type parameter "charset". + * If the parameter is not present the {@link #UTF8} charset is utilized. + * + * @param m the media type. + * @return the character set. + */ + public static Charset getCharset(MediaType m) { + final String name = (m == null) ? "" : m.getParameters().get(MediaType.CHARSET_PARAMETER); + try { + if(name != null) { + return Charset.forName(name.toUpperCase(Locale.ROOT)); + } + } catch (Exception ex ) { + } + return UTF8; + } + +} diff --git a/bundles/org.openhab.binding.sony/src/main/feature/feature.xml b/bundles/org.openhab.binding.sony/src/main/feature/feature.xml new file mode 100644 index 0000000000000..e39f29f1c8c24 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + + + openhab-runtime-base + openhab-transport-upnp + openhab-transformation-map + mvn:org.openhab.addons.bundles/org.openhab.binding.sony/${project.version} + + diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AbstractConfig.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AbstractConfig.java new file mode 100644 index 0000000000000..5fa3bdb3f0033 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AbstractConfig.java @@ -0,0 +1,234 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Base Configuration class for all configs in the sony system. This class defines the common configuration for each + * handler + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class AbstractConfig { + /** The device address (ipAddress for simpleIP, Full URL for others) */ + private @Nullable String deviceAddress; + + /** The device mac address. */ + private @Nullable String deviceMacAddress; + + /** The refresh time in seconds (null for default, < 1 to disable) */ + private @Nullable Integer refresh; + + /** The retry polling in seconds (null for default, < 1 to disable) */ + private @Nullable Integer retryPolling; + + /** The check status polling in seconds (null for default, < 1 to disable) */ + private @Nullable Integer checkStatusPolling; + + // ---- the following properties are not part of the config.xml (and are properties) ---- + + /** The mac address that was discovered */ + private @Nullable String discoveredMacAddress; + + /** + * Constructs (and returns) a URL represented by the {@link #deviceAddress} + * + * @return the non-null URL + * @throws MalformedURLException if the deviceURL was an improper URL (or null/empty) + */ + public URL getDeviceUrl() throws MalformedURLException { + if (SonyUtil.isEmpty(deviceAddress)) { + throw new MalformedURLException("deviceAddress was blank"); + } + return new URL(deviceAddress); + } + + /** + * Returns the IP address part of the device address or null if malformed, empty or null + * + * @return a possibly null, possibly empty IP Address + */ + public @Nullable String getDeviceIpAddress() { + try { + return getDeviceUrl().getHost(); + } catch (final MalformedURLException e) { + // check if deviceAddress is just IP/host + try { + // add dummy protocol + return new URI("my://" + deviceAddress).getHost(); + } catch (URISyntaxException ex) { + return null; + } + } + } + + /** + * Returns the device address + * + * @return the possibly null, possibly empty device address + */ + public @Nullable String getDeviceAddress() { + return deviceAddress; + } + + /** + * Sets the device address + * + * @param deviceAddress the possibly null, possibly empty device address + */ + public void setDeviceAddress(final String deviceAddress) { + this.deviceAddress = deviceAddress; + } + + /** + * Gets the device mac address. + * + * @return the device mac address + */ + public @Nullable String getDeviceMacAddress() { + return SonyUtil.defaultIfEmpty(deviceMacAddress, discoveredMacAddress); + } + + /** + * Sets the device mac address. + * + * @param deviceMacAddress the new device mac address + */ + public void setDeviceMacAddress(final @Nullable String deviceMacAddress) { + this.deviceMacAddress = deviceMacAddress; + } + + /** + * Sets the discovered mac address. + * + * @param discoveredMacAddress the device mac address + */ + public void setDiscoveredMacAddress(final @Nullable String discoveredMacAddress) { + this.discoveredMacAddress = discoveredMacAddress; + } + + /** + * Checks if is wol. + * + * @return true, if is wol + */ + public boolean isWOL() { + final @Nullable String deviceMacAddress = getDeviceMacAddress(); + return !(deviceMacAddress != null && !deviceMacAddress.isBlank()); + } + + /** + * Returns the refresh interval (-1/null to disable) + * + * @return a possibly null refresh interval + */ + public @Nullable Integer getRefresh() { + return refresh; + } + + /** + * Sets the refresh interval + * + * @param refresh the possibly null refresh interval + */ + public void setRefresh(final Integer refresh) { + this.refresh = refresh; + } + + /** + * Returns the retry connection polling interval (-1/null to disable) + * + * @return a possibly null polling interval + */ + public @Nullable Integer getRetryPolling() { + return retryPolling; + } + + /** + * Sets the polling interval + * + * @param retryPolling the possibly null polling interval + */ + public void setRetryPolling(final Integer retryPolling) { + this.retryPolling = retryPolling; + } + + /** + * Returns the check status polling interval (-1/null to disable) + * + * @return a possibly null check status interval + */ + public @Nullable Integer getCheckStatusPolling() { + return checkStatusPolling; + } + + /** + * Sets the check status interval + * + * @param checkStatusPolling the possibly null check status interval + */ + public void setCheckStatusPolling(final Integer checkStatusPolling) { + this.checkStatusPolling = checkStatusPolling; + } + + /** + * Returns the configuration as a map of properties + * + * @return a non-null, non-empty map + */ + public Map asProperties() { + final Map props = new HashMap<>(); + props.put("deviceAddress", Objects.requireNonNull(SonyUtil.defaultIfEmpty(deviceAddress, ""))); + props.put("discoveredMacAddress", Objects.requireNonNull(SonyUtil.defaultIfEmpty(discoveredMacAddress, ""))); + conditionallyAddProperty(props, "deviceMacAddress", deviceMacAddress); + conditionallyAddProperty(props, "refresh", refresh); + conditionallyAddProperty(props, "retryPolling", retryPolling); + conditionallyAddProperty(props, "checkStatusPolling", checkStatusPolling); + + return props; + } + + /** + * Conditionally adds a property to the property map if the property is not null (or empty if a string) + * + * @param props a non-null, possibly empty property map + * @param propName a non-null, non-empty property name + * @param propValue a possibly null, possibly empty (if string) property value + */ + protected void conditionallyAddProperty(final Map props, final String propName, + final @Nullable Object propValue) { + Objects.requireNonNull(props, "props cannot be null"); + SonyUtil.validateNotEmpty(propName, "propName cannot be empty"); + + if (propValue == null) { + return; + } + + if (propValue instanceof String && SonyUtil.isEmpty((String) propValue)) { + return; + } + + props.put(propName, propValue); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AbstractDiscoveryParticipant.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AbstractDiscoveryParticipant.java new file mode 100644 index 0000000000000..707a03eaae7f6 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AbstractDiscoveryParticipant.java @@ -0,0 +1,287 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.model.meta.DeviceDetails; +import org.jupnp.model.meta.RemoteDevice; +import org.jupnp.model.meta.RemoteDeviceIdentity; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.providers.SonyDefinitionProvider; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.type.ThingType; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Modified; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This abstract class provides all the base functionality for discovery participants + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractDiscoveryParticipant { + + /** The logger */ + protected Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** The service this discovery participant is for */ + private final String service; + + /** The sony definition provider */ + private final SonyDefinitionProvider sonyDefinitionProvider; + + /** Whether discovery is enabled */ + private final AtomicBoolean discoveryEnabled = new AtomicBoolean(); + + /** + * Construct the participant from the given service + * + * @param service a non-null, non-empty service + * @param sonyDefinitionProvider a non-null sony definition provider + */ + protected AbstractDiscoveryParticipant(final String service, final SonyDefinitionProvider sonyDefinitionProvider) { + SonyUtil.validateNotEmpty(service, "service cannot be empty"); + Objects.requireNonNull(sonyDefinitionProvider, "sonyDefinitionProvider cannot be null"); + + this.service = service; + this.sonyDefinitionProvider = sonyDefinitionProvider; + } + + /** + * Returns a list of thing type uids supported by this discovery implementation + * + * @return a non-null, never empty list of supported thing type uids + */ + public Set getSupportedThingTypeUIDs() { + final Set uids = new HashSet(); + + // Add the generic one + uids.add(new ThingTypeUID(SonyBindingConstants.BINDING_ID, service)); + + // Add any specific ones + for (final ThingType tt : sonyDefinitionProvider.getThingTypes(null)) { + uids.add(tt.getUID()); + } + return uids; + } + + /** + * Determines if the remote device is a sony device (based on it's manufacturer) + * + * @param device a non-null device + * @return true if it's a sony device, false otherwise + */ + protected static boolean isSonyDevice(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + + final DeviceDetails details = device.getDetails(); + final String manufacturer = details == null || details.getManufacturerDetails() == null ? null + : details.getManufacturerDetails().getManufacturer(); + return details != null && manufacturer != null + && manufacturer.toLowerCase().contains(SonyBindingConstants.SONY_REMOTEDEVICE_ID.toLowerCase()); + } + + /** + * Get model name for the remove device + * + * @param device a non-null device + * @return the model name or null if none found + */ + protected static @Nullable String getModelName(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + + final DeviceDetails details = device.getDetails(); + if (details == null) { + return null; + } + + final String modelName = details.getModelDetails() == null ? null : details.getModelDetails().getModelName(); + if (modelName != null && !modelName.isEmpty() && SonyUtil.isValidModelName(modelName)) { + return modelName; + } + + final String friendlyName = details.getFriendlyName(); + if (friendlyName != null && !friendlyName.isEmpty() && SonyUtil.isValidModelName(friendlyName)) { + return friendlyName; + } + + return friendlyName != null && !friendlyName.isEmpty() ? friendlyName : modelName; + } + + /** + * Get model description for the remove device + * + * @param device a non-null device + * @return the model description or null if none found + */ + protected static @Nullable String getModelDescription(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + + final DeviceDetails details = device.getDetails(); + return details == null || details.getModelDetails() == null ? null + : details.getModelDetails().getModelDescription(); + } + + /** + * Get's the mac address of the remote device (or if the UID is a mac address) + * + * @param identity a non-null identity + * @param uid a non-null tyhing UID + * @return a valid mac address if found, null if not + */ + protected static @Nullable String getMacAddress(final RemoteDeviceIdentity identity, final ThingUID uid) { + Objects.requireNonNull(identity, "identity cannot be null"); + Objects.requireNonNull(uid, "uid cannot be null"); + + final String wolMacAddress = NetUtil.getMacAddress(identity.getWakeOnLANBytes()); + if (NetUtil.isMacAddress(wolMacAddress)) { + return wolMacAddress; + } else if (NetUtil.isMacAddress(uid.getId())) { + return uid.getId(); + } + return null; + } + + /** + * Gets the label from the remote device + * + * @param device a non-null, non-empty device + * @param suffix a non-null, non-empty suffix + * @return the label for the device + */ + protected static String getLabel(final RemoteDevice device, final String suffix) { + Objects.requireNonNull(device, "device cannot be null"); + SonyUtil.validateNotEmpty(suffix, "suffix cannot be empty"); + + final String modelName = getModelName(device); + final String friendlyName = device.getDetails().getFriendlyName(); + + final StringBuilder sb = new StringBuilder(); + + if (!SonyUtil.isEmpty(friendlyName)) { + sb.append(friendlyName); + } else if (!SonyUtil.isEmpty(modelName)) { + sb.append(modelName); + } else { + sb.append(device.getDisplayString()); + } + + return sb.toString(); + } + + /** + * Determines if there is a scalar thing type defined for the device + * + * @param device the non-null device + * @return true if found, false otherwise + */ + protected boolean isScalarThingType(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + return getThingTypeUID(SonyBindingConstants.SCALAR_THING_TYPE_PREFIX, getModelName(device)) != null; + } + + /** + * Returns the thing type related to the model name. This will search the registry for a thing type that is + * specific to the model. If found, that thing type will be used. If not found, the generic scalar thing type will + * be used + * + * @param modelName a possibly null, possibly empty model name + * @return a ThingTypeUID if found, null if not + */ + protected @Nullable ThingTypeUID getThingTypeUID(final @Nullable String modelName) { + return getThingTypeUID(service, modelName); + } + + /** + * Returns the thing type related to the service/model name. This will search the registry for a thing type that is + * specific to the model. If found, that thing type will be used. If not found, the generic scalar thing type will + * be used + * + * @param service a non-null, non-empty service + * @param modelName a possibly null, possibly empty model name + * @return a ThingTypeUID if found, null if not + */ + private @Nullable ThingTypeUID getThingTypeUID(final String service, final @Nullable String modelName) { + SonyUtil.validateNotEmpty(service, "service cannot be empty"); + + if (modelName == null || modelName.isEmpty()) { + logger.debug("Emtpy model name!"); + return null; + } + + for (final ThingType tt : sonyDefinitionProvider.getThingTypes(Locale.getDefault())) { + if (SonyUtil.isModelMatch(tt.getUID(), service, modelName)) { + logger.debug("Using specific thing type for {}: {}", modelName, tt); + return tt.getUID(); + } + } + + logger.debug("No specific thing type found for {}", modelName); + return null; + } + + /** + * Determines if discovery is enabled or not + * + * @return true if enabled, false otherwise + */ + protected boolean isDiscoveryEnabled() { + return discoveryEnabled.get(); + } + + /** + * Abstract function to determine the default for enabling discovery + * + * @return true to enable by default, false otherwise + */ + protected abstract boolean getDiscoveryEnableDefault(); + + @Activate + protected void activate(final ComponentContext componentContext) { + activateOrModifyService(componentContext); + } + + @Modified + protected void modified(final ComponentContext componentContext) { + activateOrModifyService(componentContext); + } + + /** + * Helper method to determine if discovery is enabled via a configuration file + * + * @param componentContext a non-null component context + */ + private void activateOrModifyService(final ComponentContext componentContext) { + Objects.requireNonNull(componentContext, "componentContext cannot be null"); + + final Dictionary properties = componentContext.getProperties(); + final String discoveryEnabled = (String) properties.get("background"); + if (discoveryEnabled == null || discoveryEnabled.isEmpty()) { + this.discoveryEnabled.set(getDiscoveryEnableDefault()); + } else { + this.discoveryEnabled.set(Boolean.valueOf(discoveryEnabled)); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AbstractThingHandler.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AbstractThingHandler.java new file mode 100644 index 0000000000000..4afacb29b272e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AbstractThingHandler.java @@ -0,0 +1,530 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import static java.lang.Math.pow; +import static java.lang.Math.round; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.URL; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the base thing handler for all sony things. This base handler provides common services to all sony things + * like polling, connection retry and status checking. + * + * @author Tim Roberts - Initial contribution + * @param The configuration type for the handler + */ +@NonNullByDefault +public abstract class AbstractThingHandler extends BaseThingHandler { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(AbstractThingHandler.class); + + /** The configuration class type */ + private final Class configType; + + /** The refresh state event - will only be created when we are connected. */ + private final AtomicReference<@Nullable Future> refreshState = new AtomicReference<>(null); + + /** The check status event - will only be created when we are connected. */ + private final AtomicReference<@Nullable Future> checkStatus = new AtomicReference<>(null); + + /** The retry connection event - will only be created when we are disconnected. */ + private final AtomicReference<@Nullable Future> retryConnection = new AtomicReference<>(null); + + /** The delay for trying to reconnect after command has been sent to offline thing */ + private static final int AUTO_RECONNECT_INTERVAL = 2; + private static final double AUTO_RECONNECT_MULTIPLIER = 2.0; + private static final int AUTO_RECONNECT_MAX_ATTEMPTS = 6; + private final AtomicInteger autoRetryCount = new AtomicInteger(0); + + /** The queue used to cache commands until online */ + private final Queue commandQueue = new ConcurrentLinkedQueue<>(); + + /** constants to handle power on/off commands */ + protected enum PowerCommand { + ON, + OFF, + NON + }; + + /** + * Constructs the handler from the specified {@link Thing} + * + * @param thing the non-null thing + * @param configType the non-null configuration type + */ + public AbstractThingHandler(final Thing thing, final Class configType) { + super(thing); + + Objects.requireNonNull(thing, "thing cannot be null"); + Objects.requireNonNull(configType, "configType cannot be null"); + + this.configType = configType; + } + + /** + * Called when the thing handler should attempt a connection. Note that this method is reentrant. The implementation + * of this method MUST call {@link #updateStatus(ThingStatus, ThingStatusDetail, String)} prior to exiting (either + * with ONLINE or OFFLINE) to allow this abstract base to process the results of the connection attempt properly. + */ + protected abstract void connect(); + + /** + * Called when the thing handler should attempt to refresh state. Note that this method is reentrant. + * + * @param initial true if this is the initial refresh state after going online, false otherwise + */ + protected abstract void refreshState(boolean initial); + + /** + * Returns the configuration cast to the specific type + * + * @return a non-null configuration + */ + protected C getSonyConfig() { + return getConfigAs(configType); + } + + @Override + public void initialize() { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Initializing ..."); + logger.info("Thing initialization is called, trying to connect..."); + SonyUtil.cancel(retryConnection.getAndSet(this.scheduler.submit(this::doConnect))); + } + + /** + * Helper method to start a connection attempt + */ + private void doConnect() { + if (isReachable()) { + // try to connect and set status only if reachable + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Initializing ..."); + connect(); + } else { + logger.debug("Device with ip/host {} - not reachable. Giving-up connection attempt", getDeviceIpAddress()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Error connecting to device: not reachable"); + } + } + + /** + * Helper method to determine if we should auto reconnect on a command + * + * @return true if auto reconnect is supported, false otherwise + */ + private boolean isAutoReconnect() { + final @Nullable Integer retryPolling = getSonyConfig().getRetryPolling(); + return (retryPolling == null || retryPolling <= 0); + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + final ThingStatus status = getThing().getStatus(); + // if no retry connection is set, start reconnect on command reception + final boolean autoReconnect = isAutoReconnect(); + logger.debug("Handle command: {} {} {} {}", status, autoReconnect, channelUID, command); + // this implies issuing WOL in case of power on command + final PowerCommand powerCommand = handlePotentialPowerOnCommand(channelUID, command); + if (status == ThingStatus.OFFLINE) { + // handle power on commands if thing is offline by using WOL + if (powerCommand == PowerCommand.ON) { + logger.info("Received power on command when thing is offline - trying to turn on thing via WOL"); + } + if (autoReconnect || powerCommand == PowerCommand.ON) { + logger.debug("AutoReconnect on - scheduling reconnect"); + // do no schedule auto retry if already active + if (autoRetryCount.get() == 0) { + logger.debug("Schedule auto reconnect"); + autoRetryCount.set(1); + scheduleReconnect(AUTO_RECONNECT_INTERVAL); + } + } + } + if (status != ThingStatus.ONLINE && (autoReconnect || autoRetryCount.get() > 0)) { + // do not cache power commands as this is likely unwanted in case thing is offline but might happen + // when using power toggle command to switch on device with power item being in an inconsistent 'ON' + // state + if (powerCommand == PowerCommand.NON) { + logger.debug("Caching: {} {}", channelUID, command); + commandQueue.add(new CachedCommand(channelUID, command)); + } + } else if (status == ThingStatus.ONLINE) { + logger.debug("doHandleCommand"); + doHandleCommand(channelUID, command); + } + } + + /** + * Method to check if command is a power on command. + * If applicable, power on will be executed by Wake-on-Lan. + * + * @param channelUID a non-null channel UID + * @param command a non-null command + * + * @return true if command is power on, otherwise false + */ + protected abstract PowerCommand handlePotentialPowerOnCommand(final ChannelUID channelUID, final Command command); + + /** + * This will execute any cached commands + */ + private void doCachedCommands() { + logger.debug("Executing any cached commands"); + final long now = System.currentTimeMillis(); + while (true) { + final CachedCommand cmd = commandQueue.poll(); + if (cmd == null) { + return; + } + + if (now - cmd.timestamp > SonyBindingConstants.THING_CACHECOMMAND_TIMEOUTMS) { + logger.debug("Command expired waiting on a connect: {} {}", cmd.channelUID, cmd.command); + } else { + logger.debug("Executing cached command: {} {}", cmd.channelUID, cmd.command); + doHandleCommand(cmd.channelUID, cmd.command); + } + } + } + + /** + * Internal method to handle a command + * + * @param channelUID a non-null channel UID + * @param command a non-null command + */ + private void doHandleCommand(final ChannelUID channelUID, final Command command) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + if (command instanceof RefreshType) { + handleRefreshCommand(channelUID); + } else { + handleSetCommand(channelUID, command); + } + } + + /** + * Method that handles the {@link RefreshType} command specifically. + * + * @param channelUID a non-null channel UID + */ + protected abstract void handleRefreshCommand(final ChannelUID channelUID); + + /** + * Method that handles a non {@link RefreshType} command. + * + * @param channelUID a non-null channel UID + * @param command a non-null refresh command + */ + protected abstract void handleSetCommand(final ChannelUID channelUID, final Command command); + + @Override + protected void updateStatus(final ThingStatus status, final ThingStatusDetail statusDetail, + final @Nullable String description) { + super.updateStatus(status, statusDetail, description); + + if (status == ThingStatus.ONLINE) { + schedulePolling(); + scheduleCheckStatus(); + doCachedCommands(); + autoRetryCount.set(0); + } else if (status == ThingStatus.UNKNOWN) { + // probably in the process of reconnecting - ignore + logger.trace("Ignoring thing status of UNKNOWN"); + } else { + // status == Thing.Status.OFFLINE + SonyUtil.cancel(refreshState.getAndSet(null)); + SonyUtil.cancel(checkStatus.getAndSet(null)); + + // don't bother reconnecting - won't fix a configuration error + if (statusDetail != ThingStatusDetail.CONFIGURATION_ERROR) { + if (autoRetryCount.get() == 0) { + scheduleReconnect(); + } else { + // Try until maximum number of auto retries are reached. + // This might happen when the auto retry delay is too short for the device services to become online + if (autoRetryCount.getAndIncrement() <= AUTO_RECONNECT_MAX_ATTEMPTS) { + logger.debug("Schedule auto reconnect counter={}", autoRetryCount.get()); + scheduleReconnect((int) round( + AUTO_RECONNECT_INTERVAL * pow(AUTO_RECONNECT_MULTIPLIER, autoRetryCount.get()))); + } else { + // stop auto retry + autoRetryCount.set(0); + } + } + } + } + } + + /** + * Starts the polling process. The polling process will refresh the state of the sony device if the refresh time (in + * seconds) is greater than 0. This process will continue until cancelled. + */ + private void schedulePolling() { + final C config = getSonyConfig(); + final Integer refresh = config.getRefresh(); + + if (refresh != null && refresh > 0) { + logger.info("Starting state polling every {} seconds", refresh); + SonyUtil.cancel(refreshState.getAndSet( + this.scheduler.scheduleWithFixedDelay(new RefreshState(), refresh, refresh, TimeUnit.SECONDS))); + } else { + logger.info("Refresh not a positive number - polling has been disabled"); + } + } + + /** + * Tries to reconnect to the sony device. The results of the connection should call + * {@link #updateStatus(ThingStatus, ThingStatusDetail, String)} and if set to OFFLINE, this method will be called + * to schedule another connection attempt + * + * There is one warning here - if the retryPolling is set lower than how long + * it takes to connect, you can get in an infinite loop of the connect getting cancelled for the next retry. + */ + private void scheduleReconnect() { + final C config = getSonyConfig(); + final Integer retryPolling = config.getRetryPolling(); + scheduleReconnect(retryPolling); + } + + /** + * Tries to reconnect to the sony device. The results of the connection should call + * {@link #updateStatus(ThingStatus, ThingStatusDetail, String)} and if set to OFFLINE, this method will be called + * to schedule another connection attempt + * + * There is one warning here - if the retryPolling is set lower than how long + * it takes to connect, you can get in an infinite loop of the connect getting cancelled for the next retry. + * + * @param retryPolling the possibly null retry polling interval in seconds + * + */ + private void scheduleReconnect(@Nullable Integer retryPolling) { + if (retryPolling != null && retryPolling >= 0) { + SonyUtil.cancel(retryConnection.getAndSet(this.scheduler.schedule(() -> { + if (!SonyUtil.isInterrupted() && !isRemoved()) { + logger.debug("Do reconnect for {}", thing.getLabel()); + doConnect(); + } + }, retryPolling, TimeUnit.SECONDS))); + } else { + logger.info("Retry connection has been disabled via configuration setting"); + } + } + + /** + * Schedules a check status attempt by simply getting the configuration and calling + * {@link #scheduleCheckStatus(Integer, String, Integer)} + */ + private void scheduleCheckStatus() { + final C config = getSonyConfig(); + URL url; + + try { + url = getCheckStatusUrl(); + } catch (final MalformedURLException e) { + logger.debug("Cannot check status - URL is malformed: {}", e.getMessage()); + return; + } + + final String ipAddress = url.getHost(); + + final int urlPort = url.getPort(); + final int port = urlPort == -1 ? url.getDefaultPort() : urlPort; + + scheduleCheckStatus(config.getCheckStatusPolling(), ipAddress, port); + } + + /** + * Gets the URL to check the status for + * + * @return a non-null URL containing an ipaddress and possibly port + * @throws MalformedURLException if the url is malformed + */ + protected URL getCheckStatusUrl() throws MalformedURLException { + final C config = getSonyConfig(); + return config.getDeviceUrl(); + } + + /** + * Checks if device is reachable + * + * @return true if device is reachable, otherweise false + */ + private boolean isReachable() { + final C config = getSonyConfig(); + // check if device is reachable before trying to login + + final String iPAddress = config.getDeviceIpAddress(); + if (SonyUtil.isEmpty(iPAddress)) + return false; + try { + return InetAddress.getByName(iPAddress).isReachable(500); + } catch (IOException ex) { + return false; + } + } + + /** + * Gets the device IP to check if reachable + * + * @return a potentially null ip address or host name of device + */ + protected @Nullable String getDeviceIpAddress() { + final C config = getSonyConfig(); + return config.getDeviceIpAddress(); + } + + /** + * Schedules the check status for the given interval and IP Address/port. If the status is successful, another check + * status is schedule after checkStatusInterval seconds. If the connection was unsuccessful, the state is updated to + * OFFLINE (which will trigger a connection attempt) + * + * If any of the parameters are null (or checkStatusInterval is <= 0), no check status will be scheduled + * + * @param checkStatusInterval a possibly null checkStatus interval + * @param ipAddress a possibly null, possibly empty IP address to check + * @param port a possibly null port to check + */ + private void scheduleCheckStatus(final @Nullable Integer checkStatusInterval, final @Nullable String ipAddress, + final @Nullable Integer port) { + if (ipAddress != null && !ipAddress.isBlank() && port != null && checkStatusInterval != null + && checkStatusInterval > 0) { + SonyUtil.cancel(checkStatus.getAndSet(scheduler.schedule(() -> { + try { + if (!SonyUtil.isInterrupted() && !isRemoved()) { + try (Socket soc = new Socket()) { + soc.connect(new InetSocketAddress(ipAddress, port), 5000); + } + logger.debug("Checking connectivity to {}:{} - successful", ipAddress, port); + scheduleCheckStatus(checkStatusInterval, ipAddress, port); + } + } catch (final IOException ex) { + logger.debug("Checking connectivity to {}:{} - unsuccessful - going offline: {}", ipAddress, port, + ex.getMessage(), ex); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Could not connect to " + ipAddress + ":" + port); + } + }, checkStatusInterval, TimeUnit.SECONDS))); + } + } + + /** + * Helper method to determine if the thing is being removed (or is removed) + * + * @return true if removed, false otherwise + */ + protected boolean isRemoved() { + final ThingStatus status = getThing().getStatus(); + return status == ThingStatus.REMOVED || status == ThingStatus.REMOVING; + } + + @Override + public void dispose() { + super.dispose(); + logger.debug("Disposing {}", thing.getLabel()); + SonyUtil.cancel(refreshState.getAndSet(null)); + SonyUtil.cancel(retryConnection.getAndSet(null)); + SonyUtil.cancel(checkStatus.getAndSet(null)); + } + + /** + * This is a helper class to track a command that has been cached until the thing goes online. The channelUID, + * command and a timestamp will be recorded. + */ + private class CachedCommand { + /** When the cached command was created */ + private final long timestamp = System.currentTimeMillis(); + + /** The channel UID */ + private final ChannelUID channelUID; + + /** The command */ + private final Command command; + + /** + * Creates the cached command + * + * @param channelUID a non-null channel UID + * @param command a non-null command + */ + public CachedCommand(ChannelUID channelUID, Command command) { + this.channelUID = channelUID; + this.command = command; + } + } + + /** + * This helper class is used to manage refreshing of the state + */ + private class RefreshState implements Runnable { + + // boolean indicating if the refresh is the first refresh after going online + private boolean initial = true; + + @Override + public void run() { + // throw exceptions to prevent future tasks under these circumstances + if (isRemoved()) { + throw new IllegalStateException("Thing has been removed - ending state polling"); + } + if (SonyUtil.isInterrupted()) { + throw new IllegalStateException("State polling has been cancelled"); + } + + // catch the various runtime exceptions that may occur here (the biggest being ProcessingException) + // and handle it. + try { + if (thing.getStatus() == ThingStatus.ONLINE) { + refreshState(initial); + initial = false; + } else { + initial = true; + } + } catch (final Exception ex) { + final @Nullable String message = ex.getMessage(); + if (message != null && message.contains("Connection refused")) { + logger.info("Connection refused - device is probably turned off"); + } else { + logger.error("Uncaught exception (refreshstate) : {}", ex.getMessage(), ex); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AccessResult.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AccessResult.java new file mode 100644 index 0000000000000..e612c2b75f1a8 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/AccessResult.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.net.HttpResponse; + +/** + * This enum represents what type of action is needed when we first connect to the device + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class AccessResult { + /** OK - device either needs no pairing or we have already paird */ + public static final AccessResult OK = new AccessResult("ok", "OK"); + public static final AccessResult NEEDSPAIRING = new AccessResult("needspairing", "Device needs pairing"); + public static final AccessResult SERVICEMISSING = new AccessResult("servicemissing", "Service is missing"); + /** Device needs pairing but the display is off */ + public static final AccessResult DISPLAYOFF = new AccessResult("displayoff", + "Unable to request an access code - Display is turned off (must be on to see code)"); + /** Need to be in the home menu */ + public static final AccessResult HOMEMENU = new AccessResult("homemenu", + "Unable to request an access code - HOME menu not displayed on device. Please display the home menu and try again."); + /** Need to be in the home menu */ + public static final AccessResult PENDING = new AccessResult("pending", + "Access Code requested. Please update the Access Code with what is shown on the device screen."); + public static final AccessResult NOTACCEPTED = new AccessResult("notaccepted", + "Access code was not accepted - please either request a new one or verify number matches what's shown on the device."); + /** Some other error */ + public static final String OTHER = "other"; + + /** The actual code */ + private final String code; + + /** The actual message */ + private final String msg; + + /** + * Creates the result from the code/msg + * + * @param code the non-null, non-empty code + * @param msg the non-null, non-empty msg + */ + public AccessResult(final String code, final String msg) { + SonyUtil.validateNotEmpty(code, "code cannot be empty"); + SonyUtil.validateNotEmpty(msg, "msg cannot be empty"); + + this.code = code; + this.msg = msg; + } + + /** + * Constructs the result from the response + * + * @param resp the non-null response + */ + public AccessResult(final HttpResponse resp) { + Objects.requireNonNull(resp, "resp cannot be null"); + this.code = AccessResult.OTHER; + + final String content = resp.getContent(); + this.msg = resp.getHttpCode() + " - " + (SonyUtil.defaultIfEmpty(content, resp.getHttpReason())); + } + + /** + * Returns the related code + * + * @return a non-null, non-empty code + */ + public String getCode() { + return this.code; + } + + /** + * Returns the related message + * + * @return a non-null, non-empty message + */ + public String getMsg() { + return this.msg; + } + + @Override + public boolean equals(final @Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || !(obj instanceof AccessResult)) { + return false; + } + + final AccessResult other = (AccessResult) obj; + return code.equals(other.code); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/CheckResult.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/CheckResult.java new file mode 100644 index 0000000000000..f55140ca41c04 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/CheckResult.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This represents the result of an authorization check (extends {@link AccessResults} to provide more fine grained + * response of OK conditions) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class CheckResult extends AccessResult { + /** Authorization was fine and uses HEADER style of authorization */ + public static final CheckResult OK_HEADER = new CheckResult("okHeader", "OK"); + + /** Authorization was fine and uses COOKIE style of authorization */ + public static final CheckResult OK_COOKIE = new CheckResult("okCookie", "OK"); + + /** + * Constructs the check result from the code and message + * + * @param code a non-null, non-empty code + * @param msg a non-null, non-empty message + */ + public CheckResult(final String code, final String msg) { + super(code, msg); + } + + /** + * Constructst he check result from the access request + * + * @param res a non-null access request + */ + public CheckResult(final AccessResult res) { + super(res.getCode(), res.getMsg()); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ExpiringMap.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ExpiringMap.java new file mode 100644 index 0000000000000..f0981785eed0b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ExpiringMap.java @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This is a limited functional map (not using the full Map interface) that will allow entries to expire after a certain + * period of time. Once expired, a listener will be notified of the expiration + * + * @author Tim Roberts - Initial contribution + * + * @param the key type + * @param the value type + */ +@NonNullByDefault +public class ExpiringMap implements AutoCloseable { + + /** The lock used to control access to both internal maps */ + private final ReadWriteLock mapLock = new ReentrantReadWriteLock(); + + /** The internal map used to store key/values */ + private final Map internalMap = new HashMap<>(); + + /** The map of keys to their creation timestamps */ + private final Map timeStamps = new HashMap<>(); + + /** The expiring check job */ + private final @Nullable Future expireCheck; + + /** The array list of expire listeners */ + private final List> expireListeners = new CopyOnWriteArrayList<>(); + + /** + * Constructs the map from the parameters + * + * @param scheduler the possibly null scheduler (if null, nothing will expire) + * @param expireTime the expiration time + * @param timeUnit the non-null time unit of expireTime + */ + public ExpiringMap(final @Nullable ScheduledExecutorService scheduler, final int expireTime, + final TimeUnit timeUnit) { + Objects.requireNonNull(timeUnit, "timeUnit cannot be null"); + + if (scheduler == null) { + expireCheck = null; + } else { + expireCheck = scheduler.scheduleWithFixedDelay(() -> { + final long now = System.currentTimeMillis(); + final Lock writeLock = mapLock.writeLock(); + writeLock.lock(); + try { + timeStamps.entrySet().removeIf(e -> { + if (e.getValue() + expireTime <= now) { + final K key = e.getKey(); + final @Nullable V val = internalMap.remove(key); + // can't happen, but required to pass null safety checks + if (val != null) { + expireListeners.forEach(l -> l.expired(key, val)); + } + return true; + } + return false; + }); + } finally { + writeLock.unlock(); + } + }, expireTime, expireTime, timeUnit); + } + } + + /** + * Adds a listener for expiration notices + * + * @param listener a non-null listener + */ + public void addExpireListener(final ExpireListener listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + expireListeners.add(listener); + } + + /** + * Gets a value associated with the key + * + * @param key a non-null key + * @return the value associated with the key or null if not found + */ + public @Nullable V get(final @Nullable K key) { + Objects.requireNonNull(key, "key cannot be null"); + final Lock readLock = mapLock.readLock(); + readLock.lock(); + try { + return internalMap.get(key); + } finally { + readLock.unlock(); + } + } + + /** + * Puts a value associated with the key into the map + * + * @param key a non-null key + * @param value a non-null value + * @return the old key value if replaced (or null if nothing replaced) + */ + public @Nullable V put(final K key, final V value) { + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + final Lock writeLock = mapLock.writeLock(); + writeLock.lock(); + try { + timeStamps.put(key, System.currentTimeMillis()); + return internalMap.put(key, value); + } finally { + writeLock.unlock(); + } + } + + /** + * Removes the value associated with the key and returns it + * + * @param key the non-null key + * @return the value associated with the key or null if key not found + */ + public @Nullable V remove(final @Nullable K key) { + Objects.requireNonNull(key, "key cannot be null"); + final Lock writeLock = mapLock.writeLock(); + writeLock.lock(); + try { + timeStamps.remove(key); + return internalMap.remove(key); + } finally { + writeLock.unlock(); + } + } + + @Override + public void close() { + SonyUtil.cancel(expireCheck); + } + + /** + * This represents a functional interface to defined an expiration callback + * + * @author Tim Roberts - Initial contribution + * + * @param the key type + * @param the value type + */ + public interface ExpireListener { + void expired(K key, V value); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/LoginUnsuccessfulResponse.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/LoginUnsuccessfulResponse.java new file mode 100644 index 0000000000000..78e7152da17ac --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/LoginUnsuccessfulResponse.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingStatusDetail; + +/** + * This class represents a login response to a sony device if the login was unsuccessful + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class LoginUnsuccessfulResponse { + /** The specific thing status detail for the response */ + private final ThingStatusDetail thingStatusDetail; + + /** The message related to the response */ + private final String message; + + /** + * Constructs the unsuccessful response from the detail and message + * + * @param thingStatusDetail a non-null thing status detail + * @param message the non-null, non-empty message + */ + public LoginUnsuccessfulResponse(final ThingStatusDetail thingStatusDetail, final String message) { + Objects.requireNonNull(thingStatusDetail, "thingStatusDetail cannot be null"); + SonyUtil.validateNotEmpty(message, "message cannot be empty"); + + this.thingStatusDetail = thingStatusDetail; + this.message = message; + } + + /** + * Returns the thing status detail for the response + * + * @return a non-null thing status detail + */ + public ThingStatusDetail getThingStatusDetail() { + return thingStatusDetail; + } + + /** + * Returns the message related to the response + * + * @return a non-null, non-empty message response + */ + public String getMessage() { + return message; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyAuth.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyAuth.java new file mode 100644 index 0000000000000..8730fde27863e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyAuth.java @@ -0,0 +1,393 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.ircc.models.IrccClient; +import org.openhab.binding.sony.internal.ircc.models.IrccSystemInformation; +import org.openhab.binding.sony.internal.net.HttpResponse; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.scalarweb.gson.GsonUtilities; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebError; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebRequest; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.ActRegisterId; +import org.openhab.binding.sony.internal.scalarweb.models.api.ActRegisterOptions; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransport; +import org.openhab.binding.sony.internal.transports.TransportOption; +import org.openhab.binding.sony.internal.transports.TransportOptionAutoAuth; +import org.openhab.binding.sony.internal.transports.TransportOptionHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * This class contains all the logic to authorized against a sony device (either Scalar or IRCC) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyAuth { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(SonyAuth.class); + + /** The GSON to use */ + private final Gson gson = GsonUtilities.getApiGson(); + + /** The callback to get an IRCC client instance */ + private final @Nullable IrccClientProvider irccClientProvider; + + /** The activation URL */ + private final @Nullable String activationUrl; + + /** The activation URL version */ + private final String activationVersion; + + /** + * Constructs the authentication from a SCALAR URL + * + * @param url a non-null URL + */ + public SonyAuth(final URL url) { + Objects.requireNonNull(url, "url cannot be null"); + + activationUrl = NetUtil.getSonyUrl(url, ScalarWebService.ACCESSCONTROL); + activationVersion = ScalarWebMethod.V1_0; + irccClientProvider = null; + } + + /** + * Constructs the authentication with a callback for a IRCC client + * + * @param irccClientProvider a non-null callback + */ + public SonyAuth(final IrccClientProvider irccClientProvider) { + this(irccClientProvider, null); + } + + /** + * Constructs the authentication witha callback for a IRCC client and a scalar access control service + * + * @param getIrccClient a non-null IRCC client + * @param accessControlService a possibly null access control service + */ + public SonyAuth(final IrccClientProvider getIrccClient, final @Nullable ScalarWebService accessControlService) { + Objects.requireNonNull(getIrccClient, "getIrccClient cannot be null"); + this.irccClientProvider = getIrccClient; + + String actUrl = null, actVersion = null; + + if (accessControlService != null) { + actUrl = accessControlService == null ? null : accessControlService.getTransport().getBaseUri().toString(); + actVersion = accessControlService == null ? null + : accessControlService.getVersion(ScalarWebMethod.ACTREGISTER); + } + + this.activationUrl = actUrl; + this.activationVersion = SonyUtil.defaultIfEmpty(actVersion, ScalarWebMethod.V1_0); + } + + /** + * Helper method to get the device id header name (X-CERS-DEVICE-ID generally) + * + * @return a non-null device id header name + */ + private String getDeviceIdHeaderName() { + final IrccClient irccClient = irccClientProvider == null ? null : irccClientProvider.getClient(); + final IrccSystemInformation sysInfo = irccClient == null ? null : irccClient.getSystemInformation(); + final String actionHeader = sysInfo == null ? null : sysInfo.getActionHeader(); + return "X-" + SonyUtil.defaultIfEmpty(actionHeader, "CERS-DEVICE-ID"); + } + + /** + * Helper method to get the IRCC registration mode (or null if none) + * + * @return a integer specifying the registration mode or null if none + */ + private @Nullable Integer getRegistrationMode() { + final IrccClient irccClient = irccClientProvider == null ? null : irccClientProvider.getClient(); + return irccClient == null ? null : irccClient.getRegistrationMode(); + } + + /** + * Helper method to get the IRCC registration URL (or null if none) + * + * @return a non-empty URL if found, null if not + */ + private @Nullable String getRegistrationUrl() { + final IrccClient irccClient = irccClientProvider == null ? null : irccClientProvider.getClient(); + return irccClient == null ? null + : SonyUtil.defaultIfEmpty(irccClient.getUrlForAction(IrccClient.AN_REGISTER), null); + } + + /** + * Helper method to get the IRCC activation URL (or null if none) + * + * @return a non-empty URL if found, null if not + */ + private @Nullable String getActivationUrl() { + if (activationUrl != null && !activationUrl.isEmpty()) { + return activationUrl; + } + + final IrccClient irccClient = irccClientProvider == null ? null : irccClientProvider.getClient(); + return irccClient == null ? null : NetUtil.getSonyUrl(irccClient.getBaseUrl(), ScalarWebService.ACCESSCONTROL); + } + + /** + * Request access by initiating the registration or doing the activation if on the second step + * + * @param transport a non-null transport to use + * @param accessCode the access code (null for initial setup) + * @return the http response + */ + public AccessResult requestAccess(final SonyHttpTransport transport, final @Nullable String accessCode) { + Objects.requireNonNull(transport, "transport cannot be null"); + + logger.debug("Requesting access: {}", SonyUtil.defaultIfEmpty(accessCode, "(initial)")); + + if (accessCode != null) { + transport.setOption(new TransportOptionHeader(NetUtil.createAccessCodeHeader(accessCode))); + } + transport.setOption(new TransportOptionHeader(getDeviceIdHeaderName(), NetUtil.getDeviceId())); + + final ScalarWebResult result = scalarActRegister(transport, accessCode); + final HttpResponse httpResponse = result.getHttpResponse(); + + final String registrationUrl = getRegistrationUrl(); + if (httpResponse.getHttpCode() == HttpStatus.UNAUTHORIZED_401) { + if (registrationUrl == null || registrationUrl.isEmpty()) { + return accessCode == null ? AccessResult.PENDING : AccessResult.NOTACCEPTED; + } + } + + if (result.getDeviceErrorCode() == ScalarWebError.NOTIMPLEMENTED + || (result.getDeviceErrorCode() == ScalarWebError.HTTPERROR + && httpResponse.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) + || httpResponse.getHttpCode() == HttpStatus.UNAUTHORIZED_401 + || httpResponse.getHttpCode() == HttpStatus.FORBIDDEN_403 + || httpResponse.getHttpCode() == HttpStatus.NOT_FOUND_404) { + if (registrationUrl != null && !registrationUrl.isEmpty()) { + final HttpResponse irccResponse = irccRegister(transport, accessCode); + if (irccResponse.getHttpCode() == HttpStatus.OK_200) { + return AccessResult.OK; + } else if (irccResponse.getHttpCode() == HttpStatus.UNAUTHORIZED_401) { + return AccessResult.PENDING; + } else { + return new AccessResult(irccResponse); + } + } + } + + if (result.getDeviceErrorCode() == ScalarWebError.DISPLAYISOFF) { + return AccessResult.DISPLAYOFF; + } + + if (httpResponse.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) { + return AccessResult.HOMEMENU; + } + + if (httpResponse.getHttpCode() == HttpStatus.OK_200 + || result.getDeviceErrorCode() == ScalarWebError.ILLEGALARGUMENT) { + return AccessResult.OK; + } + + return new AccessResult(httpResponse); + } + + /** + * Register an access renewal + * + * @param transport a non-null transport to use + * @return the non-null {@link HttpResponse} + */ + public AccessResult registerRenewal(final SonyHttpTransport transport) { + Objects.requireNonNull(transport, "transport cannot be null"); + + logger.debug("Registering Renewal"); + + transport.setOption(new TransportOptionHeader(getDeviceIdHeaderName(), NetUtil.getDeviceId())); + + final ScalarWebResult response = scalarActRegister(transport, null); + + // if good response, return it + if (response.getHttpResponse().getHttpCode() == HttpStatus.OK_200) { + return AccessResult.OK; + } + + // If we got a 401 (unauthorized) and there is no ircc registration url + // return it as well + final String registrationUrl = getRegistrationUrl(); + if (response.getHttpResponse().getHttpCode() == HttpStatus.UNAUTHORIZED_401 + && (registrationUrl == null || registrationUrl.isEmpty())) { + return AccessResult.NEEDSPAIRING; + } + + final HttpResponse irccResponse = irccRenewal(transport); + if (irccResponse.getHttpCode() == HttpStatus.OK_200) { + return AccessResult.OK; + } else { + return new AccessResult(irccResponse); + } + } + + /** + * Register the specified access code + * + * @param transport a non-null transport to use + * @param accessCode the possibly null access code + * @return the non-null {@link HttpResponse} + */ + private HttpResponse irccRegister(final SonyHttpTransport transport, final @Nullable String accessCode) { + Objects.requireNonNull(transport, "transport cannot be null"); + + final String registrationUrl = getRegistrationUrl(); + if (registrationUrl == null || registrationUrl.isEmpty()) { + return new HttpResponse(HttpStatus.SERVICE_UNAVAILABLE_503, "No registration URL"); + } + + // Do the registration first with what the mode says, + // then try it again with the other mode (so registration mode sometimes lie) + final String[] registrationTypes = new String[3]; + if (getRegistrationMode() == 2) { + registrationTypes[0] = "new"; + registrationTypes[1] = "initial"; + registrationTypes[2] = "renewal"; + } else { + registrationTypes[0] = "initial"; + registrationTypes[1] = "new"; + registrationTypes[2] = "renewal"; + } + + final TransportOption[] headers = accessCode == null ? new TransportOption[0] + : new TransportOption[] { new TransportOptionHeader(NetUtil.createAuthHeader(accessCode)) }; + + HttpResponse resp = new HttpResponse(HttpStatus.SERVICE_UNAVAILABLE_503, "unavailable"); + for (String rType : registrationTypes) { + try { + final String rqst = "?name=" + URLEncoder.encode(NetUtil.getDeviceName(), "UTF-8") + + "®istrationType=" + rType + "&deviceId=" + + URLEncoder.encode(NetUtil.getDeviceId(), "UTF-8"); + resp = transport.executeGet(registrationUrl + rqst, headers); + if (resp.getHttpCode() == HttpStatus.OK_200 || resp.getHttpCode() == HttpStatus.UNAUTHORIZED_401) { + return resp; + } + } catch (final UnsupportedEncodingException e) { + resp = new HttpResponse(HttpStatus.SERVICE_UNAVAILABLE_503, e.toString()); + } + } + + return resp; + } + + /** + * Helper method to initiate an IRCC renewal + * + * @param transport a non-null transport to use + * @return the non-null HttpResponse of the renewal + */ + private HttpResponse irccRenewal(final SonyHttpTransport transport) { + Objects.requireNonNull(transport, "transport cannot be null"); + + final String registrationUrl = getRegistrationUrl(); + if (registrationUrl == null || registrationUrl.isEmpty()) { + return new HttpResponse(HttpStatus.SERVICE_UNAVAILABLE_503, "No registration URL"); + } + + try { + final String parms = "?name=" + URLEncoder.encode(NetUtil.getDeviceName(), "UTF-8") + + "®istrationType=renewal&deviceId=" + URLEncoder.encode(NetUtil.getDeviceId(), "UTF-8"); + return transport.executeGet(registrationUrl + parms); + } catch (final UnsupportedEncodingException e) { + return new HttpResponse(HttpStatus.SERVICE_UNAVAILABLE_503, e.toString()); + } + } + + /** + * Helper method to execute an ActRegister to register the system + * + * @param transport a non-null transport to use + * @param accessCode the access code to use (or null to initiate the first step of ActRegister) + * @return the scalar web result + */ + private ScalarWebResult scalarActRegister(final SonyHttpTransport transport, final @Nullable String accessCode) { + Objects.requireNonNull(transport, "transport cannot be null"); + + final String actReg = gson.toJson(new ScalarWebRequest(ScalarWebMethod.ACTREGISTER, activationVersion, + new ActRegisterId(), new Object[] { new ActRegisterOptions() })); + + final String actUrl = getActivationUrl(); + if (actUrl == null) { + return ScalarWebResult.createNotImplemented(ScalarWebMethod.ACTREGISTER); + } + + final HttpResponse r = transport.executePostJson(actUrl, actReg, accessCode == null ? new TransportOption[0] + : new TransportOption[] { new TransportOptionHeader(NetUtil.createAuthHeader(accessCode)) }); + + if (r.getHttpCode() == HttpStatus.OK_200) { + return Objects.requireNonNull(gson.fromJson(r.getContent(), ScalarWebResult.class)); + } else { + return new ScalarWebResult(r); + } + } + + /** + * Sets the authentication header for all specified transports (generally used for preshared keys) + * + * @param accessCode a non-null, non-empty access code + * @param transports the transports to set header authentication + */ + public static void setupHeader(final String accessCode, final SonyTransport... transports) { + SonyUtil.validateNotEmpty(accessCode, "accessCode cannot be empty"); + for (final SonyTransport transport : transports) { + transport.setOption(TransportOptionAutoAuth.FALSE); + transport.setOption(new TransportOptionHeader(NetUtil.createAccessCodeHeader(accessCode))); + } + } + + /** + * Sets up cookie authorization on all specified transports + * + * @param transports the transports to set cookie authentication + */ + public static void setupCookie(final SonyTransport... transports) { + for (final SonyTransport transport : transports) { + transport.setOption(TransportOptionAutoAuth.TRUE); + } + } + + /** + * Functional interface to retrive an IRCC client + */ + @NonNullByDefault + public interface IrccClientProvider { + /** + * Called when an IRCC client is needed + * + * @return a potentially null IRCC client + */ + @Nullable + IrccClient getClient(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyAuthChecker.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyAuthChecker.java new file mode 100644 index 0000000000000..8424c74583913 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyAuthChecker.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebConstants; +import org.openhab.binding.sony.internal.transports.SonyTransport; +import org.openhab.binding.sony.internal.transports.TransportOptionHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class contains the logic to determine if an authorization call is needed (via {@link SonyAuth}) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyAuthChecker { + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(SonyAuthChecker.class); + + /** The transport to use for check authorization */ + private final SonyTransport transport; + + /** The current access code */ + private final @Nullable String accessCode; + + /** + * Constructs the checker from the transport and access code + * + * @param transport a non-null transport + * @param accessCode a possibly null, possibly empty access code + */ + public SonyAuthChecker(final SonyTransport transport, final @Nullable String accessCode) { + Objects.requireNonNull(transport, "transport cannot be null"); + + this.transport = transport; + this.accessCode = accessCode; + } + + /** + * Checks the result using the specified callback + * + * @param callback a non-null callback + * @return a non-null result + */ + public CheckResult checkResult(final CheckResultCallback callback) { + Objects.requireNonNull(callback, "callback cannot be null"); + + final String localAccessCode = accessCode; + + // If we have an access code and it's not RQST... + // try to set the access code header and check for a good result + // This will work in a few different scenarios where a header is required for communications to be successful + // If this works - return back that we had an OK using a HEADER (ie OK_HEADER) + // + // Note: we ignore RQST because we don't want to trigger the pairing screen on a device at this stage + // and/or we are cookie based (probably websocket or authentication has been turned off on the device) + if (localAccessCode != null && !ScalarWebConstants.ACCESSCODE_RQST.equalsIgnoreCase(localAccessCode)) { + final TransportOptionHeader authHeader = new TransportOptionHeader( + NetUtil.createAccessCodeHeader(localAccessCode)); + try { + logger.debug("localAccessCode: '{}'", localAccessCode); + transport.setOption(authHeader); + if (AccessResult.OK.equals(callback.checkResult())) { + logger.debug("checkResult: '{}'", CheckResult.OK_HEADER.getCode()); + return CheckResult.OK_HEADER; + } + } finally { + transport.removeOption(authHeader); + } + } + + // If we made it here - we are likely not header based but cookie based (or we are not even authenticated) + // Attempt the check result without the auth header and return OK_COOKIE is good + final AccessResult res = callback.checkResult(); + logger.debug("res: '{}'", res.getCode()); + if (res == null) { + logger.debug("checkResult: '{}'", CheckResult.OTHER); + return new CheckResult(CheckResult.OTHER, "Check result returned null"); + } + + if (AccessResult.OK.equals(res)) { + logger.debug("checkResult: '{}'", CheckResult.OK_COOKIE.getCode()); + return CheckResult.OK_COOKIE; + } + + // We aren't either cookie or header based - return the results (likely needs pairing or the screen is off or + // not on the main screen) + logger.debug("checkResult: '{}'", new CheckResult(res).getCode()); + return new CheckResult(res); + } + + /** + * Functional interface defining the check result callback + */ + @NonNullByDefault + public interface CheckResultCallback { + /** + * Called to check a result and return an {@link AccessResult} + * + * @return a non-null access result + */ + AccessResult checkResult(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyBindingConstants.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyBindingConstants.java new file mode 100644 index 0000000000000..00085a8150001 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyBindingConstants.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SonyBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyBindingConstants { + + /** The binding identifier for all sony products. */ + public static final String BINDING_ID = "sony"; + + /** Constants used by discovery */ + public static final String SONY_REMOTEDEVICE_ID = "sony"; + public static final String SONY_SERVICESCHEMA = "schemas-sony-com"; + public static final String DIAL_SERVICESCHEMA = "dial-multiscreen-org"; + public static final String SONY_IRCCSERVICENAME = "IRCC"; + public static final String SONY_DIALSERVICENAME = "dial"; + public static final String SONY_SCALARWEBSERVICENAME = "ScalarWebAPI"; + + /** Thing type identifiers */ + public static final String SIMPLEIP_THING_TYPE_PREFIX = "simpleip"; + public static final String DIAL_THING_TYPE_PREFIX = "dial"; + public static final String IRCC_THING_TYPE_PREFIX = "ircc"; + public static final String SCALAR_THING_TYPE_PREFIX = "scalar"; + + /** Misc */ + public static final String MODELNAME_VERSION_PREFIX = "_V"; + + // The timeout (in seconds) to wait on a response + public static final Integer RSP_WAIT_TIMEOUTSECONDS = 10; + public static final Integer THING_CACHECOMMAND_TIMEOUTMS = 120000; + + /** The user agent for communications (and identification on the device) */ + public static final String NET_USERAGENT = "OpenHab/Sony/Binding"; +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyHandlerFactory.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyHandlerFactory.java new file mode 100644 index 0000000000000..e5563aba0f830 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyHandlerFactory.java @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import java.util.Map; +import java.util.Objects; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.openhab.binding.sony.internal.dial.DialConstants; +import org.openhab.binding.sony.internal.dial.DialHandler; +import org.openhab.binding.sony.internal.ircc.IrccConstants; +import org.openhab.binding.sony.internal.ircc.IrccHandler; +import org.openhab.binding.sony.internal.providers.SonyDefinitionProvider; +import org.openhab.binding.sony.internal.providers.SonyDynamicStateProvider; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebHandler; +import org.openhab.binding.sony.internal.simpleip.SimpleIpConstants; +import org.openhab.binding.sony.internal.simpleip.SimpleIpHandler; +import org.openhab.core.io.net.http.WebSocketFactory; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.transform.TransformationHelper; +import org.openhab.core.transform.TransformationService; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SonyHandlerFactory} is responsible for creating all things sony! + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@Component(service = ThingHandlerFactory.class, configurationPid = "sony.things") +public class SonyHandlerFactory extends BaseThingHandlerFactory { + /** The logger */ + protected Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * websocket client used for scalar operations + */ + private final WebSocketClient webSocketClient; + + /** + * The sony thing type provider + */ + private final SonyDefinitionProvider sonyDefinitionProvider; + + /** + * The sony thing type provider + */ + private final SonyDynamicStateProvider sonyDynamicStateProvider; + + /** + * The clientBuilder used in HttpRequest + */ + private final ClientBuilder clientBuilder; + + /** + * The OSGI properties for the things + */ + private final Map osgiProperties; + + /** + * Constructs the handler factory + * + * @param webSocketFactory a non-null websocket factory + * @param sonyDefinitionProvider a non-null sony definition provider + * @param sonyDynamicStateProvider a non-null sony dynamic state provider + * @param clientBuilder a non-null client builder + * @param osgiProperties a non-null, possibly empty list of OSGI properties + */ + @Activate + public SonyHandlerFactory(final @Reference WebSocketFactory webSocketFactory, + final @Reference SonyDefinitionProvider sonyDefinitionProvider, + final @Reference SonyDynamicStateProvider sonyDynamicStateProvider, + final @Reference ClientBuilder clientBuilder, final Map osgiProperties) { + Objects.requireNonNull(webSocketFactory, "webSocketFactory cannot be null"); + Objects.requireNonNull(sonyDefinitionProvider, "sonyDefinitionProvider cannot be null"); + Objects.requireNonNull(sonyDynamicStateProvider, "sonyDynamicStateProvider cannot be null"); + Objects.requireNonNull(osgiProperties, "osgiProperties cannot be null"); + this.webSocketClient = webSocketFactory.getCommonWebSocketClient(); + this.sonyDefinitionProvider = sonyDefinitionProvider; + this.sonyDynamicStateProvider = sonyDynamicStateProvider; + this.clientBuilder = clientBuilder; + this.osgiProperties = osgiProperties; + } + + @Override + public boolean supportsThingType(final ThingTypeUID thingTypeUID) { + Objects.requireNonNull(thingTypeUID, "thingTypeUID cannot be null"); + return SonyBindingConstants.BINDING_ID.equalsIgnoreCase(thingTypeUID.getBindingId()); + } + + @Override + protected @Nullable ThingHandler createHandler(final Thing thing) { + Objects.requireNonNull(thing, "thing cannot be null"); + + final ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (thingTypeUID.equals(SimpleIpConstants.THING_TYPE_SIMPLEIP)) { + final TransformationService transformationService = TransformationHelper + .getTransformationService(getBundleContext(), "MAP"); + return new SimpleIpHandler(thing, transformationService); + } else if (thingTypeUID.equals(IrccConstants.THING_TYPE_IRCC)) { + final TransformationService transformationService = TransformationHelper + .getTransformationService(getBundleContext(), "MAP"); + return new IrccHandler(thing, transformationService, clientBuilder); + } else if (thingTypeUID.equals(DialConstants.THING_TYPE_DIAL)) { + return new DialHandler(thing, clientBuilder); + } else if (thingTypeUID.getId().startsWith(SonyBindingConstants.SCALAR_THING_TYPE_PREFIX)) { + final TransformationService transformationService = TransformationHelper + .getTransformationService(getBundleContext(), "MAP"); + + ThingHandler th = new ScalarWebHandler(thing, transformationService, webSocketClient, clientBuilder, + sonyDefinitionProvider, sonyDynamicStateProvider, osgiProperties); + return th; + // return new ScalarWebHandler(thing, transformationService, webSocketClient, sonyDefinitionProvider, + // sonyDynamicStateProvider, osgiProperties, clientBuilder); + } + logger.info("ThingHandler returns null"); + return null; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyUtil.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyUtil.java new file mode 100644 index 0000000000000..f8e6aa0990e49 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/SonyUtil.java @@ -0,0 +1,834 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.Future; +import java.util.stream.Collectors; + +import javax.measure.Quantity; +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Various, usually unrelated, utility functions used across the sony binding + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyUtil { + + /** + * Maps primitive {@code Class}es to their corresponding wrapper {@code Class}. + */ + private static final Map, Class> primitiveWrapperMap = new HashMap<>(); + static { + primitiveWrapperMap.put(Boolean.TYPE, Boolean.class); + primitiveWrapperMap.put(Byte.TYPE, Byte.class); + primitiveWrapperMap.put(Character.TYPE, Character.class); + primitiveWrapperMap.put(Short.TYPE, Short.class); + primitiveWrapperMap.put(Integer.TYPE, Integer.class); + primitiveWrapperMap.put(Long.TYPE, Long.class); + primitiveWrapperMap.put(Double.TYPE, Double.class); + primitiveWrapperMap.put(Float.TYPE, Float.class); + primitiveWrapperMap.put(Void.TYPE, Void.TYPE); + } + + /** Bigdecimal hundred (used in scale/unscale methods) */ + public static final BigDecimal BIGDECIMAL_HUNDRED = BigDecimal.valueOf(100); + + /** + * Creates a channel identifier from the group (if specified) and channel id + * + * @param groupId the possibly null, possibly empty group id + * @param channelId the non-null, non-empty channel id + * @return a non-null, non-empty channel id + */ + public static String createChannelId(final @Nullable String groupId, final String channelId) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + return groupId == null || groupId.isEmpty() ? channelId : (groupId + "#" + channelId); + } + + /** + * This utility function will take a potential channelUID string and return a valid channelUID by removing all + * invalidate characters (see {@link AbstractUID#SEGMENT_PATTERN}) + * + * @param channelUID the non-null, possibly empty channelUID to validate + * @return a non-null, potentially empty string representing what was valid + */ + public static String createValidChannelUId(final String channelUID) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + final String id = channelUID.replaceAll("[^A-Za-z0-9_-]", "").toLowerCase(); + return SonyUtil.isEmpty(id) ? "na" : id; + } + + /** + * Utility function to close a {@link AutoCloseable} and log any exception thrown. + * + * @param closeable a possibly null {@link AutoCloseable}. If null, no action is done. + */ + public static void close(final @Nullable AutoCloseable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (final Exception e) { + LoggerFactory.getLogger(SonyUtil.class).debug("Exception closing: {}", e.getMessage(), e); + } + } + } + + /** + * Determines if the current thread has been interrupted or not + * + * @return true if interrupted, false otherwise + */ + public static boolean isInterrupted() { + return Thread.currentThread().isInterrupted(); + } + + /** + * Checks whether the current thread has been interrupted and throws {@link InterruptedException} if it's been + * interrupted. + * + * @throws InterruptedException the interrupted exception + */ + public static void checkInterrupt() throws InterruptedException { + if (isInterrupted()) { + throw new InterruptedException("thread interrupted"); + } + } + + /** + * Cancels the specified {@link Future}. + * + * @param future a possibly null future. If null, no action is done + */ + public static void cancel(final @Nullable Future future) { + if (future != null) { + future.cancel(true); + } + } + + /** + * Returns a new string type or UnDefType.UNDEF if the string is null + * + * @param str the possibly null string + * @return either a StringType or UnDefType.UNDEF is null + */ + public static State newStringType(final @Nullable String str) { + return str == null ? UnDefType.UNDEF : new StringType(str); + } + + /** + * Returns a new quantity type or UnDefType.UNDEF if the integer is null + * + * @param itgr the possibly null integer + * @param unit a non-null unit + * @return either a QuantityType or UnDefType.UNDEF is null + */ + public static > State newQuantityType(final @Nullable Integer itgr, final Unit unit) { + Objects.requireNonNull(unit, "unit cannot be null"); + return itgr == null ? UnDefType.UNDEF : new QuantityType(itgr, unit); + } + + /** + * Returns a new quantity type or UnDefType.UNDEF if the double is null + * + * @param dbl the possibly null double + * @param unit a non-null unit + * @return either a QuantityType or UnDefType.UNDEF is null + */ + public static > State newQuantityType(final @Nullable Double dbl, final Unit unit) { + Objects.requireNonNull(unit, "unit cannot be null"); + return dbl == null ? UnDefType.UNDEF : new QuantityType(dbl, unit); + } + + /** + * Returns a new decimal type or UnDefType.UNDEF if the integer is null + * + * @param itgr the possibly null integer + * @return either a DecimalType or UnDefType.UNDEF is null + */ + public static State newDecimalType(final @Nullable Integer itgr) { + return itgr == null ? UnDefType.UNDEF : new DecimalType(itgr); + } + + /** + * Returns a new decimal type or UnDefType.UNDEF if the double is null + * + * @param dbl the possibly null double + * @return either a DecimalType or UnDefType.UNDEF is null + */ + public static State newDecimalType(final @Nullable Double dbl) { + return dbl == null ? UnDefType.UNDEF : new DecimalType(dbl); + } + + /** + * Returns a new decimal type or UnDefType.UNDEF if the string representation is null + * + * @param nbr the possibly null, possibly empty string decimal + * @return either a DecimalType or UnDefType.UNDEF is null + */ + public static State newDecimalType(final @Nullable String nbr) { + return nbr == null || nbr.isEmpty() ? UnDefType.UNDEF : new DecimalType(nbr); + } + + /** + * Returns a new percent type or UnDefType.UNDEF if the value is null + * + * @param val the possibly null big decimal + * @return either a PercentType or UnDefType.UNDEF is null + */ + public static State newPercentType(final @Nullable BigDecimal val) { + return val == null ? UnDefType.UNDEF : new PercentType(val); + } + + /** + * Returns a new percent type or UnDefType.UNDEF if the value is null + * + * @param val the possibly null big decimal + * @return either a PercentType or UnDefType.UNDEF is null + */ + public static State newBooleanType(final @Nullable Boolean val) { + return val == null ? UnDefType.UNDEF : val.booleanValue() ? OnOffType.ON : OnOffType.OFF; + } + + /** + * Scales the associated big decimal within the miniumum/maximum defined + * + * @param value a non-null value to scale + * @param minimum a possibly null minimum value (if null, zero will be used) + * @param maximum a possibly null maximum value (if null, 100 will be used) + * @return a scaled big decimal value + */ + public static BigDecimal scale(final BigDecimal value, final @Nullable BigDecimal minimum, + final @Nullable BigDecimal maximum) { + Objects.requireNonNull(value, "value cannot be null"); + + final int initialScale = value.scale(); + + final BigDecimal min = minimum == null ? BigDecimal.ZERO : minimum; + final BigDecimal max = maximum == null ? BIGDECIMAL_HUNDRED : maximum; + + if (min.compareTo(max) > 0) { + return BigDecimal.ZERO; + } + + final BigDecimal val = guard(value, min, max); + final BigDecimal scaled = val.subtract(min).multiply(BIGDECIMAL_HUNDRED).divide(max.subtract(min), + initialScale + 2, RoundingMode.HALF_UP); + return guard(scaled.setScale(initialScale, RoundingMode.HALF_UP), BigDecimal.ZERO, BIGDECIMAL_HUNDRED); + } + + /** + * Unscales the associated big decimal within the miniumum/maximum defined + * + * @param scaledValue a non-null scaled value + * @param minimum a possibly null minimum value (if null, zero will be used) + * @param maximum a possibly null maximum value (if null, 100 will be used) + * @return a scaled big decimal value + */ + public static BigDecimal unscale(final BigDecimal scaledValue, final @Nullable BigDecimal minimum, + final @Nullable BigDecimal maximum) { + Objects.requireNonNull(scaledValue, "scaledValue cannot be null"); + + final int initialScale = scaledValue.scale(); + final BigDecimal min = minimum == null ? BigDecimal.ZERO : minimum; + final BigDecimal max = maximum == null ? BIGDECIMAL_HUNDRED : maximum; + + if (min.compareTo(max) > 0) { + return min; + } + + final BigDecimal scaled = guard(scaledValue, BigDecimal.ZERO, BIGDECIMAL_HUNDRED); + final BigDecimal val = max.subtract(min) + .multiply(scaled.divide(BIGDECIMAL_HUNDRED, initialScale + 2, RoundingMode.HALF_UP)).add(min); + + return guard(val.setScale(initialScale, RoundingMode.HALF_UP), min, max); + } + + /** + * Provides a guard to value (value must be within the min/max range - if outside, will be set to the min or max) + * + * @param value a non-null value to guard + * @param minimum a non-null minimum value + * @param maximum a non-null maximum value + * @return a big decimal within the min/max range + */ + public static BigDecimal guard(final BigDecimal value, final BigDecimal minimum, final BigDecimal maximum) { + Objects.requireNonNull(value, "value cannot be null"); + Objects.requireNonNull(minimum, "minimum cannot be null"); + Objects.requireNonNull(maximum, "maximum cannot be null"); + if (value.compareTo(minimum) < 0) { + return minimum; + } + if (value.compareTo(maximum) > 0) { + return maximum; + } + return value; + } + + /** + * Performs a WOL if there is a configured ip address and mac address. If either ip address or mac address is + * null/empty, call is ignored + * + * @param logger the non-null logger to log messages to + * @param deviceIpAddress the possibly null, possibly empty device ip address + * @param deviceMacAddress the possibly null, possibly empty device mac address + */ + public static void sendWakeOnLan(final Logger logger, final @Nullable String deviceIpAddress, + final @Nullable String deviceMacAddress) { + Objects.requireNonNull(logger, "logger cannot be null"); + + if (deviceIpAddress != null && deviceMacAddress != null && !deviceIpAddress.isBlank() + && !deviceMacAddress.isBlank()) { + try { + NetUtil.sendWol(deviceIpAddress, deviceMacAddress); + // logger.debug("WOL packet sent to {}", deviceMacAddress); + logger.info("WOL packet sent to {}", deviceMacAddress); + } catch (final IOException e) { + logger.debug("Exception occurred sending WOL packet to {}", deviceMacAddress, e); + } + } else { + logger.debug( + "WOL packet is not supported - specify the IP address and mac address in config if you want a WOL packet sent"); + } + } + + /** + * Returns true if the two maps are: both null or of equal size, all keys and values (case insensitve) match + * + * @param map1 a possibly null, possibly empty map + * @param map2 a possibly null, possibly empty map + * @return true if they match, false otherwise + */ + public static boolean equalsIgnoreCase(final Map map1, final Map map2) { + Objects.requireNonNull(map1, "map1 cannot be null"); + Objects.requireNonNull(map2, "map2 cannot be null"); + + if (map1.size() != map2.size()) { + return false; + } + + final Map lowerMap1 = map1.entrySet().stream() + .map(s -> new AbstractMap.SimpleEntry<>(s.getKey().toLowerCase(), s.getValue().toLowerCase())) + .collect(Collectors.toMap(k -> k.getKey(), v -> v.getValue())); + + final Map lowerMap2 = map1.entrySet().stream() + .map(s -> new AbstractMap.SimpleEntry<>(s.getKey().toLowerCase(), s.getValue().toLowerCase())) + .collect(Collectors.toMap(k -> k.getKey(), v -> v.getValue())); + + return lowerMap1.equals(lowerMap2); + } + + /** + * Null safety compare of two strings + * + * @param str1 a possibly null string + * @param str2 a possibly null string to compare + * @return true if strings are equal or both null, other false + */ + public static boolean equals(final @Nullable String str1, final @Nullable String str2) { + if (str1.equals(str2)) { + return true; + } + if (str1 == null || str2 == null) { + return false; + } + return str1.equals(str2); + } + + /** + * Converts a string to a Boolean object + * + * @param str a possibly null string representation of a boolean + * @return null if string is null, TRUE if string represents a true boolean, FALSE otherwise + */ + public static @Nullable Boolean toBooleanObject(@Nullable String str) { + if (str == null) { + return null; + } + if ("true".equalsIgnoreCase(str)) { + return Boolean.TRUE; + } + if ("yes".equalsIgnoreCase(str)) { + return Boolean.TRUE; + } + if ("on".equalsIgnoreCase(str)) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } + + /** + * Checks if a string represents an integer number + * + * @param str the possibly null string + * @return true if string represents an integer number, false otherwise + */ + public static boolean isNumeric(@Nullable String str) { + if (str == null) { + return false; + } + try { + Long.parseLong(str); + } catch (NumberFormatException nfe) { + return false; + } + return true; + } + + /** + * Checks if a string represetns a (double) number + * + * @param str the possibly null string + * @return true if string represents a double number, false otherwise + */ + public static boolean isNumber(@Nullable String str) { + if (str == null) { + return false; + } + try { + Double.parseDouble(str); + } catch (NumberFormatException nfe) { + return false; + } + return true; + } + + /** + * Capitalize string + * + * @param str the possibly null string + * @return empty string id string i null or empty, otherwise the capitalized string + */ + public static String capitalize(@Nullable String str) { + if (isEmpty(str)) { + return ""; + } + return str.substring(0, 1).toUpperCase() + str.substring(1); + } + + /** + * Left padding of string with character + * + * @param str the string + * @param padSize the padding size + * @param padChar the padding character + * @return the left padded string + */ + public static String leftPad(String str, int padSize, Character padChar) { + return padChar.toString().repeat(Math.max(padSize - str.length(), 0)) + str; + } + + /** + * Right padding of string with character + * + * @param str the string + * @param padSize the padding size + * @param padChar the padding character + * @return the right padded string + */ + public static String rightPad(String str, int padSize, Character padChar) { + return str + padChar.toString().repeat(Math.max(padSize - str.length(), 0)); + } + + /** + * Trim string + * + * @param str possibly null string + * @return empty string if input string is null, otherwise trimmed string + */ + public static String trimToEmpty(@Nullable String str) { + if (str == null) { + return ""; + } else { + return str.trim(); + } + } + + /** + * Strip character sequence from start of a string + * + * @param str the string + * @param stripChars the strip characters + * @return the stripped string + */ + public static String stripStart(final String str, final String stripChars) { + final int strLen = str.length(); + if (strLen == 0) { + return str; + } + int start = 0; + if (stripChars.isEmpty()) { + return str; + } else { + while (start != strLen && stripChars.indexOf(str.charAt(start)) >= 0) { + start++; + } + } + return str.substring(start); + } + + /** + * Strip character sequence from end of a string + * + * @param str the string + * @param stripChars the strip characters + * @return the stripped string + */ + public static String stripEnd(final String str, final String stripChars) { + int end = str.length(); + if (end == 0) { + return str; + } + + if (stripChars == null) { + while (end != 0 && Character.isWhitespace(str.charAt(end - 1))) { + end--; + } + } else if (stripChars.isEmpty()) { + return str; + } else { + while (end != 0 && stripChars.indexOf(str.charAt(end - 1)) >= 0) { + end--; + } + } + return str.substring(0, end); + } + + public static String join(final String delimiter, final @Nullable String @Nullable [] strArray) { + // public static String join(final String delimiter, final String[] strArray) { + if (strArray == null) { + return ""; + } + return Arrays.stream(strArray).map(s -> s == null ? "" : s).collect(Collectors.joining(delimiter)); + } + + /** + * Returns true if the two sets are: both null or of equal size and all keys match (case insensitive) + * + * @param set1 a possibly null, possibly empty set + * @param set2 a possibly null, possibly empty set + * @return true if they match, false otherwise + */ + public static boolean equalsIgnoreCase(final @Nullable Set<@Nullable String> set1, + final @Nullable Set<@Nullable String> set2) { + if (set1 == null && set2 == null) { + return true; + } + + if (set1 == null) { + return false; + } + + if (set2 == null) { + return false; + } + + if (set1.size() != set2.size()) { + return false; + } + final TreeSet tset1 = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + // convert null strings in source set to empty strings to avoid null type mismatch + set1.stream().map(s -> s == null ? "" : s).forEach(s -> tset1.add(Objects.requireNonNull(s))); + final TreeSet tset2 = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + // convert null strings in source set to empty strings to avoid null type mismatch + set2.stream().map(s -> s == null ? "" : s).forEach(s -> tset2.add(Objects.requireNonNull(s))); + return tset1.equals(tset2); + } + + /** + * Determines if the model name is valid (alphanumeric plus dash) + * + * @param modelName a non-null, possibly empty model name + * @return true if a valid model name, false otherwise + */ + public static boolean isValidModelName(final String modelName) { + return modelName.matches("[A-Za-z0-9-]+");// && modelName.matches(".*\\d\\d.*"); - only valid for tvs + } + + /** + * Determines if the thing type UID is a generic thing type (scalar) or a custom one (scalar-X800) + * + * @param uid a non-null UID + * @return true if generic, false otherwise + */ + public static boolean isGenericThingType(final ThingTypeUID uid) { + Objects.requireNonNull(uid, "uid cannot be null"); + + final String typeId = uid.getId(); + return typeId.indexOf("-") < 0; + } + + /** + * Get's the service name from a thing type uid ("scalar" for example if "scalar" or "scalar-X800" or + * "scalar-X800_V2") + * + * @param uid a non-null UID + * @return a non-null service name + */ + public static String getServiceName(final ThingTypeUID uid) { + Objects.requireNonNull(uid, "uid cannot be null"); + + final String typeId = uid.getId(); + final int idx = typeId.indexOf("-"); + + return idx < 0 ? typeId : typeId.substring(0, idx); + } + + /** + * Get's the model name from a thing type uid (null if just "scalar" or "X800" if "scalar-X800" or "X800" if + * "scalar-X800_V2") + * + * @param uid a non-null UID + * @return a model name or null if not found (ie generic) + */ + public static @Nullable String getModelName(final ThingTypeUID uid) { + Objects.requireNonNull(uid, "uid cannot be null"); + + final String typeId = uid.getId(); + final int idx = typeId.indexOf("-"); + if (idx < 0) { + return null; + } + + final String modelName = typeId.substring(idx + 1); + + final int versIdx = modelName.lastIndexOf(SonyBindingConstants.MODELNAME_VERSION_PREFIX); + return versIdx >= 0 ? modelName.substring(0, versIdx) : modelName; + } + + /** + * Get's the model version number from a thing type uid ("2" if "scalar-X800_V2" or 0 in all other cases) + * + * @param uid a non-null thing type uid + * @return the model version (with 0 being the default) + */ + public static int getModelVersion(final ThingTypeUID uid) { + Objects.requireNonNull(uid, "uid cannot be null"); + + final String modelName = getModelName(uid); + if (modelName == null || modelName.isEmpty()) { + return 0; + } + + final int versIdx = modelName.lastIndexOf(SonyBindingConstants.MODELNAME_VERSION_PREFIX); + if (versIdx > 0) { + final String vers = modelName.substring(versIdx + SonyBindingConstants.MODELNAME_VERSION_PREFIX.length()); + try { + return Integer.parseInt(vers); + } catch (final NumberFormatException e) { + return 0; + } + } + + return 0; + } + + /** + * Determins if a thing type service/model name (which can contain wildcards) matches the corresponding + * service/model name (regardless of the model version) + * + * @param thingTypeServiceName a possibly null, possibly empty thing service name + * @param thingTypeModelName a possibly null, possibly empty thing model name. Use "x" (case sensitive) to denote a + * wildcard (like 'XBR-xX830' to match all screen sizes) + * @param serviceName a non-null, non-empty service name + * @param modelName a non-null, non-empty model name + * @return true if they match (regardless of model name version), false otherwise + */ + public static boolean isModelMatch(final @Nullable String thingTypeServiceName, + final @Nullable String thingTypeModelName, final String serviceName, final String modelName) { + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + SonyUtil.validateNotEmpty(modelName, "modelName cannot be empty"); + if (thingTypeServiceName == null || thingTypeServiceName.isEmpty()) { + return false; + } + + if (thingTypeModelName == null || thingTypeModelName.isEmpty()) { + return false; + } + + String modelPattern = thingTypeModelName.replaceAll("x", ".*").toLowerCase(); + + // remove a version identifier ("_V1" or "_V292") + final int versIdx = modelPattern.lastIndexOf(SonyBindingConstants.MODELNAME_VERSION_PREFIX.toLowerCase()); + if (versIdx > 0) { + final String vers = modelPattern.substring(versIdx + 2); + if (SonyUtil.isNumeric(vers)) { + modelPattern = modelPattern.substring(0, versIdx); + } + } + + return thingTypeServiceName.equals(serviceName) && modelName.toLowerCase().matches(modelPattern); + } + + /** + * Determines if the thingtype uid matches the specified serviceName/model name + * + * @param uid a non-null thing type UID + * @param serviceName a non-null, non-empty service name + * @param modelName a non-null, non-empty model name + * @return true if they match (regardless of model name version), false otherwise + */ + public static boolean isModelMatch(final ThingTypeUID uid, final String serviceName, final String modelName) { + Objects.requireNonNull(uid, "uid cannot be null"); + SonyUtil.validateNotEmpty(modelName, "modelName cannot be empty"); + + final String uidServiceName = getServiceName(uid); + final String uidModelName = getModelName(uid); + return uidModelName == null || uidModelName.isEmpty() ? false + : isModelMatch(uidServiceName, uidModelName, serviceName, modelName); + } + + /** + * Converts a nullable list (with nullable elements) to a non-null list (containing no null elements) by filtering + * all null elements out + * + * @param list the list to convert + * @return a non-null list of the same type + */ + public static List convertNull(final @Nullable List<@Nullable T> list) { + if (list == null) { + return new ArrayList<>(); + } + + return list.stream().filter(e -> e != null).collect(Collectors.toList()); + } + + /** + * Converts a nullable array (with nullable elements) to a non-null list (containing no null elements) by filtering + * all null elements out + * + * @param list the array to convert + * @return a non-null list of the same type + */ + public static List convertNull(final @Nullable T @Nullable [] list) { + if (list == null) { + return new ArrayList<>(); + } + + return Arrays.stream(list).filter(e -> e != null).collect(Collectors.toList()); + } + + /** + * Determines if the pass class is a primitive (we treat string as a primitive here) + * + * @param clazz a non-null class + * @return true if primitive, false otherwise + */ + public static boolean isPrimitive(final Class clazz) { + Objects.requireNonNull(clazz, "clazz cannot be null"); + return clazz.isPrimitive() || primitiveWrapperMap.get(clazz) != null || clazz == String.class; + } + + /** + * Determines if the pass string is a null or empty + * + * @param str the String to check, may be null + * @return true if string is null or empty, false otherwise + */ + public static boolean isEmpty(final @Nullable String str) { + return (str == null || str.isEmpty()); + } + + /** + * Returns original string if not empty, otherwise default string + * + * @param str the original string which is checked for emptiness + * @param defStr the default string + * @return str if not null or empty, otherwise the default or empty string + */ + public static String defaultIfEmpty(final @Nullable String str, final @Nullable String defStr) { + return str != null && !str.isEmpty() ? str : (defStr != null ? defStr : ""); + } + + /** + * Case insensitive check if a String ends with a specified suffix + * + * @see java.lang.String#endsWith(String) + * @param str the String to check, may be null + * @param suffix the suffix to find, may be null + * @return true if the String ends with the suffix, case insensitive, or + * both null + * @since 2.4 + */ + public static boolean endsWithIgnoreCase(final String str, final String suffix) { + int strOffset = str.length() - suffix.length(); + return str.regionMatches(true, strOffset, suffix, 0, suffix.length()); + } + + /** + * Validates if string is not empty and throws IllegalArgumentExction if invalid + * + * @param str the String to validate + * @param message the message + */ + public static void validateNotEmpty(final String str, final String message) { + if (SonyUtil.isEmpty(str)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Returns the substring before the first occurrence of a delimiter. The + * delimiter is not part of the result. + * + * @param str String to get a substring from. + * @param del String to search for. + * @return Substring before the first occurrence of the delimiter. + */ + public static String substringBefore(String str, String del) { + int pos = str.indexOf(del); + + return pos >= 0 ? str.substring(0, pos) : del; + } + + /** + * Returns the substring after the first occurrence of a delimiter. The + * delimiter is not part of the result. + * + * @param str String to get a substring from. + * @param del String to search for. + * @return Substring after the last occurrence of the delimiter. + */ + public static String substringAfter(String str, String del) { + int pos = str.indexOf(del); + + return pos >= 0 ? str.substring(pos + del.length()) : ""; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ThingCallback.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ThingCallback.java new file mode 100644 index 0000000000000..084bf331f8a70 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ThingCallback.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.types.State; + +/** + * This interface is used to provide a callback mechanism between listener (usually a protocol of some sort) and the + * associated {@link ThingHandler}. This is necessary since the status and state of a thing is private and the protocol + * handler cannot access it directly. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface ThingCallback { + + /** + * Callback to the bridge/thing to update the status of the bridge/thing. + * + * @param state the non-null {@link ThingStatus} + * @param detail a non-null {@link ThingStatusDetail} + * @param msg a possibly null, possibly empty message + */ + void statusChanged(final ThingStatus state, final ThingStatusDetail detail, final @Nullable String msg); + + /** + * Callback to the bridge/thing to update the state of a channel in the bridge/thing. + * + * @param channelId the non-null, non-empty channel id + * @param newState the possibly null new state + */ + void stateChanged(final String channelId, final State newState); + + /** + * Callback to set a property in the bridge/thing. + * + * @param propertyName a non-null, non-empty property name + * @param propertyValue a possibly null, possibly empty property value + */ + void setProperty(final String propertyName, final @Nullable String propertyValue); +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/UidUtils.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/UidUtils.java new file mode 100644 index 0000000000000..e9ace5758c69a --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/UidUtils.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.model.types.UDN; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; + +/** + * Utility class for various UID type of operations + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class UidUtils { + /** + * Gets the device id from the UUID string ("[uuid:]{deviceid}::etc") from the specified {@link UDN} + * + * @param udn a non-null {@link UDN} + * @return the device id or null (possibly empty) if not found + */ + @Nullable + public static String getDeviceId(final UDN udn) { + Objects.requireNonNull(udn, "udn cannot be null"); + + final String uuid = udn.getIdentifierString(); + + final String[] uuidParts = uuid.split(":"); + if (uuidParts == null || uuidParts.length == 0) { + return null; + } else if (uuidParts.length == 1) { + return uuidParts[0]; // probably was just "{deviceid}" or "{deviceid}:etc" + } else if (uuidParts.length == 2) { + return uuidParts[1]; // probably was "uuid:{deviceid}.." + } + return uuid; + } + + /** + * Create a {@link ThingUID} for the specified {@link ThingTypeUID} and {@link UDN} + * + * @param udn a non-null {@link UDN} + * @return a possibly null {@link ThingUID} + */ + @Nullable + public static String getThingId(final UDN udn) { + Objects.requireNonNull(udn, "udn cannot be null"); + + final String uuid = getDeviceId(udn); + if (SonyUtil.isEmpty(uuid)) { + return null; + } + + // Not entirely correct however most UUIDs are version 1 + // which makes the last node the mac address + // Close enough to unique for our purposes - we just + // verify the mac address is 12 characters in length + // if not, we fall back to using the full uuid + final String[] uuidParts = uuid.split("-"); + final String macAddress = uuidParts[uuidParts.length - 1]; + return macAddress.length() == 12 ? macAddress : uuid; + } + + /** + * Create a {@link ThingUID} for the specified {@link ThingTypeUID} and {@link UDN} + * + * @param thingTypeId a non-null {@link ThingTypeUID} + * @param udn a non-null {@link UDN} + * @return a possibly null {@link ThingUID} + */ + public static ThingUID createThingUID(final ThingTypeUID thingTypeId, final UDN udn) { + Objects.requireNonNull(thingTypeId, "thingTypeId cannot be null"); + Objects.requireNonNull(udn, "udn cannot be null"); + final @Nullable String thingId = getThingId(udn); + Objects.requireNonNull(thingId, "thingId cannot be null"); + return new ThingUID(thingTypeId, thingId); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialClientFactory.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialClientFactory.java new file mode 100644 index 0000000000000..0e1768e880d01 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialClientFactory.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.Objects; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.dial.models.DialClient; +import org.openhab.binding.sony.internal.dial.models.DialDeviceInfo; +import org.openhab.binding.sony.internal.dial.models.DialRoot; +import org.openhab.binding.sony.internal.dial.models.DialXmlReader; +import org.openhab.binding.sony.internal.net.HttpResponse; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class represents a factory for creating {@link DialClient} + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +public class DialClientFactory { + /** + * Attempts to retrieve the {@link DialClient} from the specified URL. Null will be returned if the URL contained an + * invalid representation + * + * @param dialUrl a non-null, non-empty URL to find + * @return the {@link DialClient} if found, null otherwise + * @throws IOException if an IO exception occurs getting the client + */ + public static @Nullable DialClient get(final String dialUrl, final ClientBuilder clientBuilder) throws IOException { + SonyUtil.validateNotEmpty(dialUrl, "dialUrl cannot be empty"); + + final Logger logger = LoggerFactory.getLogger(DialClientFactory.class); + + try { + if (dialUrl.isEmpty()) { + logger.debug("Creating default DIAL client for {}", dialUrl); + return createDefaultClient(dialUrl); + } else { + logger.debug("Querying DIAL client: {}", dialUrl); + return queryDialClient(dialUrl, logger, clientBuilder); + } + } catch (final URISyntaxException e) { + logger.debug("Malformed DIAL URL: {}", dialUrl, e); + return null; + } + } + + /** + * Private method to create a default dial client (assumes the URLs are standard) + * + * @param dialUrl a non-null, non-emtpy dial URL + * @return a non-null default {@link DialClient} + * @throws MalformedURLException if the URL was malformed + */ + private static DialClient createDefaultClient(final String dialUrl) throws MalformedURLException { + SonyUtil.validateNotEmpty(dialUrl, "dialUrl cannot be empty"); + + final String appUrl = dialUrl + "/DIAL/apps/"; + final DialDeviceInfo ddi = new DialDeviceInfo(dialUrl + "/DIAL/sony/applist", null, null); + return new DialClient(new URL(appUrl), Collections.singletonList(ddi)); + } + + /** + * Private method to create a dial client by querying it's parameters + * + * @param dialUrl a non-null, non-emtpy dial URL + * @param logger a non-null logger + * @return a possibly null (if no content can be found) {@link DialClient} + * @throws URISyntaxException if a URI exception occurred + * @throws IOException if an IO exception occurred + */ + private static @Nullable DialClient queryDialClient(final String dialUrl, final Logger logger, + final ClientBuilder clientBuilder) throws URISyntaxException, IOException { + SonyUtil.validateNotEmpty(dialUrl, "dialUrl cannot be empty"); + Objects.requireNonNull(logger, "logger cannot be null"); + + try (SonyHttpTransport transport = SonyTransportFactory.createHttpTransport(dialUrl, clientBuilder)) { + final HttpResponse resp = transport.executeGet(dialUrl); + if (resp.getHttpCode() != HttpStatus.OK_200) { + throw resp.createException(); + } + + final String content = resp.getContent(); + final DialRoot root = DialXmlReader.ROOT.fromXML(content); + if (root == null) { + logger.debug("No content found from {}: {}", dialUrl, content); + return null; + } + + final String appUrl = resp.getResponseHeader("Application-URL"); + logger.debug("Creating DIAL client: {} - {}", appUrl, content); + return new DialClient(new URL(appUrl), root.getDevices()); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialConfig.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialConfig.java new file mode 100644 index 0000000000000..df1ebb73aaae8 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialConfig.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.AbstractConfig; + +/** + * Configuration class for the {@link DialHandler} + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class DialConfig extends AbstractConfig { + /** The access code */ + private @Nullable String accessCode; + + /** + * Gets the access code + * + * @return the access code + */ + public @Nullable String getAccessCode() { + return accessCode; + } + + /** + * Sets the access code. + * + * @param accessCode the new access code + */ + public void setAccessCode(final String accessCode) { + this.accessCode = accessCode; + } + + @Override + public Map asProperties() { + final Map props = super.asProperties(); + conditionallyAddProperty(props, "accessCode", accessCode); + return props; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialConstants.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialConstants.java new file mode 100644 index 0000000000000..b11bc4927e881 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialConstants.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.type.ChannelTypeUID; + +/** + * The class provides all the constants specific to the DIAL system. + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +public class DialConstants { + /** The constant for the thing type */ + public static final ThingTypeUID THING_TYPE_DIAL = new ThingTypeUID(SonyBindingConstants.BINDING_ID, + SonyBindingConstants.DIAL_THING_TYPE_PREFIX); + + /** The constant requesting access */ + public static final String ACCESSCODE_RQST = "RQST"; + + /** The channel id constants */ + public static final ChannelTypeUID CHANNEL_TITLE_UID = new ChannelTypeUID(SonyBindingConstants.BINDING_ID, + "dialtitle"); + public static final ChannelTypeUID CHANNEL_ICON_UID = new ChannelTypeUID(SonyBindingConstants.BINDING_ID, + "dialicon"); + public static final ChannelTypeUID CHANNEL_STATE_UID = new ChannelTypeUID(SonyBindingConstants.BINDING_ID, + "dialstate"); + + /** The name that will be part of the channel identifier */ + static final String CHANNEL_TITLE = "title"; + static final String CHANNEL_ICON = "icon"; + static final String CHANNEL_STATE = "state"; + + /** The property for the channel's application id */ + static final String CHANNEL_PROP_APPLID = "applid"; +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialDiscoveryParticipant.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialDiscoveryParticipant.java new file mode 100644 index 0000000000000..d95cbad4d9139 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialDiscoveryParticipant.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial; + +import java.io.IOException; +import java.net.URL; +import java.util.Objects; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.model.meta.RemoteDevice; +import org.jupnp.model.meta.RemoteDeviceIdentity; +import org.jupnp.model.meta.RemoteService; +import org.jupnp.model.types.ServiceId; +import org.jupnp.model.types.UDN; +import org.openhab.binding.sony.internal.AbstractDiscoveryParticipant; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.binding.sony.internal.UidUtils; +import org.openhab.binding.sony.internal.dial.models.DialClient; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.providers.SonyDefinitionProvider; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * This implementation of the {@link UpnpDiscoveryParticipant} provides discovery of Sony DIAL protocol devices. + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@Component(configurationPid = "discovery.sony-dial") +public class DialDiscoveryParticipant extends AbstractDiscoveryParticipant implements UpnpDiscoveryParticipant { + + /** The clientBuilder used in HttpRequest */ + private final ClientBuilder clientBuilder; + + /** + * Creates the discovery participant + * + * @param sonyDefinitionProvider a non-null sony definition provider + */ + @Activate + public DialDiscoveryParticipant(final @Reference SonyDefinitionProvider sonyDefinitionProvider, + final @Reference ClientBuilder clientBuilder) { + super(SonyBindingConstants.DIAL_THING_TYPE_PREFIX, sonyDefinitionProvider); + this.clientBuilder = clientBuilder; + } + + @Override + protected boolean getDiscoveryEnableDefault() { + return false; + } + + @Override + public @Nullable DiscoveryResult createResult(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + + if (!isDiscoveryEnabled()) { + return null; + } + + final ThingUID uid = getThingUID(device); + if (uid == null) { + return null; + } + + final RemoteDeviceIdentity identity = device.getIdentity(); + final URL dialURL = identity.getDescriptorURL(); + + String deviceId; + try { + final DialClient dialClient = DialClientFactory.get(dialURL.toString(), clientBuilder); + if (dialClient == null || !dialClient.hasDialService()) { + logger.debug( + "DIAL device couldn't be created or didn't implement any device information - ignoring: {}", + identity); + return null; + } + + deviceId = dialClient.getFirstDeviceId(); + } catch (final IOException e) { + logger.debug("DIAL device exception {}: {}", device.getIdentity(), e.getMessage(), e); + return null; + } + + final DialConfig config = new DialConfig(); + + String macAddress = getMacAddress(identity, uid); + if (macAddress == null && NetUtil.isMacAddress(deviceId)) { + macAddress = deviceId; + } + config.setDiscoveredMacAddress(macAddress); + config.setDeviceAddress(dialURL.toString()); + + final String thingId = UidUtils.getThingId(identity.getUdn()); + return DiscoveryResultBuilder.create(uid).withProperties(config.asProperties()) + .withProperty("DialUDN", thingId != null && !thingId.isEmpty() ? thingId : uid.getId()) + .withRepresentationProperty("DialUDN").withLabel(getLabel(device, "DIAL")).build(); + } + + @Override + public @Nullable ThingUID getThingUID(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + + if (!isDiscoveryEnabled()) { + return null; + } + + if (isSonyDevice(device)) { + final String modelName = getModelName(device); + if (modelName == null || modelName.isEmpty()) { + logger.debug("Found Sony device but it has no model name - ignoring"); + return null; + } + + final RemoteService dialService = device.findService( + new ServiceId(SonyBindingConstants.DIAL_SERVICESCHEMA, SonyBindingConstants.SONY_DIALSERVICENAME)); + if (dialService != null) { + final RemoteDeviceIdentity identity = device.getIdentity(); + if (identity != null) { + final UDN udn = device.getIdentity().getUdn(); + final String thingID = UidUtils.getThingId(udn); + + if (thingID != null) { + logger.debug("Found Sony DIAL service: {}", udn); + final ThingTypeUID modelUID = getThingTypeUID(modelName); + return new ThingUID(modelUID == null ? DialConstants.THING_TYPE_DIAL : modelUID, thingID); + } + } else { + logger.debug("Found Sony DIAL service but it had no identity!"); + } + } + } + return null; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialHandler.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialHandler.java new file mode 100644 index 0000000000000..78697f5685bf7 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialHandler.java @@ -0,0 +1,230 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.AbstractThingHandler; +import org.openhab.binding.sony.internal.LoginUnsuccessfulResponse; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The thing handler for a Sony DIAL device. This is the entry point provides a full two interaction between openhab + * and a DIAL system. + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +public class DialHandler extends AbstractThingHandler { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(DialHandler.class); + + /** The protocol handler being used - will be null if not initialized. */ + private final AtomicReference<@Nullable DialProtocol> protocolHandler = new AtomicReference<>(); + + /** The clientBuilder used in HttpRequest */ + private final ClientBuilder clientBuilder; + + /** + * Constructs the handler from the {@link Thing}. + * + * @param thing a non-null {@link Thing} the handler is for + */ + public DialHandler(final Thing thing, final ClientBuilder clientBuilder) { + super(thing, DialConfig.class); + this.clientBuilder = clientBuilder; + } + + @Override + protected void handleRefreshCommand(final ChannelUID channelUID) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + + Objects.requireNonNull(channelUID, "channelId cannot be null"); + + if (getThing().getStatus() != ThingStatus.ONLINE) { + return; + } + + final DialProtocol localProtocolHandler = protocolHandler.get(); + if (localProtocolHandler == null) { + logger.debug("Trying to handle a refresh command before a protocol handler has been created"); + return; + } + + final String channelId = channelUID.getId(); + final Channel channel = getThing().getChannel(channelId); + if (channel == null) { + logger.debug("Channel wasn't found for {}", channelUID); + return; + } + + final String applId = channel.getProperties().get(DialConstants.CHANNEL_PROP_APPLID); + if (applId == null || applId.isEmpty()) { + logger.debug("Called with an empty applicationid - ignoring: {}", channelUID); + return; + } + + if (channelId.endsWith(DialConstants.CHANNEL_STATE)) { + localProtocolHandler.refreshState(channelId, applId); + } else if (channelId.endsWith(DialConstants.CHANNEL_TITLE)) { + localProtocolHandler.refreshName(channelId, applId); + } else if (channelId.endsWith(DialConstants.CHANNEL_ICON)) { + localProtocolHandler.refreshIcon(channelId, applId); + } + } + + @Override + protected void handleSetCommand(final ChannelUID channelUID, final Command command) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + final DialProtocol localProtocolHandler = protocolHandler.get(); + if (localProtocolHandler == null) { + logger.debug("Trying to handle a channel command before a protocol handler has been created"); + return; + } + + final String channelId = channelUID.getId(); + final Channel channel = getThing().getChannel(channelId); + if (channel == null) { + logger.debug("Channel wasn't found for {}", channelUID); + return; + } + + final String applId = channel.getProperties().get(DialConstants.CHANNEL_PROP_APPLID); + if (applId == null || applId.isEmpty()) { + logger.debug("Called with an empty applicationid - ignoring: {}", channelUID); + return; + } + + if (channelId.endsWith(DialConstants.CHANNEL_STATE)) { + if (command instanceof OnOffType) { + localProtocolHandler.setState(channelId, applId, OnOffType.ON == command); + } else { + logger.debug("Received a STATE channel command with a non OnOffType: {}", command); + } + } else { + logger.debug("Unknown/Unsupported Channel id: {}", channelUID); + } + } + + @Override + protected PowerCommand handlePotentialPowerOnCommand(final ChannelUID channelUID, final Command command) { + return PowerCommand.NON; + } + + @Override + protected void connect() { + final DialConfig config = getSonyConfig(); + + try { + // Validate the device URL + config.getDeviceUrl(); + } catch (final MalformedURLException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Device URL (in configuration) was missing or malformed"); + return; + } + + logger.debug("Attempting connection to DIAL device..."); + try { + SonyUtil.checkInterrupt(); + final DialProtocol localProtocolHandler = new DialProtocol<>(config, new ThingCallback() { + @Override + public void statusChanged(final ThingStatus state, final ThingStatusDetail detail, + final @Nullable String msg) { + updateStatus(state, detail, msg); + } + + @Override + public void stateChanged(final String channelId, final State newState) { + updateState(channelId, newState); + } + + @Override + public void setProperty(final String propertyName, final @Nullable String propertyValue) { + getThing().setProperty(propertyName, propertyValue); + } + }, clientBuilder); + + SonyUtil.checkInterrupt(); + final LoginUnsuccessfulResponse response = localProtocolHandler.login(); + if (response == null) { + final ThingBuilder thingBuilder = editThing(); + thingBuilder.withChannels( + DialUtil.generateChannels(getThing().getUID(), localProtocolHandler.getDialApps().values())); + updateThing(thingBuilder.build()); + + SonyUtil.checkInterrupt(); + SonyUtil.close(protocolHandler.getAndSet(localProtocolHandler)); + updateStatus(ThingStatus.ONLINE); + + SonyUtil.checkInterrupt(); + logger.debug("DIAL System now connected"); + } else { + updateStatus(ThingStatus.OFFLINE, response.getThingStatusDetail(), response.getMessage()); + } + } catch (IOException | URISyntaxException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Error connecting to DIAL device (may need to turn it on manually): " + e.getMessage()); + } catch (final InterruptedException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "Initialization was interrupted"); + } + } + + @Override + protected void refreshState(boolean initial) { + final DialProtocol protocol = protocolHandler.get(); + if (protocol != null) { + getThing().getChannels().stream().forEach(chn -> { + final String channelId = chn.getUID().getId(); + if (SonyUtil.endsWithIgnoreCase(channelId, DialConstants.CHANNEL_STATE)) { + final String applId = chn.getProperties().get(DialConstants.CHANNEL_PROP_APPLID); + if (applId == null || applId.isEmpty()) { + logger.debug("Unknown application id for channel {}", channelId); + } else { + protocol.refreshState(channelId, applId); + } + } + }); + } + } + + @Override + public void dispose() { + super.dispose(); + SonyUtil.close(protocolHandler.getAndSet(null)); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialProtocol.java new file mode 100644 index 0000000000000..541c1b81120d1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialProtocol.java @@ -0,0 +1,334 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.AccessResult; +import org.openhab.binding.sony.internal.CheckResult; +import org.openhab.binding.sony.internal.LoginUnsuccessfulResponse; +import org.openhab.binding.sony.internal.SonyAuth; +import org.openhab.binding.sony.internal.SonyAuthChecker; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.dial.models.DialApp; +import org.openhab.binding.sony.internal.dial.models.DialAppState; +import org.openhab.binding.sony.internal.dial.models.DialClient; +import org.openhab.binding.sony.internal.dial.models.DialDeviceInfo; +import org.openhab.binding.sony.internal.dial.models.DialService; +import org.openhab.binding.sony.internal.net.HttpResponse; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.openhab.binding.sony.internal.transports.TransportOptionAutoAuth; +import org.openhab.binding.sony.internal.transports.TransportOptionHeader; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the protocol handler for the DIAL System. This handler will issue the protocol commands and will + * process the responses from the DIAL system. + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + * + * @param the generic type for the callback + */ +@NonNullByDefault +class DialProtocol<@NonNull T extends ThingCallback> implements AutoCloseable { + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(DialProtocol.class); + + /** The DIAL device full address */ + private final String deviceUrlStr; + + /** The {@link ThingCallback} that we can callback to set state and status */ + private final T callback; + + /** The {@link SonyHttpTransport} used to make http requests */ + private final SonyHttpTransport transport; + + /** The {@link DialClient} representing the DIAL application */ + private final DialClient dialClient; + + /** The configuration for the dial device */ + private final DialConfig config; + + /** The authorization service */ + private final SonyAuth sonyAuth; + + /** + * Constructs the protocol handler from the configuration and callback + * + * @param config a non-null {@link DialConfig} (may be connected or disconnected) + * @param callback a non-null {@link ThingCallback} to callback + * @throws IOException if an ioexception is thrown + * @throws URISyntaxException if a uri is malformed + */ + DialProtocol(final DialConfig config, final @NonNull T callback, final ClientBuilder clientBuilder) + throws IOException, URISyntaxException { + // Confirm the address is a valid URL + final String deviceAddress = config.getDeviceAddress(); + final URL deviceURL = new URL(deviceAddress); + + this.config = config; + this.deviceUrlStr = deviceURL.toExternalForm(); + + this.callback = callback; + + transport = SonyTransportFactory.createHttpTransport(deviceUrlStr, clientBuilder); + + final DialClient dialClient = DialClientFactory.get(this.deviceUrlStr, clientBuilder); + if (dialClient == null) { + throw new IOException("DialState could not be retrieved from " + deviceAddress); + } + this.dialClient = dialClient; + + this.sonyAuth = new SonyAuth(deviceURL); + } + + /** + * Attempts to log into the system. This method will attempt to get the current applications list. If the current + * application list is forbidden, we attempt to register the device (either by registring the access code or + * requesting an access code). If we get the current application list, we simply renew our registration code. + * + * @return a non-null {@link LoginUnsuccessfulResponse} if we can't login (usually pending access) or null if the + * login was successful + * + * @throws IOException if an io exception occurs to the IRCC device + */ + @Nullable + LoginUnsuccessfulResponse login() throws IOException { + final String accessCode = config.getAccessCode(); + + transport.setOption(TransportOptionAutoAuth.FALSE); + final SonyAuthChecker authChecker = new SonyAuthChecker(transport, accessCode); + + final CheckResult checkResult = authChecker.checkResult(() -> { + for (final DialDeviceInfo info : dialClient.getDeviceInfos()) { + final String appsListUrl = info.getAppsListUrl(); + if (appsListUrl == null || appsListUrl.isEmpty()) { + return new AccessResult(Integer.toString(HttpStatus.INTERNAL_SERVER_ERROR_500), + "No application list URL to check"); + } + final HttpResponse resp = getApplicationList(appsListUrl); + if (resp.getHttpCode() == HttpStatus.FORBIDDEN_403) { + return AccessResult.NEEDSPAIRING; + } + } + return AccessResult.OK; + }); + + if (CheckResult.OK_HEADER.equals(checkResult)) { + if (accessCode == null || accessCode.isEmpty()) { + // This shouldn't happen - if our check result is OK_HEADER, then + // we had a valid (non-null, non-empty) accessCode. Unfortunately + // nullable checking thinks this can be null now. + logger.debug("This shouldn't happen - access code is blank!: {}", accessCode); + return new LoginUnsuccessfulResponse(ThingStatusDetail.CONFIGURATION_ERROR, + "Access code cannot be blank"); + } else { + SonyAuth.setupHeader(accessCode, transport); + } + } else if (CheckResult.OK_COOKIE.equals(checkResult)) { + SonyAuth.setupCookie(transport); + } else if (AccessResult.NEEDSPAIRING.equals(checkResult)) { + if (accessCode == null || accessCode.isEmpty()) { + return new LoginUnsuccessfulResponse(ThingStatusDetail.CONFIGURATION_ERROR, + "Access code cannot be blank"); + } else { + final AccessResult result = sonyAuth.requestAccess(transport, + DialConstants.ACCESSCODE_RQST.equalsIgnoreCase(accessCode) ? null : accessCode); + if (AccessResult.OK.equals(result)) { + SonyAuth.setupCookie(transport); + } else { + return new LoginUnsuccessfulResponse(ThingStatusDetail.CONFIGURATION_ERROR, result.getMsg()); + } + } + } else { + return new LoginUnsuccessfulResponse(ThingStatusDetail.CONFIGURATION_ERROR, checkResult.getMsg()); + } + + return null; + } + + /** + * Returns the callback used by this protocol + * + * @return the non-null callback used by this protocol + */ + T getCallback() { + return callback; + } + + /** + * Sets the 'state' channel for a specific application id. on to start the app, off to turn it off (off generally + * isn't supported by SONY devices - but we try as per the protocol anyway) + * + * @param channelId the non-null, non-empty channel id + * @param applId the non-null, non-empty application id + * @param start true to start, false otherwise + */ + public void setState(final String channelId, final String applId, final boolean start) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + SonyUtil.validateNotEmpty(applId, "applId cannot be empty"); + + final URL urr = NetUtil.getUrl(dialClient.getAppUrl(), applId); + if (urr == null) { + logger.debug("Could not combine {} and {}", dialClient.getAppUrl(), applId); + } else { + final HttpResponse resp = start ? transport.executePostXml(urr.toString(), "") + : transport.executeDelete(urr.toString()); + if (resp.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) { + logger.debug("Cannot start {}, another application is currently running.", applId); + } else if (resp.getHttpCode() != HttpStatus.CREATED_201) { + logger.debug("Error setting the 'state' of the application: {}", resp.getHttpCode()); + } + } + } + + /** + * Refresh state of a specific DIAL application + * + * @param channelId the non-null non-empty channel ID + * @param applId the non-null, non-empty application ID + */ + public void refreshState(final String channelId, final String applId) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + SonyUtil.validateNotEmpty(applId, "applId cannot be empty"); + + try { + final URL urr = NetUtil.getUrl(dialClient.getAppUrl(), applId); + if (urr == null) { + logger.debug("Could not combine {} and {}", dialClient.getAppUrl(), applId); + } else { + final HttpResponse resp = transport.executeGet(urr.toExternalForm()); + if (resp.getHttpCode() != HttpStatus.OK_200) { + throw resp.createException(); + } + + final DialAppState state = DialAppState.get(resp.getContent()); + if (state != null) { + callback.stateChanged(channelId, state.isRunning() ? OnOffType.ON : OnOffType.OFF); + } + } + } catch (final IOException e) { + logger.debug("Error refreshing the 'state' of the application: {}", e.getMessage()); + } + } + + /** + * Refresh the name of the application + * + * @param channelId the non-null non-empty channel ID + * @param applId the non-null application id + */ + public void refreshName(final String channelId, final String applId) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + SonyUtil.validateNotEmpty(applId, "applId cannot be empty"); + + final DialApp app = getDialApp(applId); + callback.stateChanged(channelId, SonyUtil.newStringType(app == null ? null : app.getName())); + } + + /** + * Refresh the icon for the application + * + * @param channelId the non-null non-empty channel ID + * @param applId the non-null application id + */ + public void refreshIcon(final String channelId, final String applId) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + SonyUtil.validateNotEmpty(applId, "applId cannot be empty"); + + final DialApp app = getDialApp(applId); + final String url = app == null ? null : app.getIconUrl(); + + final RawType rawType = NetUtil.getRawType(transport, url); + callback.stateChanged(channelId, rawType == null ? UnDefType.UNDEF : rawType); + } + + /** + * Helper method to get the dial application for the given application id + * + * @param appId a non-null, non-empty appication id + * @return the DialApp for the applId or null if not found + */ + private @Nullable DialApp getDialApp(final String appId) { + SonyUtil.validateNotEmpty(appId, "appId cannot be empty"); + final Map apps = getDialApps(); + return apps.get(appId); + } + + /** + * Returns the list of dial apps on the sony device + * + * @return a non-null, maybe empty list of dial apps + */ + public Map getDialApps() { + final Map apps = new HashMap<>(); + for (final DialDeviceInfo info : dialClient.getDeviceInfos()) { + final String appsListUrl = info.getAppsListUrl(); + if (appsListUrl != null && !appsListUrl.isBlank()) { + final HttpResponse appsResp = getApplicationList(appsListUrl); + if (appsResp.getHttpCode() == HttpStatus.OK_200) { + final DialService service = DialService.get(appsResp.getContent()); + if (service != null) { + service.getApps().forEach(a -> { + final String id = a.getId(); + if (id != null) { + apps.putIfAbsent(id, a); + } + }); + } + } else { + logger.debug("Exception getting dial service from {}: {}", appsListUrl, appsResp); + } + } + } + return Collections.unmodifiableMap(apps); + } + + /** + * Gets the application list for a given url + * + * @param appsListUrl a non-null, non-empty application list url + * @return the http response for the call + */ + private HttpResponse getApplicationList(final String appsListUrl) { + SonyUtil.validateNotEmpty(appsListUrl, "appsListUrl cannot be empty"); + return transport.executeGet(appsListUrl, + new TransportOptionHeader("Content-Type", "text/xml; charset=\"utf-8\"")); + } + + @Override + public void close() { + transport.close(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialUtil.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialUtil.java new file mode 100644 index 0000000000000..002c7d796822c --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/DialUtil.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.dial.models.DialApp; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.builder.ChannelBuilder; + +/** + * This is a utility class for the DIAL app + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +public class DialUtil { + /** + * Generates the list of channels for each of the dial applications + * + * @param thingUID a non-null thing UID + * @param dialApps a non-null, possibly empty list of dial applications + * @return a non-null, possibly empty list of channels + */ + static List generateChannels(final ThingUID thingUID, final Collection dialApps) { + Objects.requireNonNull(thingUID, "thingUID cannot be null"); + Objects.requireNonNull(dialApps, "dialApps cannot be null"); + + final Set cachedIds = new HashSet<>(); + + return dialApps.stream().map(da -> { + final List channels = new ArrayList<>(); + + final String applId = da.getId(); + if (applId != null && !applId.isEmpty()) { + final Map props = new HashMap<>(); + props.put(DialConstants.CHANNEL_PROP_APPLID, applId); + + // The following tries to simplify the channel names generated by + // using the part of the ApplID that is likely unique. + // + // Applids are similar to domain names in form: com.sony.netflix + // The following tries to find a unique channel name from it by .. + // 1. Starting with the last node ('netflix') + // 2. If not unique - backups up one digit ('ynetflix') + // Note: non valid characters will be ignored + // 3. If the whole id is not unique (should never happen) - starts + // to add the count to the end ('com.sony.netflix-1') + // Eventually we'll have a unique id + // + // The risk is that there is a very small chance channel ids may not be stable + // between restarts. + // Example: let say we generated a channel from 'com.sony.netflix' named 'netflix' + // Now lets say the user added a new channel with an id of 'com.malware.netflix' + // and it comes in before the original one. 'com.malware.netflix' will be assigned 'netflix' + // and the real one would be 'ynetflix'. + // Sucks but I think the risk is very very small (doubt sony would assign something like that) + // and we gain much smaller channel names as the benefit + int i = applId.lastIndexOf('.'); + String channelId = SonyUtil.createValidChannelUId(applId.substring(i)); + while (cachedIds.contains(channelId)) { + if (i <= 0) { + channelId = applId + "-" + (-(--i)); + } else { + channelId = SonyUtil.createValidChannelUId(applId.substring(--i)); + } + } + cachedIds.add(channelId); + + channels.add(ChannelBuilder + .create(new ChannelUID(thingUID, channelId + "-" + DialConstants.CHANNEL_TITLE), "String") + .withLabel(da.getName() + " Title").withProperties(props) + .withType(DialConstants.CHANNEL_TITLE_UID).build()); + channels.add(ChannelBuilder + .create(new ChannelUID(thingUID, channelId + "-" + DialConstants.CHANNEL_ICON), "Image") + .withLabel(da.getName() + " Icon").withProperties(props) + .withType(DialConstants.CHANNEL_ICON_UID).build()); + channels.add(ChannelBuilder + .create(new ChannelUID(thingUID, channelId + "-" + DialConstants.CHANNEL_STATE), "Switch") + .withLabel(da.getName() + " Status").withProperties(props) + .withType(DialConstants.CHANNEL_STATE_UID).build()); + } + return channels; + }).flatMap(List::stream).collect(Collectors.toCollection(ArrayList::new)); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialApp.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialApp.java new file mode 100644 index 0000000000000..1b86004919f6d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialApp.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial.models; + +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents a single DIAL application. The element being deserialized will typically look like: + * + *

+ * {@code
+     
+       com.sony.videoexplorer
+       Video Explorer
+       
+         start
+       
+       
+     
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +public class DialApp { + + /** The application identifier */ + private @Nullable String id; + + /** The name of the application */ + private @Nullable String name; + + /** The url to the application icon */ + @XStreamAlias("icon_url") + private @Nullable String iconUrl; + + /** The actions supported by the application */ + @XStreamAlias("supportAction") + private @Nullable SupportedAction supportedAction; + + /** + * Gets the application id + * + * @return a possibly null, possibly empty application id + */ + public @Nullable String getId() { + return id; + } + + /** + * Gets the name of the application + * + * @return a possibly null, possibly empty application name + */ + public @Nullable String getName() { + return name; + } + + /** + * Gets the application's icon URL + * + * @return a possibly null, possibly empty application icon URL + */ + public @Nullable String getIconUrl() { + return iconUrl; + } + + /** + * Gets the actions supported by the application + * + * @return the non-null, possibly empty list of application actions + */ + public List getActions() { + final SupportedAction localSupportedAction = supportedAction; + return localSupportedAction == null || localSupportedAction.actions == null ? Collections.emptyList() + : Collections.unmodifiableList(localSupportedAction.actions); + } + + /** + * Internal class used simply for deserializing the supported actions. Note: this class is not private since + * DialXmlReader needs access to the class (to process the annotations) + * + * @author Tim Roberts - Initial contribution + */ + class SupportedAction { + @XStreamImplicit(itemFieldName = "action") + private @Nullable List actions; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialAppState.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialAppState.java new file mode 100644 index 0000000000000..93556efd0e234 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialAppState.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial.models; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * This class represents the DIAL application state. The state will be retrieved from a call to {@link #get(String)} and + * the XML looks like the following + * + *
+ * {@code
+   
+   
+      com.sony.videoexplorer
+      
+      stopped
+   
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@XStreamAlias("service") +@NonNullByDefault +public class DialAppState { + + /** + * The state for running. Also valid are "stopped" and "installUrl=url" + */ + private static final String RUNNING = "running"; + + /** + * The application state. Please note that the application state has been broken by sony for quite awhile + * (everything says stopped) + */ + private @Nullable String state; + + /** + * Checks if is running. + * + * @return true, if is running + */ + public boolean isRunning() { + return RUNNING.equalsIgnoreCase(state); + } + + /** + * Get's the DIAL application state from the given content + * + * @param xml the non-null, non-empty XML + * @return a {@link DialAppState} or null if cannot be parsed + */ + public static @Nullable DialAppState get(final String xml) { + SonyUtil.validateNotEmpty(xml, "xml cannot be empty"); + return DialXmlReader.APPSTATE.fromXML(xml); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialClient.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialClient.java new file mode 100644 index 0000000000000..16d6a6b3a2b0b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialClient.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial.models; + +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents the state of a DIAL device. The DIAL state will include all the devices specified and the URL + * to access the device + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +public class DialClient { + /** The list of {@link DialDeviceInfo} */ + @XStreamImplicit + private final List deviceInfos; + + /** The url to get application state */ + private final URL appUrl; + + /** + * Constructs the instance from the specified URL and list of {@link DialDeviceInfo} + * + * @param appUrl a non-null application URL + * @param infos a non-null, possibly emply list of {@link DialDeviceInfo} + */ + public DialClient(final URL appUrl, final List infos) { + Objects.requireNonNull(appUrl, "appUrl cannot be null"); + Objects.requireNonNull(infos, "infos cannot be null"); + + this.appUrl = appUrl; + deviceInfos = Collections.unmodifiableList(infos); + } + + /** + * Returns the device application URL + * + * @return the non-null device application URL + */ + public URL getAppUrl() { + return appUrl; + } + + /** + * Checks to see if the state has any services + * + * @return true, if successful, false otherwise + */ + public boolean hasDialService() { + return !deviceInfos.isEmpty(); + } + + /** + * Returns the first device ID or null if there are no devices + * + * @return the first device ID or null + */ + public @Nullable String getFirstDeviceId() { + return deviceInfos.stream().map(e -> e.getDeviceId()).filter(e -> e != null && !e.isEmpty()).findFirst() + .orElse(null); + } + + /** + * Returns the list of device information. Likely only a single device + * + * @return a non-null, possibly empty non-modifiable list of {@link DialDeviceInfo} + */ + public List getDeviceInfos() { + return deviceInfos; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialDeviceInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialDeviceInfo.java new file mode 100644 index 0000000000000..ea0c6a7e89825 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialDeviceInfo.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial.models; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * The class representing a DIAL device and it's information. The element being deserialized will typically look like: + * + *
+ * {@code
+       
+         http://192.168.1.12:50202/appslist
+         B0:00:04:07:DD:7E
+         BDP_DIAL
+       
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@XStreamAlias("X_DIALEX_DeviceInfo") +public class DialDeviceInfo { + + /** The apps list url. */ + @XStreamAlias("X_DIALEX_AppsListURL") + private final @Nullable String appsListUrl; + + /** The device id. */ + @XStreamAlias("X_DIALEX_DeviceID") + private final @Nullable String deviceId; + + /** The device type. */ + @XStreamAlias("X_DIALEX_DeviceType") + private final @Nullable String deviceType; + + /** + * Private constructor to construct the object - only called from the {@link #withApps(List)} + * + * @param appsListUrl the possibly null, possibly empty application list URL + * @param deviceId the possibly null, possibly empty application device ID + * @param deviceType the possibly null, possibly empty application device type + */ + public DialDeviceInfo(final @Nullable String appsListUrl, final @Nullable String deviceId, + final @Nullable String deviceType) { + this.appsListUrl = appsListUrl; + this.deviceId = deviceId; + this.deviceType = deviceType; + } + + /** + * Get's the application list URL + * + * @return a possibly null, possibly empty application list URL + */ + public @Nullable String getAppsListUrl() { + return appsListUrl; + } + + /** + * Gets the device id + * + * @return a possibly null, possibly empty device id + */ + public @Nullable String getDeviceId() { + return deviceId; + } + + /** + * Gets the device type + * + * @return a possibly null, possibly empty device type + */ + public @Nullable String getDeviceType() { + return deviceType; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialRoot.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialRoot.java new file mode 100644 index 0000000000000..f800639a7234a --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialRoot.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial.models; + +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents the root element in the XML for a DIAL device. The XML that will be deserialized will look like + * + *
+ * {@code
+   
+   
+     
+       1
+       0
+     
+     
+       urn:schemas-upnp-org:device:Basic:1
+       Blu-ray Disc Player
+       Sony Corporation
+       http://www.sony.net/
+       
+       Blu-ray Disc Player
+       
+       uuid:00000004-0000-1010-8000-ac9b0ac65609
+       
+         
+           urn:dial-multiscreen-org:service:dial:1
+           urn:dial-multiscreen-org:serviceId:dial
+           /dialSCPD.xml
+           /dial/control
+           /dial/event
+         
+       
+       
+       
+         http://192.168.1.12:50202/appslist
+         B0:00:04:07:DD:7E
+         BDP_DIAL
+       
+     
+   
+ * }
+ * 
+ * + * Please note this class is used strictly in the deserialization process and retrieval of the {@link DialClient} + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@XStreamAlias("root") +public class DialRoot { + + /** The deserialized {@link DialClient} */ + @XStreamAlias("device") + private @Nullable RootDevice device; + + /** + * Get's the list of dial device infos + * + * @return a non-null list of dial clients + */ + public List getDevices() { + final RootDevice dev = device; + return dev == null || dev.deviceInfos == null ? Collections.emptyList() + : Collections.unmodifiableList(dev.deviceInfos); + } + + /** + * Internal class used simply for deserializing the device infos. Note: this class is not private since + * DialXmlReader needs access to the class (to process the annotations) + * + * @author Tim Roberts - Initial contribution + */ + class RootDevice { + @XStreamImplicit + @Nullable + private List deviceInfos; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialService.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialService.java new file mode 100644 index 0000000000000..20250e430ed43 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialService.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial.models; + +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents the root element in the XML for a DIAL service. The XML that will be deserialized will look + * like + * + *
+ * {@code
+   
+   
+     
+       com.sony.videoexplorer
+       Video Explorer
+       
+         start
+       
+       
+     
+     
+       com.sony.musicexplorer
+       Music Explorer
+       
+         start
+       
+       
+     
+     
+       com.sony.videoplayer
+       Video Player
+       
+         start
+       
+       
+     
+     ...
+   
+ * }
+ * 
+ * + * Please note this class is used strictly in the deserialization process and retrieval of the {@link DialApp} + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@XStreamAlias("service") +public class DialService { + /** The list of {@link DialApp} */ + @XStreamImplicit(itemFieldName = "app") + private @Nullable List apps; + + /** + * Creates a DialServer from the given XML or null if the representation is incorrect + * + * @param xml a non-null, non-empty XML representation + * @return A DialService or null if the XML is not valid + */ + public static @Nullable DialService get(String xml) { + SonyUtil.validateNotEmpty(xml, "xml cannot be empty"); + return DialXmlReader.SERVICE.fromXML(xml); + } + + /** + * Returns the list of {@link DialApp} for the service + * + * @return a non-null, possibly empty list of {@link DialApp} + */ + public List getApps() { + return apps == null ? Collections.emptyList() : Collections.unmodifiableList(apps); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialXmlReader.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialXmlReader.java new file mode 100644 index 0000000000000..6096e80744837 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/dial/models/DialXmlReader.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.dial.models; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyHandlerFactory; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.io.xml.StaxDriver; +import com.thoughtworks.xstream.security.NoTypePermission; + +/** + * This class represents creates the various XML readers (using XStream) to deserialize various calls. + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + * + * @param the generic type to cast the XML to + */ +@NonNullByDefault +public class DialXmlReader { + + /** The XStream instance */ + private final XStream xstream = new XStream(new StaxDriver()); + + /** The reader for the ROOT XML (see {@link DialRoot}) */ + public static final DialXmlReader ROOT = new DialXmlReader<>( + new Class[] { DialRoot.class, DialRoot.RootDevice.class, DialClient.class, DialDeviceInfo.class }); + + /** The reader for the SERVICE XML (see {@link DialService}) */ + static final DialXmlReader SERVICE = new DialXmlReader<>( + new Class[] { DialService.class, DialApp.class, DialApp.SupportedAction.class }); + + /** The reader for the APP STATE XML (see {@link DialAppState}) */ + static final DialXmlReader APPSTATE = new DialXmlReader<>(new Class[] { DialAppState.class }); + + /** + * Constructs the reader using the specified classes to process annotations with + * + * @param classes a non-null, non-empty array of classes + */ + private DialXmlReader(@SuppressWarnings("rawtypes") final Class[] classes) { + Objects.requireNonNull(classes, "classes cannot be null"); + + xstream.addPermission(NoTypePermission.NONE); + xstream.allowTypesByWildcard(new String[] { SonyHandlerFactory.class.getPackageName() + ".**" }); + xstream.setClassLoader(getClass().getClassLoader()); + xstream.ignoreUnknownElements(); + xstream.processAnnotations(classes); + } + + /** + * Will translate the XML and casts to the specified class + * + * @param xml the non-null, possibly empty XML to process + * @return the possibly null translation + */ + @SuppressWarnings("unchecked") + public @Nullable T fromXML(final String xml) { + Objects.requireNonNull(xml, "xml cannot be null"); + + if (!xml.isEmpty()) { + return (T) this.xstream.fromXML(xml); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccClientFactory.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccClientFactory.java new file mode 100644 index 0000000000000..4b15e7e647e31 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccClientFactory.java @@ -0,0 +1,350 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ircc.models.IrccActionList; +import org.openhab.binding.sony.internal.ircc.models.IrccClient; +import org.openhab.binding.sony.internal.ircc.models.IrccCodeList; +import org.openhab.binding.sony.internal.ircc.models.IrccDevice; +import org.openhab.binding.sony.internal.ircc.models.IrccRemoteCommands; +import org.openhab.binding.sony.internal.ircc.models.IrccRoot; +import org.openhab.binding.sony.internal.ircc.models.IrccSystemInformation; +import org.openhab.binding.sony.internal.ircc.models.IrccUnrDeviceInfo; +import org.openhab.binding.sony.internal.ircc.models.IrccXmlReader; +import org.openhab.binding.sony.internal.net.HttpResponse; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.openhab.binding.sony.internal.upnp.models.UpnpScpd; +import org.openhab.binding.sony.internal.upnp.models.UpnpService; +import org.openhab.binding.sony.internal.upnp.models.UpnpXmlReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The class represents a factory for creating {@link IrccClient} classes and will attempt to detect basic information + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + * + */ +@NonNullByDefault +public class IrccClientFactory { + /** The default service type for IRCC */ + public static final String SRV_IRCC_SERVICETYPE = "urn:schemas-sony-com:service:IRCC:1"; + + /** The typical IRCC values for TV and blurays */ + private static final String LIKELY_TVAVR_SCPD = "/sony/ircc/IRCCSCPD.xml"; + // private static final String LIKELY_TVAVR_IRCC = "/sony/ircc"; + private static final String LIKELY_TVAVR_IRCC = "/sony/IRCC"; + private static final int LIKELY_BLURAY_PORT = 50001; + private static final String LIKELY_BLURAY_SCPD = "/IRCCSCPD.xml"; + private static final String LIKELY_BLURAY_IRCC = "/upnp/control/IRCC"; + private static final String LIKELY_SCPD_RESULT = "X_SendIRCCIRCCCodeinX_A_ARG_TYPE_IRCCCode"; + + /** + * Creates an {@link IrccClient} for the given URL. If the URL is a basic URL (ie host name only), will attempt to + * detect the correct settings + * + * @param irccUrl a non-null, non-empty IRCC URL + * @return a non-null {@link IrccClient} + * @throws IOException if an IO exception occurs + * @throws URISyntaxException if a URL has an incorrect syntax + */ + public static IrccClient get(final String irccUrl, final ClientBuilder clientBuilder) + throws IOException, URISyntaxException { + SonyUtil.validateNotEmpty(irccUrl, "irccUrl cannot be empty"); + return get(new URL(irccUrl), clientBuilder); + } + + /** + * Instantiates a new IRCC client give the IRCC URL + * + * @param irccUrl the non-null IRCC URL + * @throws IOException if an IO exception occurs getting information from the client + * @throws URISyntaxException if a URL has an incorrect syntax + */ + public static IrccClient get(final URL irccUrl, final ClientBuilder clientBuilder) + throws IOException, URISyntaxException { + Objects.requireNonNull(irccUrl, "irccUrl cannot be null"); + + final Logger logger = LoggerFactory.getLogger(IrccClientFactory.class); + + if (irccUrl.getPath().isEmpty()) { + return getDefaultClient(irccUrl, logger, clientBuilder); + } else { + try { + return queryIrccClient(irccUrl, logger, clientBuilder); + } catch (final IOException | URISyntaxException e) { + logger.debug("Exception occurred querying IRCC client - trying default client: {}", e.getMessage(), e); + return getDefaultClient(irccUrl, logger, clientBuilder); + } + } + } + + /** + * Helper method to generate a default {@link IrccClient}. A 'default' client is one where we try to detect and use + * standard URLs/ports + * + * @param irccUrl a non-null IRCC url + * @param logger a non-null logger + * @return a non-null {@link IrccClient} + * @throws URISyntaxException if a URL has an inccorect syntax + * @throws MalformedURLException if a URL is malformed + */ + private static IrccClient getDefaultClient(final URL irccUrl, final Logger logger, + final ClientBuilder clientBuilder) throws URISyntaxException, MalformedURLException { + Objects.requireNonNull(irccUrl, "irccUrl cannot be null"); + Objects.requireNonNull(logger, "logger cannot be null"); + + logger.debug("Creating default IRCC client for {}", irccUrl); + + final IrccActionList actions = new IrccActionList(); + final IrccSystemInformation sysInfo = new IrccSystemInformation(); + final IrccRemoteCommands remoteCommands = new IrccRemoteCommands(); + final IrccUnrDeviceInfo irccDeviceInfo = new IrccUnrDeviceInfo(); + + final Map services = new HashMap<>(); + final Map scpdByService = new HashMap<>(); + + URL baseUrl = irccUrl; + + logger.debug("Testing Default IRCC client to see if it's a TV/AVR or BLURAY: {}{}", baseUrl, LIKELY_TVAVR_SCPD); + try (SonyHttpTransport transport = SonyTransportFactory.createHttpTransport(baseUrl, clientBuilder)) { + final HttpResponse tvavr = transport.executeGet(new URL(baseUrl, LIKELY_TVAVR_SCPD).toExternalForm()); + + String irccScpdResponse = null; + if (tvavr.getHttpCode() == HttpStatus.OK_200) { + logger.debug("Default IRCC client likely a TV/AVR: {}{}", baseUrl, LIKELY_TVAVR_IRCC); + services.put(IrccClient.SRV_IRCC, new UpnpService(IrccClient.SRV_IRCC, SRV_IRCC_SERVICETYPE, + LIKELY_TVAVR_SCPD, LIKELY_TVAVR_IRCC)); + + irccScpdResponse = tvavr.getContent(); + } else { + final URL blurayURL = new URL(baseUrl.getProtocol(), baseUrl.getHost(), LIKELY_BLURAY_PORT, + LIKELY_BLURAY_SCPD); + + logger.debug("Default IRCC client may not be a TV/AVR - trying BLURAY: {}", blurayURL); + final HttpResponse bluray = transport.executeGet(blurayURL.toExternalForm()); + if (bluray.getHttpCode() == HttpStatus.OK_200) { + logger.debug("Default IRCC client likely a BLURAY: {}{}", baseUrl, LIKELY_BLURAY_IRCC); + irccScpdResponse = bluray.getContent(); + services.put(IrccClient.SRV_IRCC, new UpnpService(IrccClient.SRV_IRCC, SRV_IRCC_SERVICETYPE, + LIKELY_BLURAY_SCPD, LIKELY_BLURAY_IRCC)); + + baseUrl = blurayURL; // override to get the port + } + } + + if (irccScpdResponse != null && !irccScpdResponse.isEmpty()) { + logger.debug("Default IRCC client using SCPD: {}", irccScpdResponse); + final UpnpScpd scpd = UpnpXmlReader.SCPD.fromXML(irccScpdResponse); + if (scpd != null) { + scpdByService.put(IrccClient.SRV_IRCC, scpd); + } + } + } + + if (services.isEmpty()) { + logger.debug("Default IRCC detection failed - assuming TV/AVR: {}{}", baseUrl, LIKELY_TVAVR_IRCC); + services.put(IrccClient.SRV_IRCC, + new UpnpService(IrccClient.SRV_IRCC, SRV_IRCC_SERVICETYPE, LIKELY_TVAVR_SCPD, LIKELY_TVAVR_IRCC)); + } + + if (scpdByService.isEmpty()) { + logger.debug("Default SCPD detection failed - assuming result: {}", LIKELY_SCPD_RESULT); + final UpnpScpd scpd = Objects.requireNonNull(UpnpXmlReader.SCPD.fromXML(LIKELY_SCPD_RESULT)); + scpdByService.put(IrccClient.SRV_IRCC, scpd); + } + + return new IrccClient(baseUrl, services, actions, sysInfo, remoteCommands, irccDeviceInfo, scpdByService); + } + + /** + * Helper method to create a {@link IrccClient} from a URL discovered via UPNP + * + * @param irccUrl a non-null URL pointing to the UPNP description + * @param logger a non-null logger + * @return a non-null {@link IrccClient} + * @throws URISyntaxException if a URL has an inccorect syntax + * @throws MalformedURLException if a URL is malformed + */ + private static IrccClient queryIrccClient(final URL irccUrl, final Logger logger, final ClientBuilder clientBuilder) + throws IOException, URISyntaxException { + Objects.requireNonNull(irccUrl, "irccUrl cannot be null"); + Objects.requireNonNull(logger, "logger cannot be null"); + + try (SonyHttpTransport transport = SonyTransportFactory.createHttpTransport(irccUrl, clientBuilder)) { + logger.debug("Querying IRCC client {}", irccUrl); + final HttpResponse resp = transport.executeGet(irccUrl.toExternalForm()); + if (resp.getHttpCode() != HttpStatus.OK_200) { + throw resp.createException(); + } + + final String irccResponse = resp.getContent(); + final IrccRoot irccRoot = IrccXmlReader.ROOT.fromXML(irccResponse); + if (irccRoot == null) { + throw new IOException("IRCC response (" + irccUrl + ") was not valid: " + irccResponse); + } + logger.debug("Querying IRCC client {} and got IRCCRoot response: {}", irccUrl, irccResponse); + + final IrccDevice irccDevice = irccRoot.getDevice(); + if (irccDevice == null) { + throw new IOException("IRCC response (" + irccUrl + ") didn't contain an IRCC device"); + } + + final Map services = new HashMap<>(); + final Map scpdByService = new HashMap<>(); + + for (final UpnpService service : irccDevice.getServices()) { + final String serviceId = service.getServiceId(); + + if (serviceId == null || serviceId.isEmpty()) { + logger.debug("Querying IRCC client {} and found a service with no service id - ignoring: {}", + irccUrl, service); + continue; + } + + logger.debug("Querying IRCC client {} and found service: {} -- {}", irccUrl, serviceId, service); + services.put(serviceId, service); + + final URL scpdUrl = service.getScpdUrl(irccUrl); + if (scpdUrl != null) { + logger.debug("Querying IRCC client {} -- {} and getting SCPD: {}", irccUrl, serviceId, scpdUrl); + final HttpResponse spcdResponse = transport.executeGet(scpdUrl.toExternalForm()); + + final int httpCode = spcdResponse.getHttpCode(); + if (httpCode == HttpStatus.NOT_FOUND_404) { + logger.debug("Querying IRCC client {} -- {} -- {} -- wasn't found - skipping", irccUrl, + serviceId, scpdUrl); + continue; + } else if (spcdResponse.getHttpCode() != HttpStatus.OK_200) { + throw spcdResponse.createException(); + } + + final String scpdResponse = spcdResponse.getContent(); + final UpnpScpd scpd = UpnpXmlReader.SCPD.fromXML(scpdResponse); + if (scpd == null) { + logger.debug("spcd url '{}' didn't contain a valid response (and is being ignored): {}", + scpdUrl, spcdResponse); + } else { + logger.debug("Querying IRCC client {} -- {} and adding SCPD: {} -- {}", irccUrl, serviceId, + scpdUrl, scpd); + scpdByService.put(serviceId, scpd); + } + } + } + + final IrccUnrDeviceInfo unrDeviceInfo = irccDevice.getUnrDeviceInfo(); + final IrccUnrDeviceInfo irccDeviceInfo = unrDeviceInfo == null ? new IrccUnrDeviceInfo() : unrDeviceInfo; + + final String actionsUrl = irccDeviceInfo.getActionListUrl(); + + IrccActionList actionsList; + IrccSystemInformation sysInfo; + + // If empty - likely version 1.0 or 1.1 + if (actionsUrl == null || actionsUrl.isEmpty()) { + logger.debug("Querying IRCC client {} and found no actionsUrl - generating default", irccUrl); + actionsList = new IrccActionList(); + sysInfo = new IrccSystemInformation(); + } else { + logger.debug("Querying IRCC client {} and finding action: {}", irccUrl, actionsUrl); + final HttpResponse actionsResp = transport.executeGet(actionsUrl); + if (actionsResp.getHttpCode() == HttpStatus.OK_200) { + final String actionXml = actionsResp.getContent(); + final IrccActionList actionList = IrccXmlReader.ACTIONS.fromXML(actionXml); + if (actionList == null) { + throw new IOException( + "IRCC Actions response (" + actionsUrl + ") was not valid: " + actionXml); + } + logger.debug("Querying IRCC client {} and found action: {} -- {}", irccUrl, actionsUrl, actionList); + actionsList = actionList; + } else { + logger.debug("Querying IRCC client {} for actions url {} -- got error {} and defaulting to none", + irccUrl, actionsUrl, actionsResp.getHttpCode()); + actionsList = new IrccActionList(); + } + + final String sysUrl = actionsList.getUrlForAction(IrccClient.AN_GETSYSTEMINFORMATION); + if (sysUrl == null || sysUrl.isEmpty()) { + logger.debug("Querying IRCC client {} but found no system information actions URL: {} - defaulting", + irccUrl, actionsList); + sysInfo = new IrccSystemInformation(); + } else { + logger.debug("Querying IRCC client {} and getting system information: {}", irccUrl, sysUrl); + final HttpResponse sysResp = transport.executeGet(sysUrl); + if (sysResp.getHttpCode() == HttpStatus.OK_200) { + final String sysXml = sysResp.getContent(); + final IrccSystemInformation sys = IrccXmlReader.SYSINFO.fromXML(sysXml); + if (sys == null) { + throw new IOException( + "IRCC systems info response (" + sysUrl + ") was not valid: " + sysXml); + } + logger.debug("Querying IRCC client {} and found system information: {} -- {}", irccUrl, sysUrl, + sys); + sysInfo = sys; + } else { + logger.debug( + "Querying IRCC client {} for sysinfo url {} -- got error {} and defaulitn system information", + irccUrl, sysUrl, sysResp.getHttpCode()); + sysInfo = new IrccSystemInformation(); + } + } + } + + IrccRemoteCommands remoteCommands; + + final IrccCodeList codeList = irccDevice.getCodeList(); + final String remoteCommandsUrl = actionsList.getUrlForAction(IrccClient.AN_GETREMOTECOMMANDLIST); + if (remoteCommandsUrl == null || remoteCommandsUrl.isEmpty()) { + logger.debug("Querying IRCC client {} and found no remote commands - using default code list", irccUrl); + remoteCommands = new IrccRemoteCommands().withCodeList(codeList); + } else { + logger.debug("Querying IRCC client {} and getting remote commands: {}", irccUrl, remoteCommandsUrl); + final HttpResponse rcResp = transport.executeGet(remoteCommandsUrl); + if (rcResp.getHttpCode() == HttpStatus.OK_200) { + final String rcXml = rcResp.getContent(); + final IrccRemoteCommands rcCmds = IrccXmlReader.REMOTECOMMANDS.fromXML(rcXml); + if (rcCmds == null) { + throw new IOException( + "IRCC systems info response (" + remoteCommandsUrl + ") was not valid: " + rcXml); + } + logger.debug("Querying IRCC client {} and getting remote commands: {} -- {}", irccUrl, + remoteCommandsUrl, rcCmds); + remoteCommands = rcCmds; + } else { + logger.debug( + "Querying IRCC client {} and encountered an error getting remote commands (using default now): {}", + irccUrl, rcResp); + remoteCommands = new IrccRemoteCommands().withCodeList(codeList); + } + } + + return new IrccClient(irccUrl, services, actionsList, sysInfo, remoteCommands, irccDeviceInfo, + scpdByService); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccConfig.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccConfig.java new file mode 100644 index 0000000000000..cc14d2d81f67f --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccConfig.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc; + +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.AbstractConfig; + +/** + * Configuration class for the {@link IrccHandler}. + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +public class IrccConfig extends AbstractConfig { + /** The access code */ + private @Nullable String accessCode; + + /** The commands map file */ + private @Nullable String commandsMapFile; + + // ---- the following properties are not part of the config.xml (and are properties) ---- + + /** The commands map file. */ + private @Nullable String discoveredCommandsMapFile; + + /** + * Gets the access code + * + * @return the access code + */ + public @Nullable String getAccessCode() { + return accessCode; + } + + /** + * Sets the access code. + * + * @param accessCode the new access code + */ + public void setAccessCode(final String accessCode) { + this.accessCode = accessCode; + } + + /** + * Gets the commands map file. + * + * @return the commands map file + */ + public @Nullable String getCommandsMapFile() { + final @Nullable String mapFile = commandsMapFile; + return mapFile != null && !mapFile.isEmpty() ? mapFile : discoveredCommandsMapFile; + } + + /** + * Sets the commands map file. + * + * @param commandsMapFile the new commands map file + */ + public void setCommandsMapFile(final String commandsMapFile) { + this.commandsMapFile = commandsMapFile; + } + + /** + * Sets the discovered commands map file. + * + * @param discoveredCommandsMapFile the new commands map file + */ + public void setDiscoveredCommandsMapFile(final String discoveredCommandsMapFile) { + this.discoveredCommandsMapFile = discoveredCommandsMapFile; + } + + @Override + public Map asProperties() { + final Map props = super.asProperties(); + + props.put("discoveredCommandsMapFile", Objects.requireNonNullElse(discoveredCommandsMapFile, "")); + + conditionallyAddProperty(props, "accessCode", accessCode); + conditionallyAddProperty(props, "commandsMapFile", commandsMapFile); + return props; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccConstants.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccConstants.java new file mode 100644 index 0000000000000..b60a9a82c97a4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccConstants.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The class provides all the constants specific to the Ircc system. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class IrccConstants { + + // The thing constants + public static final ThingTypeUID THING_TYPE_IRCC = new ThingTypeUID(SonyBindingConstants.BINDING_ID, + SonyBindingConstants.IRCC_THING_TYPE_PREFIX); + static final String ACCESSCODE_RQST = "RQST"; + static final String GRP_PRIMARY = "primary"; + static final String GRP_VIEWING = "viewing"; + static final String GRP_CONTENT = "content"; + + // All the channel constants + static final String PROP_VERSION = "IRCC Version"; + static final String PROP_REGISTRATIONMODE = "Registration Mode"; + static final String CHANNEL_POWER = "power"; + static final String CHANNEL_CMD = "command"; + static final String CHANNEL_CONTENTURL = "contenturl"; + static final String CHANNEL_TEXT = "textfield"; + static final String CHANNEL_INTEXT = "intext"; + static final String CHANNEL_INBROWSER = "inbrowser"; + static final String CHANNEL_ISVIEWING = "isviewing"; + static final String CHANNEL_ID = "id"; + static final String CHANNEL_TITLE = "title"; + static final String CHANNEL_CLASS = "class"; + static final String CHANNEL_SOURCE = "source"; + static final String CHANNEL_SOURCE2 = "zone2source"; + static final String CHANNEL_MEDIATYPE = "mediatype"; + static final String CHANNEL_MEDIAFORMAT = "mediaformat"; + static final String CHANNEL_EDITION = "edition"; + static final String CHANNEL_DESCRIPTION = "description"; + static final String CHANNEL_GENRE = "genre"; + static final String CHANNEL_DURATION = "duration"; + static final String CHANNEL_RATING = "rating"; + static final String CHANNEL_DATERELEASE = "daterelease"; + static final String CHANNEL_DIRECTOR = "director"; + static final String CHANNEL_PRODUCER = "producer"; + static final String CHANNEL_SCREENWRITER = "screenwriter"; + static final String CHANNEL_ICONDATA = "image"; +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccDiscoveryParticipant.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccDiscoveryParticipant.java new file mode 100644 index 0000000000000..752ef8905d73f --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccDiscoveryParticipant.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Objects; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.model.meta.RemoteDevice; +import org.jupnp.model.meta.RemoteDeviceIdentity; +import org.jupnp.model.meta.RemoteService; +import org.jupnp.model.types.ServiceId; +import org.jupnp.model.types.UDN; +import org.openhab.binding.sony.internal.AbstractDiscoveryParticipant; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.binding.sony.internal.UidUtils; +import org.openhab.binding.sony.internal.ircc.models.IrccClient; +import org.openhab.binding.sony.internal.ircc.models.IrccSystemInformation; +import org.openhab.binding.sony.internal.providers.SonyDefinitionProvider; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * This implementation of the {@link UpnpDiscoveryParticipant} provides discovery of Sony IRCC protocol devices. + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@Component(configurationPid = "discovery.sony-ircc") +public class IrccDiscoveryParticipant extends AbstractDiscoveryParticipant implements UpnpDiscoveryParticipant { + /** + * The clientBuilder used in HttpRequest + */ + private final ClientBuilder clientBuilder; + + /** + * Constructs the participant + * + * @param sonyDefinitionProvider a non-null sony definition provider + */ + @Activate + public IrccDiscoveryParticipant(final @Reference SonyDefinitionProvider sonyDefinitionProvider, + final @Reference ClientBuilder clientBuilder) { + super(SonyBindingConstants.IRCC_THING_TYPE_PREFIX, sonyDefinitionProvider); + this.clientBuilder = clientBuilder; + } + + @Override + protected boolean getDiscoveryEnableDefault() { + return false; + } + + @Override + public @Nullable DiscoveryResult createResult(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + + if (!isDiscoveryEnabled()) { + return null; + } + + final ThingUID uid = getThingUID(device); + if (uid == null) { + return null; + } + + final RemoteDeviceIdentity identity = device.getIdentity(); + final URL irccURL = identity.getDescriptorURL(); + + String sysWolAddress = null; + + try { + final IrccClient irccClient = IrccClientFactory.get(irccURL, clientBuilder); + final IrccSystemInformation systemInformation = irccClient.getSystemInformation(); + sysWolAddress = systemInformation.getWolMacAddress(); + } catch (IOException | URISyntaxException e) { + logger.debug("Exception getting device info: {}", e.getMessage(), e); + return null; + } + + final IrccConfig config = new IrccConfig(); + config.setDiscoveredCommandsMapFile("ircc-" + uid.getId() + ".map"); + config.setDiscoveredMacAddress( + sysWolAddress != null && !sysWolAddress.isEmpty() ? sysWolAddress : getMacAddress(identity, uid)); + + config.setDeviceAddress(irccURL.toString()); + + final String thingId = UidUtils.getThingId(identity.getUdn()); + return DiscoveryResultBuilder.create(uid).withProperties(config.asProperties()) + .withProperty("IrccUDN", thingId != null && !thingId.isEmpty() ? thingId : uid.getId()) + .withRepresentationProperty("IrccUDN").withLabel(getLabel(device, "IRCC")).build(); + } + + @Override + public @Nullable ThingUID getThingUID(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + + if (!isDiscoveryEnabled()) { + return null; + } + + if (isSonyDevice(device)) { + final String modelName = getModelName(device); + if (modelName == null || modelName.isEmpty()) { + logger.debug("Found Sony device but it has no model name - ignoring"); + return null; + } + + final RemoteService irccService = device.findService( + new ServiceId(SonyBindingConstants.SONY_SERVICESCHEMA, SonyBindingConstants.SONY_IRCCSERVICENAME)); + if (irccService != null) { + final RemoteDeviceIdentity identity = device.getIdentity(); + if (identity != null) { + final UDN udn = device.getIdentity().getUdn(); + logger.debug("Found Sony IRCC service: {}", udn); + final ThingTypeUID modelUID = getThingTypeUID(modelName); + return UidUtils.createThingUID(modelUID == null ? IrccConstants.THING_TYPE_IRCC : modelUID, udn); + } else { + logger.debug("Found Sony IRCC service but it had no identity!"); + } + } else { + logger.debug("Could not find the IRCC service for device: {}", device); + } + } + return null; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccHandler.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccHandler.java new file mode 100644 index 0000000000000..28b94af6ddeaf --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccHandler.java @@ -0,0 +1,302 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.AbstractThingHandler; +import org.openhab.binding.sony.internal.LoginUnsuccessfulResponse; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.transform.TransformationService; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The thing handler for a Sony Ircc device. This is the entry point provides a full two interaction between openhab + * and the ircc system. + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3, power on command handling + */ +@NonNullByDefault +public class IrccHandler extends AbstractThingHandler { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(IrccHandler.class); + + /** The protocol handler being used - will be null if not initialized. */ + private final AtomicReference<@Nullable IrccProtocol> protocolHandler = new AtomicReference<>(); + + /** The transformation service to use to transform the MAP file */ + private final @Nullable TransformationService transformationService; + + /** The clientBuilder used in HttpRequest */ + private final ClientBuilder clientBuilder; + + /** + * Constructs the handler from the {@link Thing} and {@link TransformationService} + * + * @param thing a non-null {@link Thing} the handler is for + * @param transformationService a possibly null {@link TransformationService} to use to transform MAP file + */ + public IrccHandler(final Thing thing, final @Nullable TransformationService transformationService, + final ClientBuilder clientBuilder) { + super(thing, IrccConfig.class); + + Objects.requireNonNull(thing, "thing cannot be null"); + this.transformationService = transformationService; + this.clientBuilder = clientBuilder; + } + + @Override + protected void handleRefreshCommand(final ChannelUID channelUID) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + + if (getThing().getStatus() != ThingStatus.ONLINE) { + return; + } + + final IrccProtocol localProtocolHandler = protocolHandler.get(); + if (localProtocolHandler == null) { + logger.debug("Trying to handle a refresh command before a protocol handler has been created"); + return; + } + + final String groupId = channelUID.getGroupId(); + final String channelId = channelUID.getIdWithoutGroup(); + + if (groupId == null) { + logger.debug("Called with a null group id - ignoring"); + return; + } + + switch (groupId) { + case IrccConstants.GRP_PRIMARY: + switch (channelId) { + case IrccConstants.CHANNEL_CONTENTURL: + localProtocolHandler.refreshContentUrl(); + break; + case IrccConstants.CHANNEL_TEXT: + localProtocolHandler.refreshText(); + break; + case IrccConstants.CHANNEL_INTEXT: + localProtocolHandler.refreshInText(); + break; + case IrccConstants.CHANNEL_INBROWSER: + localProtocolHandler.refreshInBrowser(); + break; + case IrccConstants.CHANNEL_ISVIEWING: + localProtocolHandler.refreshIsViewing(); + break; + default: + break; + } + break; + + case IrccConstants.GRP_VIEWING: + localProtocolHandler.refreshStatus(); + break; + + case IrccConstants.GRP_CONTENT: + localProtocolHandler.refreshContentInformation(); + break; + default: + break; + } + } + + @Override + protected void handleSetCommand(final ChannelUID channelUID, final Command command) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + final IrccProtocol localProtocolHandler = protocolHandler.get(); + if (localProtocolHandler == null) { + logger.debug("Trying to handle a channel command before a protocol handler has been created"); + return; + } + + final String groupId = channelUID.getGroupId(); + final String channelId = channelUID.getIdWithoutGroup(); + + if (groupId == null) { + logger.debug("Called with a null group id - ignoring"); + return; + } + + switch (groupId) { + case IrccConstants.GRP_PRIMARY: + switch (channelId) { + case IrccConstants.CHANNEL_CMD: + if (command instanceof StringType) { + if (command.toString().isEmpty()) { + logger.debug("Received a COMMAND channel command that is empty - ignoring"); + } else { + localProtocolHandler.sendCommand(command.toString()); + } + } else { + logger.debug("Received a COMMAND channel command with a non StringType: {}", command); + } + break; + + case IrccConstants.CHANNEL_POWER: + if (command instanceof OnOffType) { + localProtocolHandler.sendPower(OnOffType.ON == command); + } else { + logger.debug("Received a POWER channel command with a non OnOffType: {}", command); + } + break; + case IrccConstants.CHANNEL_CONTENTURL: + if (command instanceof StringType) { + if (command.toString().isEmpty()) { + logger.debug("Received a CONTENTURL channel command that is empty - ignoring"); + } else { + localProtocolHandler.sendContentUrl(command.toString()); + } + } else { + logger.debug("Received a CONTENTURL channel command with a non StringType: {}", command); + } + break; + case IrccConstants.CHANNEL_TEXT: + if (command instanceof StringType) { + if (command.toString().isEmpty()) { + logger.debug("Received a TEXT channel command that is empty - ignoring"); + } else { + localProtocolHandler.sendText(command.toString()); + } + } else { + logger.debug("Received a TEXT channel command with a non StringType: {}", command); + } + break; + default: + logger.debug("Unknown/Unsupported Primary Channel id: {}", channelId); + break; + } + + break; + + case IrccConstants.GRP_VIEWING: + logger.debug("Unknown/Unsupported Viewing Channel id: {}", channelId); + break; + + case IrccConstants.GRP_CONTENT: + logger.debug("Unknown/Unsupported Content Channel id: {}", channelId); + break; + default: + break; + } + } + + @Override + protected PowerCommand handlePotentialPowerOnCommand(final ChannelUID channelUID, final Command command) { + final String groupId = channelUID.getGroupId(); + final String channelId = channelUID.getIdWithoutGroup(); + + if (groupId != null) { + if (IrccConstants.GRP_PRIMARY.equals(groupId) && IrccConstants.CHANNEL_POWER.equals(channelId)) { + if (command instanceof OnOffType) { + if (command == OnOffType.ON) { + SonyUtil.sendWakeOnLan(logger, getSonyConfig().getDeviceIpAddress(), + getSonyConfig().getDeviceMacAddress()); + return PowerCommand.ON; + } else { + return PowerCommand.OFF; + } + } + } + } + return PowerCommand.OFF; + } + + @Override + protected void refreshState(boolean initial) { + final IrccProtocol localProtocolHandler = protocolHandler.get(); + if (localProtocolHandler != null) { + localProtocolHandler.refreshState(); + } + } + + @Override + protected void connect() { + final IrccConfig config = getSonyConfig(); + final @Nullable String deviceAddress = config.getDeviceAddress(); + if (deviceAddress == null || deviceAddress.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "IRCC URL is missing from configuration"); + return; + } + + logger.debug("Attempting connection to IRCC device..."); + try { + SonyUtil.checkInterrupt(); + final IrccProtocol localProtocolHandler = new IrccProtocol<>(config, transformationService, + new ThingCallback() { + @Override + public void statusChanged(final ThingStatus state, final ThingStatusDetail detail, + final @Nullable String msg) { + updateStatus(state, detail, msg); + } + + @Override + public void stateChanged(final String channelId, final State newState) { + updateState(channelId, newState); + } + + @Override + public void setProperty(final String propertyName, final @Nullable String propertyValue) { + getThing().setProperty(propertyName, propertyValue); + } + }, clientBuilder); + + protocolHandler.set(localProtocolHandler); + + SonyUtil.checkInterrupt(); + final LoginUnsuccessfulResponse response = localProtocolHandler.login(); + if (response == null) { + updateStatus(ThingStatus.ONLINE); + SonyUtil.checkInterrupt(); + logger.debug("IRCC System now connected"); + } else { + updateStatus(ThingStatus.OFFLINE, response.getThingStatusDetail(), response.getMessage()); + } + } catch (IOException | ProcessingException | URISyntaxException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Error connecting to IRCC device (may need to turn it on manually): " + e.getMessage()); + } catch (final InterruptedException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "Initialization was interrupted"); + } + } + + @Override + public void dispose() { + super.dispose(); + SonyUtil.close(protocolHandler.getAndSet(null)); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccProtocol.java new file mode 100644 index 0000000000000..e8ee10c0125a4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/IrccProtocol.java @@ -0,0 +1,838 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.AccessResult; +import org.openhab.binding.sony.internal.CheckResult; +import org.openhab.binding.sony.internal.LoginUnsuccessfulResponse; +import org.openhab.binding.sony.internal.SonyAuth; +import org.openhab.binding.sony.internal.SonyAuthChecker; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.ircc.models.IrccClient; +import org.openhab.binding.sony.internal.ircc.models.IrccContentInformation; +import org.openhab.binding.sony.internal.ircc.models.IrccContentUrl; +import org.openhab.binding.sony.internal.ircc.models.IrccRemoteCommand; +import org.openhab.binding.sony.internal.ircc.models.IrccRemoteCommands; +import org.openhab.binding.sony.internal.ircc.models.IrccStatus; +import org.openhab.binding.sony.internal.ircc.models.IrccStatusItem; +import org.openhab.binding.sony.internal.ircc.models.IrccStatusList; +import org.openhab.binding.sony.internal.ircc.models.IrccText; +import org.openhab.binding.sony.internal.ircc.models.IrccUnrDeviceInfo; +import org.openhab.binding.sony.internal.net.HttpResponse; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.openhab.binding.sony.internal.transports.TransportOptionAutoAuth; +import org.openhab.core.OpenHAB; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.transform.TransformationException; +import org.openhab.core.transform.TransformationService; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the protocol handler for the IRCC System. This handler will issue the protocol commands and will + * process the responses from the IRCC system. The IRCC system is a little flacky and doesn't seem to handle + * multiple commands in a single session. For this reason, we create a single {@link SocketSession} to listen for any + * notifications (whose lifetime matches that of this handler) and then create separate {@link SocketSession} for each + * request. Special care must be taken to differentiate between a Control request result and the Enquiry/Notification + * results to avoid misinterpreting the result (the control "success" message will have all zeroes - which has a form + * that matches some enquery/notification results (like volume could be interpreted as 0!). + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + * + * @param the generic type for the callback + */ +@NonNullByDefault +class IrccProtocol<@NonNull T extends ThingCallback> implements AutoCloseable { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(IrccProtocol.class); + + /** The reference to the associated {@link IrccConfig} */ + private final IrccConfig config; + + /** The callback that we use to (ehm) callback */ + private final T callback; + + /** The {@link SonyHttpTransport} to use */ + private final SonyHttpTransport transport; + + /** The transform service to use to transform commands with */ + private final @Nullable TransformationService transformService; + + /** The {@link IrccClient} to use */ + private final IrccClient irccClient; + + // ---------------------- The following variables are state variables ------------------ + /** Whether the devices is in a text field or not */ + private final AtomicBoolean isInText = new AtomicBoolean(false); + + /** Whether the device is in a web browser or not */ + private final AtomicBoolean isInWebBrowse = new AtomicBoolean(false); + + /** Whether the device is viewing some content */ + private final AtomicBoolean isViewing = new AtomicBoolean(false); + + /** If viewing content, the current content identifier */ + private final AtomicReference<@Nullable String> contentId = new AtomicReference<>(); + + /** The authorization service */ + private final SonyAuth sonyAuth; + + /** + * Constructs the protocol handler from given parameters. + * + * @param config a non-null {@link IrccConfig} + * @param transformService a possibly null {@link TransformationService} to use + * @param callback a non-null {@link ThingCallback} to use as a callback + * @throws IOException if an io exception occurs to the IRCC device + */ + IrccProtocol(final IrccConfig config, final @Nullable TransformationService transformService, + final @NonNull T callback, final ClientBuilder clientBuilder) throws IOException, URISyntaxException { + this.config = config; + this.callback = callback; + + this.transformService = transformService; + + this.irccClient = IrccClientFactory.get(config.getDeviceUrl(), clientBuilder); + this.transport = SonyTransportFactory.createHttpTransport(irccClient.getBaseUrl().toExternalForm(), + clientBuilder); + this.sonyAuth = new SonyAuth(() -> irccClient); + } + + /** + * Gets the callback being using by the protocol + * + * @return the non-null callback + */ + T getCallback() { + return callback; + } + + /** + * Attempts to log into the system. This method will attempt to get the current status. If the current status is + * forbidden, we attempt to register the device (either by registring the access code or requesting an access code). + * If we get the current state, we simply renew our registration code. + * + * @return a non-null {@link LoginUnsuccessfulResponse} if we can't login (usually pending access) or null if the + * login was successful + * + * @throws IOException if an io exception occurs to the IRCC device + */ + @Nullable + LoginUnsuccessfulResponse login() throws IOException { + logger.debug("Starting ircc login..."); + transport.setOption(TransportOptionAutoAuth.FALSE); + + final String accessCode = config.getAccessCode(); + logger.debug("accessCode = '{}'", SonyUtil.defaultIfEmpty(accessCode, "")); + final SonyAuthChecker authChecker = new SonyAuthChecker(transport, accessCode); + final CheckResult checkResult = authChecker.checkResult(() -> { + // To check our authorization, we execute a non-existent command. + // If it worked (200), we need to check further if we can getstatus (BDVs will respond 200 + // on non-existent command and not authorized) + // + // Update: It seems that some receiver with header (CERS) authorization also return the 500 status code + // if unauthorized. So also this case needs to be checked with getStatus as this seems to be the easiest fix + // ToDo: Check whole implementation + // + // If we have 200 (good) or 500 (command not found), we return OK + // If we have 403 (unauthorized) or 503 (service not available), we need pairing + HttpResponse status = irccClient.executeSoap(transport, "nonexistentcommand"); + logger.debug("Response status of calling non existing command: '{}'", status.getHttpCode()); + if (status.getHttpCode() == HttpStatus.OK_200 + || status.getHttpCode() == HttpStatus.INTERNAL_SERVER_ERROR_500) { + status = getStatus(); + logger.debug("Response status of calling ircc status: '{}'", status.getHttpCode()); + } + + // here status value might com from from the getStatus call + if (status.getHttpCode() == HttpStatus.OK_200 + || status.getHttpCode() == HttpStatus.INTERNAL_SERVER_ERROR_500) { + return AccessResult.OK; + } + if (status.getHttpCode() == HttpStatus.FORBIDDEN_403 + || status.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) { + return AccessResult.NEEDSPAIRING; + } + return new AccessResult(status); + }); + logger.debug("checkResult: '{}'", checkResult.getCode()); + + if (CheckResult.OK_HEADER.equals(checkResult)) { + if (accessCode == null || accessCode.isEmpty()) { + // This shouldn't happen - if our check result is OK_HEADER, then + // we had a valid (non-null, non-empty) accessCode. Unfortunately + // nullable checking thinks this can be null now. + logger.debug("This shouldn't happen - access code is blank!: {}", accessCode); + return new LoginUnsuccessfulResponse(ThingStatusDetail.CONFIGURATION_ERROR, + "Access code cannot be blank"); + } else { + logger.debug("setup header auth"); + SonyAuth.setupHeader(accessCode, transport); + } + } else if (CheckResult.OK_COOKIE.equals(checkResult)) { + logger.debug("setup cookie auth"); + SonyAuth.setupCookie(transport); + } else if (AccessResult.NEEDSPAIRING.equals(checkResult)) { + if (accessCode == null || accessCode.isEmpty()) { + return new LoginUnsuccessfulResponse(ThingStatusDetail.CONFIGURATION_ERROR, + "Access code cannot be blank"); + } else { + final AccessResult result = sonyAuth.requestAccess(transport, + IrccConstants.ACCESSCODE_RQST.equalsIgnoreCase(accessCode) ? null : accessCode); + if (AccessResult.OK.equals(result)) { + SonyAuth.setupCookie(transport); + } else { + return new LoginUnsuccessfulResponse(ThingStatusDetail.CONFIGURATION_ERROR, result.getMsg()); + } + } + } else { + final AccessResult resp = sonyAuth.registerRenewal(transport); + logger.debug("register renewal"); + if (AccessResult.OK.equals(resp)) { + SonyAuth.setupCookie(transport); + } else { + // Use configuration_error - prevents handler from continually trying to + // reconnect + return new LoginUnsuccessfulResponse(ThingStatusDetail.CONFIGURATION_ERROR, + "Error registering renewal: " + resp.getMsg()); + } + } + + writeCommands(); + + callback.statusChanged(ThingStatus.ONLINE, ThingStatusDetail.NONE, null); + + refreshVersion(); + refreshRegistrationMode(); + + return null; + } + + /** + * Gets the current status from the IRCC device + * + * @return the non-null HttpResponse of the request + */ + public HttpResponse getStatus() { + final String statusUrl = irccClient.getUrlForAction(IrccClient.AN_GETSTATUS); + if (statusUrl == null) { + return new HttpResponse(HttpStatus.SERVICE_UNAVAILABLE_503, "No GETSTATUS url"); + } else { + return transport.executeGet(statusUrl); + } + } + + /** + * Write the various IRCC commands the device reports + * + * @throws IOException if an IO exception occurs writing the map file + */ + private void writeCommands() throws IOException { + if (transformService == null) { + logger.debug("No MAP transformation service - skipping writing a map file"); + } else { + final String cmdMap = config.getCommandsMapFile(); + if (cmdMap == null || cmdMap.isEmpty()) { + logger.debug("No command map defined - ignoring"); + return; + } + + final String filePath = OpenHAB.getConfigFolder() + File.separator + + TransformationService.TRANSFORM_FOLDER_NAME + File.separator + cmdMap; + final Path file = Paths.get(filePath); + if (file.toFile().exists()) { + logger.debug("Command map already defined - ignoring: {}", file); + return; + } + + final IrccRemoteCommands remoteCmds = irccClient.getRemoteCommands(); + final List lines = new ArrayList<>(); + for (final IrccRemoteCommand v : remoteCmds.getRemoteCommands().values()) { + // Note: encode value in case it's a URL type + lines.add(v.getName() + "=" + v.getType() + ":" + URLEncoder.encode(v.getCmd(), "UTF-8")); + } + lines.sort(String.CASE_INSENSITIVE_ORDER); + + if (!lines.isEmpty()) { + logger.debug("Writing remote commands to {}", file); + Files.write(file, lines, StandardCharsets.UTF_8); + } + } + } + + /** + * Refresh the state for this protocol (currently only calls {@link #refreshStatus}) + */ + public void refreshState() { + refreshStatus(); + } + + /** + * Refresh the status of the device + */ + public void refreshStatus() { + final String getStatusUrl = irccClient.getUrlForAction(IrccClient.AN_GETSTATUS); + if (getStatusUrl == null || getStatusUrl.isEmpty()) { + logger.debug("{} is not implemented", IrccClient.AN_GETSTATUS); + return; + } + + final HttpResponse resp = transport.executeGet(getStatusUrl); + if (resp.getHttpCode() == HttpStatus.OK_200) { + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_PRIMARY, IrccConstants.CHANNEL_POWER), + OnOffType.ON); + + final String irccStatusXml = resp.getContent(); + final IrccStatusList irccStatusList = IrccStatusList.get(irccStatusXml); + if (irccStatusList == null) { + logger.debug("IRCC Status response ({}) was not valid: {}", getStatusUrl, irccStatusXml); + return; + } + + if (irccStatusList.isTextInput()) { + if (!isInText.getAndSet(true)) { + refreshText(); + refreshInText(); + } + } else { + if (isInText.getAndSet(false)) { + refreshText(); + refreshInText(); + } + } + + if (irccStatusList.isWebBrowse()) { + if (!isInWebBrowse.getAndSet(true)) { + refreshInBrowser(); + } + refreshContentUrl(); // always refresh in case they change urls + } else { + if (isInWebBrowse.getAndSet(false)) { + refreshInBrowser(); + refreshContentUrl(); + } + } + + final IrccStatus viewing = irccStatusList.getViewing(); + + if (viewing == null) { + if (isViewing.getAndSet(false)) { + refreshIsViewing(); + } + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_ID), + UnDefType.UNDEF); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_SOURCE), + UnDefType.UNDEF); + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_SOURCE2), + UnDefType.UNDEF); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_CLASS), + UnDefType.UNDEF); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_TITLE), + UnDefType.UNDEF); + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_DURATION), + UnDefType.UNDEF); + + if (irccStatusList.isDisk()) { + refreshContentInformation(); + } + + } else { + if (!isViewing.getAndSet(true)) { + refreshIsViewing(); + } + final String id = viewing.getItemValue(IrccStatusItem.ID); + final String source = viewing.getItemValue(IrccStatusItem.SOURCE); + final String source2 = viewing.getItemValue(IrccStatusItem.SOURCE2); + final String clazz = viewing.getItemValue(IrccStatusItem.CLASS); + final String title = viewing.getItemValue(IrccStatusItem.TITLE); + final String dur = viewing.getItemValue(IrccStatusItem.DURATION); + + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_ID), + SonyUtil.newStringType(id)); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_SOURCE), + SonyUtil.newStringType(source)); + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_SOURCE2), + SonyUtil.newStringType(source2)); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_CLASS), + SonyUtil.newStringType(clazz)); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_TITLE), + SonyUtil.newStringType(title)); + if (dur == null || dur.isEmpty()) { + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_DURATION), + UnDefType.NULL); + } else { + try { + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_DURATION), + SonyUtil.newDecimalType(Integer.parseInt(dur))); + } catch (final NumberFormatException e) { + logger.debug("Could not convert {} into an integer", dur); + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_VIEWING, IrccConstants.CHANNEL_DURATION), + UnDefType.NULL); + } + } + + final String cId = contentId.get(); + if (cId == null || !cId.equals(id)) { + refreshContentInformation(); + } + } + } else if (resp.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) { + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_PRIMARY, IrccConstants.CHANNEL_POWER), + OnOffType.OFF); + } else { + logger.debug("Unknown code from {}: {}", IrccClient.AN_GETSTATUS, resp); + } + } + + /** + * Refresh whether the device is in a text field or not + */ + public void refreshInText() { + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_PRIMARY, IrccConstants.CHANNEL_INTEXT), + isInText.get() ? OnOffType.ON : OnOffType.OFF); + } + + /** + * Refresh whether the device is in a browser or not + */ + public void refreshInBrowser() { + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_PRIMARY, IrccConstants.CHANNEL_INBROWSER), + isInWebBrowse.get() ? OnOffType.ON : OnOffType.OFF); + } + + /** + * Refresh whether the device is viewing content + */ + public void refreshIsViewing() { + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_PRIMARY, IrccConstants.CHANNEL_ISVIEWING), + isViewing.get() ? OnOffType.ON : OnOffType.OFF); + } + + /** + * Set's the version property of the thing based on the device + */ + private void refreshVersion() { + final String version = irccClient.getUnrDeviceInformation().getVersion(); + if (version == null || version.isEmpty()) { + callback.setProperty(IrccConstants.PROP_VERSION, IrccUnrDeviceInfo.NOTSPECIFIED); + } else { + callback.setProperty(IrccConstants.PROP_VERSION, version); + } + } + + /** + * Set's the registration mode property of the thing based on the device + */ + private void refreshRegistrationMode() { + callback.setProperty(IrccConstants.PROP_REGISTRATIONMODE, Integer.toString(irccClient.getRegistrationMode())); + } + + /** + * Refresh the current text field's text + */ + public void refreshText() { + final String getTextUrl = irccClient.getUrlForAction(IrccClient.AN_GETTEXT); + if (getTextUrl == null || getTextUrl.isEmpty()) { + logger.debug("{} is not implemented", IrccClient.AN_GETTEXT); + return; + } + + final HttpResponse resp = transport.executeGet(getTextUrl); + if (resp.getHttpCode() == HttpStatus.OK_200) { + final String irccTextXml = resp.getContent(); + final IrccText irccText = IrccText.get(irccTextXml); + if (irccText == null) { + logger.debug("IRCC get text response ({}) was not valid: {}", getTextUrl, irccTextXml); + } else { + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_PRIMARY, IrccConstants.CHANNEL_TEXT), + SonyUtil.newStringType(irccText.getText())); + } + } else if (resp.getHttpCode() == HttpStatus.NOT_ACCEPTABLE_406 + || resp.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) { + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_PRIMARY, IrccConstants.CHANNEL_TEXT), + UnDefType.UNDEF); + } else { + logger.debug("Unknown code from {}: {}", IrccClient.AN_GETTEXT, resp); + } + } + + /** + * Refresh the device's content URL + */ + public void refreshContentUrl() { + final String getContentUrl = irccClient.getUrlForAction(IrccClient.AN_GETCONTENTURL); + if (getContentUrl == null || getContentUrl.isEmpty()) { + logger.debug("{} is not implemented", IrccClient.AN_GETCONTENTURL); + return; + } + + final HttpResponse resp = transport.executeGet(getContentUrl); + if (resp.getHttpCode() == HttpStatus.OK_200) { + final String irccContentUrlXml = resp.getContent(); + final IrccContentUrl irccContent = IrccContentUrl.get(irccContentUrlXml); + if (irccContent == null) { + logger.debug("IRCC content url response ({}) was not valid: {}", getContentUrl, irccContentUrlXml); + return; + } + + final String url = irccContent.getUrl(); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_PRIMARY, IrccConstants.CHANNEL_CONTENTURL), + SonyUtil.newStringType(url)); + + final IrccContentInformation ici = irccContent.getContentInformation(); + if (ici == null) { + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_TITLE), + UnDefType.UNDEF); + } else { + final String urlTitle = ici.getInfoItemValue(IrccContentInformation.TITLE); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_TITLE), + SonyUtil.newStringType(urlTitle)); + } + } else if (resp.getHttpCode() == HttpStatus.NOT_ACCEPTABLE_406 + || resp.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) { + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_PRIMARY, IrccConstants.CHANNEL_CONTENTURL), + UnDefType.UNDEF); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_TITLE), + UnDefType.UNDEF); + } else { + logger.debug("Unknown code from {}: {}", IrccClient.AN_GETCONTENTURL, resp); + } + } + + /** + * Refresh the device's current content information + */ + public void refreshContentInformation() { + final String getContentUrl = irccClient.getUrlForAction(IrccClient.AN_GETCONTENTINFORMATION); + if (getContentUrl == null || getContentUrl.isEmpty()) { + logger.debug("{} is not implemented", IrccClient.AN_GETCONTENTINFORMATION); + return; + } + + final HttpResponse resp = transport.executeGet(getContentUrl); + if (resp.getHttpCode() == HttpStatus.OK_200) { + final String irccContentXml = resp.getContent(); + final IrccContentInformation irccContent = IrccContentInformation.get(irccContentXml); + if (irccContent == null) { + logger.debug("IRCC get content url response ({}) was invalid: {}", getContentUrl, irccContentXml); + return; + } + + final String id = irccContent.getInfoItemValue(IrccContentInformation.ID); + final String title = irccContent.getInfoItemValue(IrccContentInformation.TITLE); + final String clazz = irccContent.getInfoItemValue(IrccContentInformation.CLASS); + final String source = irccContent.getInfoItemValue(IrccContentInformation.SOURCE); + final String mediaType = irccContent.getInfoItemValue(IrccContentInformation.MEDIATYPE); + final String mediaFormat = irccContent.getInfoItemValue(IrccContentInformation.MEDIAFORMAT); + final String edition = irccContent.getInfoItemValue(IrccContentInformation.EDITION); + final String description = irccContent.getInfoItemValue(IrccContentInformation.DESCRIPTION); + final String genre = irccContent.getInfoItemValue(IrccContentInformation.GENRE); + final String dur = irccContent.getInfoItemValue(IrccContentInformation.DURATION); + final String rating = irccContent.getInfoItemValue(IrccContentInformation.RATING); + final String daterelease = irccContent.getInfoItemValue(IrccContentInformation.DATERELEASE); + final String director = irccContent.getInfoItemValue(IrccContentInformation.DIRECTOR); + final String producer = irccContent.getInfoItemValue(IrccContentInformation.PRODUCER); + final String screen = irccContent.getInfoItemValue(IrccContentInformation.SCREENWRITER); + final String iconData = irccContent.getInfoItemValue(IrccContentInformation.ICONDATA); + + contentId.set(id); + + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_ID), + SonyUtil.newStringType(id)); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_TITLE), + SonyUtil.newStringType(title)); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_CLASS), + SonyUtil.newStringType(clazz)); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_SOURCE), + SonyUtil.newStringType(source)); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_MEDIATYPE), + SonyUtil.newStringType(mediaType)); + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_MEDIAFORMAT), + SonyUtil.newStringType(mediaFormat)); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_EDITION), + SonyUtil.newStringType(edition)); + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_DESCRIPTION), + SonyUtil.newStringType(description)); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_GENRE), + SonyUtil.newStringType(genre)); + + if (dur == null || dur.isEmpty()) { + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_DURATION), + UnDefType.NULL); + } else { + try { + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_DURATION), + SonyUtil.newDecimalType(Integer.parseInt(dur))); + } catch (final NumberFormatException e) { + logger.debug("Could not convert {} into an integer", dur); + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_DURATION), + UnDefType.NULL); + } + } + + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_RATING), + SonyUtil.newStringType(rating)); + + if (daterelease == null || daterelease.isEmpty()) { + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_DATERELEASE), + UnDefType.NULL); + } else { + try { + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_DATERELEASE), + new DateTimeType(daterelease)); + } catch (final IllegalArgumentException e) { + logger.debug("Could not convert {} into an valid date", daterelease); + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_DATERELEASE), + UnDefType.NULL); + } + } + + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_DIRECTOR), + SonyUtil.newStringType(director)); + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_PRODUCER), + SonyUtil.newStringType(producer)); + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_SCREENWRITER), + SonyUtil.newStringType(screen)); + + if (iconData == null || iconData.isEmpty()) { + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_ICONDATA), + UnDefType.NULL); + } else { + final byte[] rawBytes = Base64.getDecoder().decode(iconData); + callback.stateChanged( + SonyUtil.createChannelId(IrccConstants.GRP_CONTENT, IrccConstants.CHANNEL_ICONDATA), + new RawType(rawBytes, RawType.DEFAULT_MIME_TYPE)); + } + + } else if (resp.getHttpCode() == HttpStatus.NOT_ACCEPTABLE_406 + || resp.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) { + callback.stateChanged(SonyUtil.createChannelId(IrccConstants.GRP_PRIMARY, IrccConstants.CHANNEL_CONTENTURL), + UnDefType.UNDEF); + } else { + logger.debug("Unknown code from {}: {}", IrccClient.AN_GETCONTENTINFORMATION, resp); + } + } + + /** + * Set's the power status of the device + * + * @param turnOn true to turn on, false otherwise + */ + public void sendPower(final boolean turnOn) { + final IrccRemoteCommands cmds = irccClient.getRemoteCommands(); + if (turnOn) { + SonyUtil.sendWakeOnLan(logger, config.getDeviceIpAddress(), config.getDeviceMacAddress()); + final IrccRemoteCommand powerOn = cmds.getPowerOn(); + if (powerOn != null) { + sendIrccCommand(powerOn.getCmd()); + } + } else { + final IrccRemoteCommand cmd = cmds.getPowerOff(); + if (cmd == null) { + logger.debug("No power off (or power toggle) remote command was found"); + } else { + sendIrccCommand(cmd.getCmd()); + } + } + } + + /** + * Send command to the device (if the command is empty, nothing occurs) + * + * @param cmd a non-null, non-empty command to send + */ + public void sendCommand(final String cmd) { + SonyUtil.validateNotEmpty(cmd, "cmd cannot be empty"); + + final String cmdMap = config.getCommandsMapFile(); + + String cmdToSend = cmd; + + final TransformationService localTransformService = transformService; + if (localTransformService == null) { + logger.debug("No MAP transformation service - cannot transform command"); + } else { + try { + if (cmdMap != null && !cmdMap.isBlank()) { + cmdToSend = localTransformService.transform(cmdMap, cmd); + if (cmdToSend != null && !cmdToSend.equalsIgnoreCase(cmd)) { + logger.debug("Transformed {} with map file '{}' to {}", cmd, cmdMap, cmdToSend); + } + } + } catch (final TransformationException e) { + logger.debug("Failed to transform {} using map file '{}', exception={} - ignoring error", cmd, cmdMap, + e.getMessage()); + } + } + + try { + if (cmdToSend != null) { + cmdToSend = URLDecoder.decode(cmdToSend, "UTF-8"); + } + } catch (final UnsupportedEncodingException e) { + logger.debug("Failed to decode {}, exception={} - ignoring error", cmdToSend, e.getMessage()); + } + + final int idx = cmdToSend == null ? -1 : cmdToSend.indexOf(':'); + + String protocol = IrccRemoteCommand.IRCC; + if (cmdToSend != null && idx >= 0) { + protocol = cmdToSend.substring(0, idx); + cmdToSend = cmdToSend.substring(idx + 1); + } + + if (cmdToSend == null || cmdToSend.isEmpty()) { + logger.debug("Command was empty - ignoring"); + } else if (IrccRemoteCommand.IRCC.equalsIgnoreCase(protocol)) { + sendIrccCommand(cmdToSend); + } else if (IrccRemoteCommand.URL.equalsIgnoreCase(protocol)) { + final HttpResponse resp = transport.executeGet(cmdToSend); + if (resp.getHttpCode() == HttpStatus.OK_200) { + logger.trace("Send of command {} was successful", cmdToSend); + } else if (resp.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) { + logger.debug("URL service is unavailable (power off?)"); + } else { + logger.debug("Bad return code from {}: {}", IrccClient.SRV_ACTION_SENDIRCC, resp); + } + } else { + logger.debug("Unknown protocol found for the send command: {}", cmd); + } + } + + /** + * Send an IRCC command to the device + * + * @param cmdToSend the non-null, non-empty IRCC command to send + */ + private void sendIrccCommand(final String cmdToSend) { + SonyUtil.validateNotEmpty(cmdToSend, "cmdToSend cannot be empty"); + + final HttpResponse resp = irccClient.executeSoap(transport, cmdToSend); + if (resp.getHttpCode() == HttpStatus.OK_200) { + logger.trace("Sending of IRCC command {} was successful", cmdToSend); + } else if (resp.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) { + logger.debug("IRCC service is unavailable (power off?)"); + } else if (resp.getHttpCode() == HttpStatus.INTERNAL_SERVER_ERROR_500) { + logger.debug("IRCC service returned a 500 - probably an unknown command: {}", cmdToSend); + } else { + logger.debug("Bad return code from {}: {}", IrccClient.SRV_ACTION_SENDIRCC, resp); + } + } + + /** + * Set's the content URL of the device + * + * @param contentUrl the non-null, non-empty content url + */ + public void sendContentUrl(final String contentUrl) { + SonyUtil.validateNotEmpty(contentUrl, "contentUrl cannot be empty"); + + final String sendContentUrl = irccClient.getUrlForAction(IrccClient.AN_SENDCONTENTURL); + if (sendContentUrl == null || sendContentUrl.isEmpty()) { + logger.debug("{} action was not implmented", IrccClient.AN_SENDCONTENTURL); + return; + } + final String body = "" + contentUrl + ""; + final HttpResponse resp = transport.executePostXml(sendContentUrl, body); + if (resp.getHttpCode() == HttpStatus.OK_200) { + logger.trace("Send of URL {} was successful", contentUrl); + // Do nothing + } else if (resp.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) { + logger.debug("IRCC service is unavailable (power off?)"); + } else { + logger.debug("Bad return code from {}: {}", IrccClient.AN_SENDCONTENTURL, resp); + } + } + + /** + * Set's the text field with the specified text + * + * @param text the non-null, non-empty text + */ + public void sendText(final String text) { + SonyUtil.validateNotEmpty(text, "text cannot be empty"); + final String sendTextUrl = irccClient.getUrlForAction(IrccClient.AN_SENDTEXT); + if (sendTextUrl == null || sendTextUrl.isEmpty()) { + logger.debug("{} action was not implmented", IrccClient.AN_SENDTEXT); + return; + } + + try { + final String textParm = "?text=" + URLEncoder.encode(text, "UTF-8"); + final HttpResponse resp = transport.executeGet(sendTextUrl + textParm); + if (resp.getHttpCode() == HttpStatus.OK_200) { + logger.trace("Send of text {} was successful", text); + } else if (resp.getHttpCode() == HttpStatus.NOT_ACCEPTABLE_406) { + logger.debug("{} was sent but 'not acceptable' was returned (ie no input field to accept text)", + IrccClient.AN_SENDTEXT); + } else if (resp.getHttpCode() == HttpStatus.SERVICE_UNAVAILABLE_503) { + logger.debug("IRCC service is unavailable (power off?)"); + } else { + logger.debug("Unknown code for {}:L {}", IrccClient.AN_SENDTEXT, resp); + } + } catch (final UnsupportedEncodingException e) { + logger.debug("UTF-8 is not supported on this platform: {}", e.getMessage(), e); + } + } + + @Override + public void close() { + transport.close(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccActionList.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccActionList.java new file mode 100644 index 0000000000000..cd79dd28c15d1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccActionList.java @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents the deserialized results of an IRCC actionList command. The following is an example of the + * results that will be deserialized: + * + *
+ * {@code
+        
+        
+          
+          
+          
+          
+          
+          
+          
+          
+          
+          
+        
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@XStreamAlias("actionList") +public class IrccActionList { + /** + * The list of actions found in the XML (can be null or empty) + */ + @XStreamImplicit + private @Nullable List<@Nullable IrccAction> actions; + + /** + * Gets the url for the given action name + * + * @param actionName the non-null, non-empty action name + * @return the url for action or null if not found + */ + @Nullable + public String getUrlForAction(final String actionName) { + SonyUtil.validateNotEmpty(actionName, "actionName cannot be empty"); + + final List<@Nullable IrccAction> localActions = actions; + if (localActions != null) { + for (final IrccAction action : localActions) { + if (action != null && actionName.equalsIgnoreCase(action.getName())) { + return action.getUrl(); + } + } + } + return null; + } + + /** + * Gets the registration mode for the action list or 0 if none (or invalid) + * + * @return the registration mode (should but not guaranteed to be >= 0) + */ + public int getRegistrationMode() { + final List<@Nullable IrccAction> localActions = actions; + if (localActions != null) { + for (final IrccAction action : localActions) { + if (action != null && action.getMode() != null && Objects.requireNonNull(action.getMode()).isEmpty() + && IrccAction.REGISTER.equalsIgnoreCase(action.getName())) { + try { + return Integer.parseInt(Objects.requireNonNull(action.getMode())); + } catch (final NumberFormatException e) { + return 0; + } + } + } + } + return 0; + } + + /** + * The following class represents the IRCC action and is internal to the IrccActionList class. However, it cannot be + * private since the xstream reader needs access to the annotations + * + * @author Tim Roberts - Initial contribution + */ + @XStreamAlias("action") + class IrccAction { + /** + * Represents the register action name + */ + private static final String REGISTER = "register"; + + /** + * Represents the action name + */ + @XStreamAlias("name") + @XStreamAsAttribute + private @Nullable String name; + + /** + * Represents the URL for the action + */ + @XStreamAlias("url") + @XStreamAsAttribute + private @Nullable String url; + + /** + * Represents the IRCC registration mode and is ONLY valid if the action name is {@link #REGISTER} and should be + * a number + */ + @XStreamAlias("mode") + @XStreamAsAttribute + private @Nullable String mode; + + /** + * Get's the name of the action + * + * @return a possibly null, possibly empty name + */ + public @Nullable String getName() { + return name; + } + + /** + * Get's the registration mode + * + * @return a possibly null, possibly empty registration mode + */ + public @Nullable String getMode() { + return mode; + } + + /** + * Get's the action URL + * + * @return a possibly null, possibly empty action url + */ + public @Nullable String getUrl() { + return url; + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccClient.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccClient.java new file mode 100644 index 0000000000000..82978525159a4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccClient.java @@ -0,0 +1,261 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.net.URL; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.net.HttpResponse; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.TransportOptionHeader; +import org.openhab.binding.sony.internal.upnp.models.UpnpScpd; +import org.openhab.binding.sony.internal.upnp.models.UpnpService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The class represents an IRCC client. The client will gather all the necessary about an IRCC device and provide an + * interface to query information and to manipulate the IRCC device. + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +public class IrccClient { + + /** The logger used by the client */ + private final Logger logger = LoggerFactory.getLogger(IrccClient.class); + + /** The constant representing the REGISTER action name */ + public static final String AN_REGISTER = "register"; + + /** The constant representing the GET TEXT action name */ + public static final String AN_GETTEXT = "getText"; + + /** The constant representing the SEND TEXT action name */ + public static final String AN_SENDTEXT = "sendText"; + + /** The constant representing the GET CONTENT INFORMATION action name */ + public static final String AN_GETCONTENTINFORMATION = "getContentInformation"; + + /** The constant representing the GET SYSTEM INFORMATION action name */ + public static final String AN_GETSYSTEMINFORMATION = "getSystemInformation"; + + /** The constant representing the GET REMOTE COMMANDS action name */ + public static final String AN_GETREMOTECOMMANDLIST = "getRemoteCommandList"; + + /** The constant representing the GET STATUS action name */ + public static final String AN_GETSTATUS = "getStatus"; + + /** The constant representing the GET CONTENT URL action name */ + public static final String AN_GETCONTENTURL = "getContentUrl"; + + /** The constant representing the SEND CONTENT URL action name */ + public static final String AN_SENDCONTENTURL = "sendContentUrl"; + + /** The constant representing the IRCC service name */ + public static final String SRV_IRCC = "urn:schemas-sony-com:serviceId:IRCC"; + + /** The constant representing the IRCC service action to send an IRCC command */ + public static final String SRV_ACTION_SENDIRCC = "X_SendIRCC"; + + /** The base URL for the IRCC device */ + private final URL baseUrl; + + /** The services mapped by service id */ + private final Map services; + + /** The action list for the IRCC device */ + private final IrccActionList actions; + + /** The system information for the IRCC device */ + private final IrccSystemInformation sysInfo; + + /** The remote commands supported by the IRCC device */ + private final IrccRemoteCommands remoteCommands; + + /** The IRCC UNR Device information */ + private final IrccUnrDeviceInfo irccDeviceInfo; + + /** The scpd mapped by service id */ + private final Map scpdByService; + + /** + * Constructs the IRCC client with the essential information + * + * @param baseUrl a non-null base URL + * @param services a non-null, possibly empty map of services by service id + * @param actions the non-null ircc action list + * @param sysInfo the non-null ircc system information + * @param remoteCommands the non-null ircc remote commands + * @param irccDeviceInfo the non-null ircc device information + * @param scpdByService a non-null, possibly empty map of scpd (service control point information) by service id + */ + public IrccClient(final URL baseUrl, final Map services, final IrccActionList actions, + final IrccSystemInformation sysInfo, final IrccRemoteCommands remoteCommands, + final IrccUnrDeviceInfo irccDeviceInfo, final Map scpdByService) { + Objects.requireNonNull(baseUrl, "baseUrl cannot be null"); + Objects.requireNonNull(services, "services cannot be null"); + Objects.requireNonNull(actions, "actions cannot be null"); + Objects.requireNonNull(sysInfo, "sysInfo cannot be null"); + Objects.requireNonNull(remoteCommands, "remoteCommands cannot be null"); + Objects.requireNonNull(irccDeviceInfo, "irccDeviceInfo cannot be null"); + Objects.requireNonNull(scpdByService, "scpdByService cannot be null"); + + this.baseUrl = baseUrl; + this.services = Collections.unmodifiableMap(services); + this.actions = actions; + this.sysInfo = sysInfo; + this.remoteCommands = remoteCommands; + this.irccDeviceInfo = irccDeviceInfo; + this.scpdByService = Collections.unmodifiableMap(scpdByService); + } + + /** + * Returns the base URL of the IRCC device + * + * @return the non-null, non-empty base URL + */ + public URL getBaseUrl() { + return baseUrl; + } + + /** + * Gets the URL for the given action name or null if not found + * + * @param actionName the non-null, non-empty action name + * @return the url for action or null if not found + */ + public @Nullable String getUrlForAction(final String actionName) { + SonyUtil.validateNotEmpty(actionName, "actionName cannot be empty"); + return actions.getUrlForAction(actionName); + } + + /** + * Gets the registration mode of the IRCC device + * + * @return the registration mode (most likely >= 0) + */ + public int getRegistrationMode() { + return actions.getRegistrationMode(); + } + + /** + * Gets the service related to the service ID + * + * @param serviceId the non-null, non-empty service id + * @return the service or null if not found + */ + private @Nullable UpnpService getService(final String serviceId) { + SonyUtil.validateNotEmpty(serviceId, "serviceId cannot be empty"); + return services.get(serviceId); + } + + /** + * Gets the system information for the IRCC device + * + * @return the non-null system information + */ + public IrccSystemInformation getSystemInformation() { + return sysInfo; + } + + /** + * Gets the UNR Device information for the IRCC device + * + * @return the non-null unr device information + */ + public IrccUnrDeviceInfo getUnrDeviceInformation() { + return irccDeviceInfo; + } + + /** + * Gets the remote commands supported by the IRCC device + * + * @return the non-null remote commands + */ + public IrccRemoteCommands getRemoteCommands() { + return remoteCommands; + } + + /** + * Get's the SOAP command for the given service ID, action name and possibly parameters + * + * @param serviceId the non-null, non-empty service id + * @param actionName the non-null, non-empty action name + * @param parms the possibly null, possibly empty list of action parameters + * @return the possibly null (if not service/action is not found) SOAP command + */ + public @Nullable String getSOAP(final String serviceId, final String actionName, final String... parms) { + SonyUtil.validateNotEmpty(serviceId, "serviceId cannot be empty"); + SonyUtil.validateNotEmpty(actionName, "actionName cannot be empty"); + + final UpnpService service = services.get(serviceId); + + if (service == null) { + logger.debug("Unable to getSOAP for service id {} - service not found", serviceId); + return null; + } + final UpnpScpd scpd = scpdByService.get(serviceId); + if (scpd == null) { + logger.debug("Unable to getSOAP for service id {} - scpd not found", serviceId); + return null; + } + final String serviceType = service.getServiceType(); + if (serviceType == null || serviceType.isEmpty()) { + logger.debug("Unable to getSOAP for service id {} - serviceType was empty", serviceId); + return null; + } + return scpd.getSoap(serviceType, actionName, parms); + } + + /** + * Executes a SOAP command + * + * @param transport a non-null transport to use + * @param cmd a non-null, non-empty cmd to execute + * @return an HttpResponse indicating the results of the execution + */ + public HttpResponse executeSoap(final SonyHttpTransport transport, final String cmd) { + Objects.requireNonNull(transport, "transport cannot be null"); + SonyUtil.validateNotEmpty(cmd, "cmd cannot be empty"); + + final UpnpService service = getService(IrccClient.SRV_IRCC); + if (service == null) { + logger.debug("IRCC Service was not found"); + return new HttpResponse(HttpStatus.NOT_FOUND_404, "IRCC Service was not found"); + } + + final String soap = getSOAP(IrccClient.SRV_IRCC, IrccClient.SRV_ACTION_SENDIRCC, cmd); + if (soap == null || soap.isEmpty()) { + logger.debug("Unable to find the IRCC service/action to send IRCC"); + return new HttpResponse(HttpStatus.NOT_FOUND_404, "Unable to find the IRCC service/action to send IRCC"); + } + + final URL baseUrl = getBaseUrl(); + final URL controlUrl = service.getControlUrl(baseUrl); + if (controlUrl == null) { + logger.debug("ControlURL for IRCC service wasn't found: {}", baseUrl); + return new HttpResponse(HttpStatus.NOT_FOUND_404, "ControlURL for IRCC service wasn't found: " + baseUrl); + } else { + return transport.executePostXml(controlUrl.toExternalForm(), soap, new TransportOptionHeader("SOAPACTION", + "\"" + service.getServiceType() + "#" + IrccClient.SRV_ACTION_SENDIRCC + "\"")); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccCode.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccCode.java new file mode 100644 index 0000000000000..19b213639fefd --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccCode.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +/** + * This class represents the deserialized results of an IRCC command (both the name and the value of the command). The + * following is an example of the results that will be deserialized: + * + *
+ * {@code
+    AAAAAQAAAAEAAAAVAw==
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@XStreamAlias("X_IRCCCode") +class IrccCode { + /** + * The command for this code (will be mixed case, will not be empty) + */ + private final String command; + + /** The value for this code (will not be empty) */ + private final String value; + + /** + * Constructs the code from the given command and value + * + * @param command a non-null, non-empty command + * @param value a non-null, non-empty value + */ + private IrccCode(final String command, final String value) { + SonyUtil.validateNotEmpty(command, "command cannot be empty"); + SonyUtil.validateNotEmpty(value, "value cannot be empty"); + this.command = command; + this.value = value; + } + + /** + * The command (name) of this code + * + * @return a non-null, non-empty command + */ + public String getCommand() { + return command; + } + + /** + * The command value of this code + * + * @return a non-null, non-empty value + */ + public String getValue() { + return value; + } + + /** + * The converter used to unmarshal the {@link IrccCode}. Please note this should only be used to unmarshal XML + * (marshaling will throw a {@link UnsupportedOperationException}) + * + * @author Tim Roberts - Initial contribution + */ + static class IrccCodeConverter implements Converter { + @Override + public boolean canConvert(@SuppressWarnings("rawtypes") final @Nullable Class clazz) { + return IrccCode.class.equals(clazz); + } + + @Override + public void marshal(final @Nullable Object arg0, final @Nullable HierarchicalStreamWriter arg1, + final @Nullable MarshallingContext arg2) { + throw new UnsupportedOperationException(); + } + + @Override + public @Nullable Object unmarshal(final @Nullable HierarchicalStreamReader reader, + final @Nullable UnmarshallingContext context) { + Objects.requireNonNull(reader, "reader cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + final String command = reader.getAttribute("command"); + if (command.isEmpty()) { + return null; + } + + final String value = reader.getValue(); + if (value.isEmpty()) { + return null; + } + + return new IrccCode(command, value); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccCodeList.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccCodeList.java new file mode 100644 index 0000000000000..ced1bc75b6e2e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccCodeList.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents the deserialized results of an IRCC actionList command. The following is an example of the + * results that will be deserialized: + * + *
+ * {@code
+    
+        AAAAAQAAAAEAAAAVAw==
+    
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class IrccCodeList { + + /** + * The list of commands for this code list + */ + @XStreamImplicit + private @Nullable List<@Nullable IrccCode> cmds; + + /** + * Gets the commands for the code list + * + * @return a possibly empty list of {@link IrccCode} + */ + public List getCommands() { + final List<@Nullable IrccCode> localCmds = cmds; + + // Need to filter out nulls in case of the the IRCC was invalid + return localCmds == null ? Collections.emptyList() + : Collections.unmodifiableList(localCmds.stream().filter(x -> x != null).collect(Collectors.toList())); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccContentInformation.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccContentInformation.java new file mode 100644 index 0000000000000..969deaa9a716d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccContentInformation.java @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents the deserialized results of an IRCC content information command. The following is an example of + * the results that will be deserialized. Please note that the 'field' is not unique and can be repeated (ex: each + * director will have it's own infoItem line). The class will merge multiple lines together into a comma-delimited list + * (making 'field' unique). + * + *
+ * {@code
+    
+    
+      
+      
+      
+      
+    
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@XStreamAlias("contentInformation") +public class IrccContentInformation { + /** The constant for a title (browswer title or video title) */ + public static final String TITLE = "title"; + + /** The constant for a class (video, etc) */ + public static final String CLASS = "class"; + + /** The constant for the source (DVD, USB, etc) */ + public static final String SOURCE = "source"; + + /** The constant for the media type (DVD, etc) */ + public static final String MEDIATYPE = "mediaType"; + + /** The constant for the media format (video, etc) */ + public static final String MEDIAFORMAT = "mediaFormat"; + + /** The constant for the ID of the content (a long alphanumeric) */ + public static final String ID = "id"; // 3CD3N19Q253851813V98704329773844B92D3340A18D15901A2AP7 - matches status + + /** The constant for the content edition */ + public static final String EDITION = "edition"; // no example + + /** The constant for the content description (the dvd description) */ + public static final String DESCRIPTION = "description"; + + /** The constant for the content genre (action, adventure, etc) */ + public static final String GENRE = "genre"; // Action/Adventure + + /** The constant for the content duration (in seconds) */ + public static final String DURATION = "duration"; + + /** The constant for the content raging (G, PG, etc) */ + public static final String RATING = "rating"; // G + + /** The constant for when the content was released (2001-11-01, unknown if regional format) */ + public static final String DATERELEASE = "dateRelease"; + + /** The constant for the list of directors */ + public static final String DIRECTOR = "director"; + + /** The constant for the list of producers */ + public static final String PRODUCER = "producer"; + + /** The constant for the list of screen writers */ + public static final String SCREENWRITER = "screenWriter"; + + /** The constant for the icon data (base64 encoded) */ + public static final String ICONDATA = "iconData"; + + /** The list of {@link IrccInfoItem} */ + @XStreamImplicit + private @Nullable List<@Nullable IrccInfoItem> infoItems; + + /** + * Constructs the {@link IrccContentInformation} from the given XML + * + * @param xml a non-null, non-empty XML + * @return a {@link IrccContentInformation} or null if not valid + */ + public static @Nullable IrccContentInformation get(final String xml) { + SonyUtil.validateNotEmpty(xml, "xml cannot be null"); + return IrccXmlReader.CONTENTINFO.fromXML(xml); + } + + /** + * Gets the info item value or null if not found. If there are multiple items for the given name, they will be comma + * separated. + * + * @param name the non-null, non-empty name to get + * @return the value (possibly comma delimited) for the name or null if none found. + */ + public @Nullable String getInfoItemValue(final String name) { + SonyUtil.validateNotEmpty(name, "name cannot be empty"); + final List<@Nullable IrccInfoItem> ii = infoItems; + if (ii == null) { + return null; + } + + final StringBuilder b = new StringBuilder(); + for (final IrccInfoItem i : ii) { + if (i != null && name.equalsIgnoreCase(i.getName())) { + b.append(i.getValue()); + b.append(", "); + } + } + + if (b.length() == 0) { + return null; + } + + final int len = b.length(); + if (len > 1) { + b.delete(len - 2, len); + } + return b.toString(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccContentUrl.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccContentUrl.java new file mode 100644 index 0000000000000..083c4650075c2 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccContentUrl.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * This class represents the deserialized results of an IRCC content url command. The following is an example of + * the results that will be deserialized. + * + *
+ * {@code
+    
+    
+        www.google.com
+        
+          
+          
+          
+          
+        
+    
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@XStreamAlias("contenturl") +public class IrccContentUrl { + /** The URL representing the content */ + @XStreamAlias("url") + private @Nullable String url; + + /** The content information for the URL */ + @XStreamAlias("contentInformation") + private @Nullable IrccContentInformation contentInformation; + + /** + * Creates the {@link IrccContentUrl} from the given XML + * + * @param xml a non-null, non-empty XML to parse + * @return the {@link IrccContentUrl} or null if not valid + */ + public static @Nullable IrccContentUrl get(final String xml) { + SonyUtil.validateNotEmpty(xml, "xml cannot be empty"); + return IrccXmlReader.CONTENTURL.fromXML(xml); + } + + /** + * Returns the content of the URL + * + * @return a possibly null, possibly empty URL + */ + public @Nullable String getUrl() { + return url; + } + + /** + * Gets the content information for the URL + * + * @return the possibly null content information + */ + public @Nullable IrccContentInformation getContentInformation() { + return contentInformation; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccDevice.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccDevice.java new file mode 100644 index 0000000000000..b780fc9fc7e96 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccDevice.java @@ -0,0 +1,310 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.upnp.models.UpnpService; +import org.openhab.binding.sony.internal.upnp.models.UpnpServiceList; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * This class represents the deserialized results of an IRCC device. The following is an example of + * the results that will be deserialized. + * + * The following is an example of a Scalar/IRCC device + * + *
+ * {@code
+    
+      urn:schemas-upnp-org:device:Basic:1
+      XBR-55X900E
+      Sony Corporation
+      http://www.sony.net/
+      XBR-55X900E
+      uuid:25257d9e-41b1-43df-9332-ffe401305cf4
+
+      
+        
+          image/jpeg
+          120
+          120
+          24
+          /sony/webapi/ssdp/icon/dlna_tv_120.jpg
+        
+      
+      
+        
+          urn:schemas-sony-com:service:ScalarWebAPI:1
+          urn:schemas-sony-com:serviceId:ScalarWebAPI
+          /sony/webapi/ssdp/scpd/WebApiSCPD.xml
+          http://192.168.1.190/sony
+          
+        
+        
+          urn:schemas-sony-com:service:IRCC:1
+          urn:schemas-sony-com:serviceId:IRCC
+          http://192.168.1.190/sony/ircc/IRCCSCPD.xml
+          http://192.168.1.190/sony/ircc
+          
+        
+      
+      
+        1.0
+        http://192.168.1.190/sony
+        
+          guide        avContent        cec        audio        accessControl        system        appControl        videoScreen        encryption        contentshare
+        
+      
+      
+        1.0
+        
+          
+            AAEAAAAB
+          
+          
+            AAIAAACk
+          
+          
+            AAIAAACX
+          
+          
+            AAIAAAB3
+          
+          
+            AAIAAAAa
+          
+        
+      
+      
+        AAAAAQAAAAEAAAAVAw==
+      
+      
+        1.0
+        true
+        false
+        59095
+      
+    
+  
+ * }
+ * 
+ * + * or The following is an example of an IRCC (non-scalar) + * + *
+ * {@code
+
+
+   
+      1
+      0
+   
+   
+      urn:schemas-upnp-org:device:Basic:1
+      UBP-X800
+      Sony Corporation
+      http://www.sony.net/
+      
+      Blu-ray Disc Player
+      
+      uuid:00000003-0000-1010-8000-045d4b24d9ff
+      
+         
+            image/jpeg
+            120
+            120
+            24
+            /bdp_cx_device_icon_large.jpg
+         
+         
+            image/png
+            120
+            120
+            24
+            /bdp_cx_device_icon_large.png
+         
+         
+            image/jpeg
+            48
+            48
+            24
+            /bdp_cx_device_icon_small.jpg
+         
+         
+            image/png
+            48
+            48
+            24
+            /bdp_cx_device_icon_small.png
+         
+      
+      
+         
+            urn:schemas-sony-com:service:IRCC:1
+            urn:schemas-sony-com:serviceId:IRCC
+            /IRCCSCPD.xml
+            /upnp/control/IRCC
+            
+         
+      
+      
+      
+         1.0
+         
+            
+               AAMAABxa
+            
+         
+      
+      
+         1.3
+         http://192.168.1.123:50002/actionList
+      
+      
+         1.0
+         false
+         50004
+      
+   
+
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@XStreamAlias("device") +public class IrccDevice { + /** Represents the device type (URN) */ + @XStreamAlias("deviceType") + private @Nullable String deviceType; + + /** Represents the friendly name (usually set on the device) */ + @XStreamAlias("friendlyName") + private @Nullable String friendlyName; + + /** Represents the manufacturer (should be Sony Corporation) */ + @XStreamAlias("manufacturer") + private @Nullable String manufacturer; + + /** Represents the manufacturer URL (should be www.sony.com) */ + @XStreamAlias("manufacturerURL") + private @Nullable String manufacturerURL; + + /** Represents the model # of the device */ + @XStreamAlias("modelName") + private @Nullable String modelName; + + /** Represents the unique ID (UUID) of the device - generally GEN 3 UUID */ + @XStreamAlias("UDN") + private @Nullable String udn; + + /** Represents the services that the device support */ + @XStreamAlias("serviceList") + private @Nullable UpnpServiceList services; + + /** Represents sony extended device information */ + @XStreamAlias("X_UNR_DeviceInfo") + private @Nullable IrccUnrDeviceInfo unrDeviceInfo; + + /** Represents any additional codes supported by the device */ + @XStreamAlias("X_IRCCCodeList") + private @Nullable IrccCodeList codeList; + + /** + * Returns the device type + * + * @return the possibly null, possibly empty device type + */ + public @Nullable String getDeviceType() { + return deviceType; + } + + /** + * Returns the friendly name + * + * @return the possibly null, possibly empty friendly name + */ + public @Nullable String getFriendlyName() { + return friendlyName; + } + + /** + * Returns the manufacturer + * + * @return the possibly null, possibly empty manufacturer + */ + public @Nullable String getManufacturer() { + return manufacturer; + } + + /** + * Returns the manufacturer URL + * + * @return the possibly null, possibly empty manufacturer URL + */ + public @Nullable String getManufacturerURL() { + return manufacturerURL; + } + + /** + * Returns the model name + * + * @return the possibly null, possibly empty model name + */ + public @Nullable String getModelName() { + return modelName; + } + + /** + * Returns the unique ID (UUID) + * + * @return the possibly null, possibly empty unique ID (UUID) + */ + public @Nullable String getUdn() { + return udn; + } + + /** + * Returns the list of supported services + * + * @return the non-null, possibly empty list of services + */ + public List getServices() { + final UpnpServiceList srvc = services; + return srvc == null ? Collections.emptyList() : srvc.getServices(); + } + + /** + * Returns the {@link IrccUnrDeviceInfo} + * + * @return the possibly null {@link IrccUnrDeviceInfo} + */ + public @Nullable IrccUnrDeviceInfo getUnrDeviceInfo() { + return unrDeviceInfo; + } + + /** + * Returns the {@link IrccCodeList} + * + * @return the possibly null {@link IrccCodeList} + */ + public @Nullable IrccCodeList getCodeList() { + return codeList; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccInfoItem.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccInfoItem.java new file mode 100644 index 0000000000000..1a8cb310bb0ab --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccInfoItem.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; + +/** + * This class represents the deserialized results of an IRCC info item. The following is an example of the results that + * will be deserialized. + * + *
+ * {@code
+      
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@XStreamAlias("infoItem") +class IrccInfoItem { + /** The name of the info item */ + @XStreamAsAttribute + @XStreamAlias("field") + private @Nullable String name; + + /** The value related to the name */ + @XStreamAsAttribute + @XStreamAlias("value") + private @Nullable String value; + + /** + * Gets the name + * + * @return the possibly null, possibly empty name + */ + public @Nullable String getName() { + return name; + } + + /** + * Gets the value + * + * @return the possibly null, possibly empty value + */ + public @Nullable String getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccRemoteCommand.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccRemoteCommand.java new file mode 100644 index 0000000000000..c80d2654e467e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccRemoteCommand.java @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +/** + * This class represents the deserialized results of an IRCC remote command. The following is an example of the results + * that will be deserialized. + * + * + *
+ * {@code
+    
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +public class IrccRemoteCommand { + /** The representing an IRCC remote command type */ + public static final String IRCC = "ircc"; + + /** The representing an URL remote command type */ + public static final String URL = "url"; + + /** The name of the remote command */ + private final String name; + + /** The type of command (url, ircc) */ + private final String type; + + /** The value for the command */ + private final String cmd; + + /** + * Instantiates a new ircc remote command + * + * @param name the non-null, non-empty remote command name + * @param type the non-null, non-empty remote command type + * @param cmd the non-null, non-empty remote command value + */ + IrccRemoteCommand(final String name, final String type, final String cmd) { + SonyUtil.validateNotEmpty(name, "name cannot be empty"); + SonyUtil.validateNotEmpty(type, "type cannot be empty"); + SonyUtil.validateNotEmpty(cmd, "cmd cannot be empty"); + + this.name = name; + this.type = type; + // fix a bug in some IRCC systems + this.cmd = cmd.replace(":80:80", ":80"); + } + + /** + * Gets the remote command name + * + * @return the non-null, non-empty command name + */ + public String getName() { + return name; + } + + /** + * Gets the remote command type + * + * @return the non-null, non-empty command type + */ + public String getType() { + return type; + } + + /** + * Gets the remote command value + * + * @return the non-null, non-empty command value + */ + public String getCmd() { + return cmd; + } + + @Override + public String toString() { + return name + " (" + type + "): " + cmd; + } + + /** + * The converter used to unmarshal the {@link IrccRemoteCommandConverter}. Please note this should only be used to + * unmarshal XML (marshaling will throw a {@link UnsupportedOperationException}) + * + * @author Tim Roberts - Initial contribution + */ + static class IrccRemoteCommandConverter implements Converter { + @Override + public boolean canConvert(@SuppressWarnings("rawtypes") final @Nullable Class clazz) { + return IrccRemoteCommand.class.equals(clazz); + } + + @Override + public void marshal(final @Nullable Object obj, final @Nullable HierarchicalStreamWriter writer, + final @Nullable MarshallingContext context) { + throw new UnsupportedOperationException(); + } + + @Override + @Nullable + public Object unmarshal(final @Nullable HierarchicalStreamReader reader, + final @Nullable UnmarshallingContext context) { + Objects.requireNonNull(reader, "reader cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + final String name = reader.getAttribute("name"); + if (name.isEmpty()) { + return null; + } + + final String type = reader.getAttribute("type"); + if (type.isEmpty()) { + return null; + } + + final String value = reader.getAttribute("value"); + if (value.isEmpty()) { + return null; + } + + return new IrccRemoteCommand(name, type, value); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccRemoteCommands.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccRemoteCommands.java new file mode 100644 index 0000000000000..f114662bf8ec5 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccRemoteCommands.java @@ -0,0 +1,402 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +/** + * This class represents the deserialized results of an IRCC remote command list. The following is an example of the + * results that will be deserialized. + * + *
+ * {@code
+     
+     
+          
+          
+          
+          
+          
+          
+          
+          
+          
+     
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@XStreamAlias("remoteCommandList") +public class IrccRemoteCommands { + /** The logger used by the client */ + private final Logger logger = LoggerFactory.getLogger(IrccRemoteCommands.class); + + /** + * The map of remote commands by the remote command name (lowercased) + */ + private final Map remoteCmds = new HashMap<>(); + + /** + * Constructs the remote commands using the {@link #getDefaultCommands()} + */ + public IrccRemoteCommands() { + this(null); + } + + /** + * Constructs the remote commands from the given cmds (or the {@link #getDefaultCommands()} if null or empty) + * + * @param cmds a possibly null, possibly empty map of commands + */ + private IrccRemoteCommands(final @Nullable Map cmds) { + remoteCmds.putAll(cmds == null || cmds.isEmpty() ? getDefaultCommands() : cmds); + } + + /** + * Constructs the default list of commands (that many of the IRCC systems obey) + * + * @return a non-null, non-empty map of {@link IrccRemoteCommand} + */ + public static Map getDefaultCommands() { + final Map remoteCmds = new HashMap<>(); + remoteCmds.put("num1", new IrccRemoteCommand("Num1", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAAAw==")); + remoteCmds.put("num2", new IrccRemoteCommand("Num2", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAABAw==")); + remoteCmds.put("num3", new IrccRemoteCommand("Num3", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAACAw==")); + remoteCmds.put("num4", new IrccRemoteCommand("Num4", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAADAw==")); + remoteCmds.put("num5", new IrccRemoteCommand("Num5", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAEAw==")); + remoteCmds.put("num6", new IrccRemoteCommand("Num6", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAFAw==")); + remoteCmds.put("num7", new IrccRemoteCommand("Num7", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAGAw==")); + remoteCmds.put("num8", new IrccRemoteCommand("Num8", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAHAw==")); + remoteCmds.put("num9", new IrccRemoteCommand("Num9", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAIAw==")); + remoteCmds.put("num0", new IrccRemoteCommand("Num0", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAJAw==")); + remoteCmds.put("num11", new IrccRemoteCommand("Num11", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAKAw==")); + remoteCmds.put("num12", new IrccRemoteCommand("Num12", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAALAw==")); + remoteCmds.put("enter", new IrccRemoteCommand("Enter", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAALAw==")); + remoteCmds.put("gguide", new IrccRemoteCommand("GGuide", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAOAw==")); + remoteCmds.put("channelup", new IrccRemoteCommand("ChannelUp", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAQAw==")); + remoteCmds.put("channeldown", + new IrccRemoteCommand("ChannelDown", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAARAw==")); + remoteCmds.put("volumeup", new IrccRemoteCommand("VolumeUp", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAASAw==")); + remoteCmds.put("volumedown", + new IrccRemoteCommand("VolumeDown", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAATAw==")); + remoteCmds.put("mute", new IrccRemoteCommand("Mute", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAUAw==")); + remoteCmds.put("tvpower", new IrccRemoteCommand("TvPower", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAVAw==")); + remoteCmds.put("audio", new IrccRemoteCommand("Audio", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAXAw==")); + remoteCmds.put("mediaaudiotrack", + new IrccRemoteCommand("MediaAudioTrack", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAXAw==")); + remoteCmds.put("tv", new IrccRemoteCommand("Tv", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAkAw==")); + remoteCmds.put("input", new IrccRemoteCommand("Input", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAlAw==")); + remoteCmds.put("tvinput", new IrccRemoteCommand("TvInput", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAlAw==")); + remoteCmds.put("tvantennacable", + new IrccRemoteCommand("TvAntennaCable", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAqAw==")); + remoteCmds.put("wakeup", new IrccRemoteCommand("WakeUp", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAuAw==")); + remoteCmds.put("poweroff", new IrccRemoteCommand("PowerOff", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAvAw==")); + remoteCmds.put("sleep", new IrccRemoteCommand("Sleep", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAvAw==")); + remoteCmds.put("right", new IrccRemoteCommand("Right", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAAzAw==")); + remoteCmds.put("left", new IrccRemoteCommand("Left", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAA0Aw==")); + remoteCmds.put("sleeptimer", + new IrccRemoteCommand("SleepTimer", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAA2Aw==")); + remoteCmds.put("analog2", new IrccRemoteCommand("Analog2", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAA4Aw==")); + remoteCmds.put("tvanalog", new IrccRemoteCommand("TvAnalog", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAA4Aw==")); + remoteCmds.put("display", new IrccRemoteCommand("Display", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAA6Aw==")); + remoteCmds.put("jump", new IrccRemoteCommand("Jump", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAA7Aw==")); + remoteCmds.put("picoff", new IrccRemoteCommand("PicOff", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAA+Aw==")); + remoteCmds.put("pictureoff", + new IrccRemoteCommand("PictureOff", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAA+Aw==")); + remoteCmds.put("teletext", new IrccRemoteCommand("Teletext", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAA\\/Aw==")); + remoteCmds.put("video1", new IrccRemoteCommand("Video1", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAABAAw==")); + remoteCmds.put("video2", new IrccRemoteCommand("Video2", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAABBAw==")); + remoteCmds.put("analogrgb1", + new IrccRemoteCommand("AnalogRgb1", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAABDAw==")); + remoteCmds.put("home", new IrccRemoteCommand("Home", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAABgAw==")); + remoteCmds.put("exit", new IrccRemoteCommand("Exit", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAABjAw==")); + remoteCmds.put("picturemode", + new IrccRemoteCommand("PictureMode", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAABkAw==")); + remoteCmds.put("confirm", new IrccRemoteCommand("Confirm", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAABlAw==")); + remoteCmds.put("up", new IrccRemoteCommand("Up", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAB0Aw==")); + remoteCmds.put("down", new IrccRemoteCommand("Down", IrccRemoteCommand.IRCC, "AAAAAQAAAAEAAAB1Aw==")); + remoteCmds.put("closedcaption", + new IrccRemoteCommand("ClosedCaption", IrccRemoteCommand.IRCC, "AAAAAgAAAKQAAAAQAw==")); + remoteCmds.put("component1", + new IrccRemoteCommand("Component1", IrccRemoteCommand.IRCC, "AAAAAgAAAKQAAAA2Aw==")); + remoteCmds.put("component2", + new IrccRemoteCommand("Component2", IrccRemoteCommand.IRCC, "AAAAAgAAAKQAAAA3Aw==")); + remoteCmds.put("wide", new IrccRemoteCommand("Wide", IrccRemoteCommand.IRCC, "AAAAAgAAAKQAAAA9Aw==")); + remoteCmds.put("epg", new IrccRemoteCommand("EPG", IrccRemoteCommand.IRCC, "AAAAAgAAAKQAAABbAw==")); + remoteCmds.put("pap", new IrccRemoteCommand("PAP", IrccRemoteCommand.IRCC, "AAAAAgAAAKQAAAB3Aw==")); + remoteCmds.put("tenkey", new IrccRemoteCommand("TenKey", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAMAw==")); + remoteCmds.put("bscs", new IrccRemoteCommand("BSCS", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAQAw==")); + remoteCmds.put("ddata", new IrccRemoteCommand("Ddata", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAVAw==")); + remoteCmds.put("stop", new IrccRemoteCommand("Stop", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAYAw==")); + remoteCmds.put("pause", new IrccRemoteCommand("Pause", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAZAw==")); + remoteCmds.put("play", new IrccRemoteCommand("Play", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAaAw==")); + remoteCmds.put("rewind", new IrccRemoteCommand("Rewind", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAbAw==")); + remoteCmds.put("forward", new IrccRemoteCommand("Forward", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAcAw==")); + remoteCmds.put("dot", new IrccRemoteCommand("DOT", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAdAw==")); + remoteCmds.put("rec", new IrccRemoteCommand("Rec", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAgAw==")); + remoteCmds.put("return", new IrccRemoteCommand("Return", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAjAw==")); + remoteCmds.put("blue", new IrccRemoteCommand("Blue", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAkAw==")); + remoteCmds.put("red", new IrccRemoteCommand("Red", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAlAw==")); + remoteCmds.put("green", new IrccRemoteCommand("Green", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAmAw==")); + remoteCmds.put("yellow", new IrccRemoteCommand("Yellow", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAnAw==")); + remoteCmds.put("subtitle", new IrccRemoteCommand("SubTitle", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAoAw==")); + remoteCmds.put("cs", new IrccRemoteCommand("CS", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAArAw==")); + remoteCmds.put("bs", new IrccRemoteCommand("BS", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAsAw==")); + remoteCmds.put("digital", new IrccRemoteCommand("Digital", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAAyAw==")); + remoteCmds.put("options", new IrccRemoteCommand("Options", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAA2Aw==")); + remoteCmds.put("media", new IrccRemoteCommand("Media", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAA4Aw==")); + remoteCmds.put("prev", new IrccRemoteCommand("Prev", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAA8Aw==")); + remoteCmds.put("next", new IrccRemoteCommand("Next", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAA9Aw==")); + remoteCmds.put("dpadcenter", + new IrccRemoteCommand("DpadCenter", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAABKAw==")); + remoteCmds.put("cursorup", new IrccRemoteCommand("CursorUp", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAABPAw==")); + remoteCmds.put("cursordown", + new IrccRemoteCommand("CursorDown", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAABQAw==")); + remoteCmds.put("cursorleft", + new IrccRemoteCommand("CursorLeft", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAABNAw==")); + remoteCmds.put("cursorright", + new IrccRemoteCommand("CursorRight", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAABOAw==")); + remoteCmds.put("shopremotecontrolforceddynamic", new IrccRemoteCommand("ShopRemoteControlForcedDynamic", + IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAABqAw==")); + remoteCmds.put("flashplus", new IrccRemoteCommand("FlashPlus", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAB4Aw==")); + remoteCmds.put("flashminus", + new IrccRemoteCommand("FlashMinus", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAB5Aw==")); + remoteCmds.put("audioqualitymode", + new IrccRemoteCommand("AudioQualityMode", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAB7Aw==")); + remoteCmds.put("demomode", new IrccRemoteCommand("DemoMode", IrccRemoteCommand.IRCC, "AAAAAgAAAJcAAAB8Aw==")); + remoteCmds.put("analog", new IrccRemoteCommand("Analog", IrccRemoteCommand.IRCC, "AAAAAgAAAHcAAAANAw==")); + remoteCmds.put("mode3d", new IrccRemoteCommand("Mode3D", IrccRemoteCommand.IRCC, "AAAAAgAAAHcAAABNAw==")); + remoteCmds.put("digitaltoggle", + new IrccRemoteCommand("DigitalToggle", IrccRemoteCommand.IRCC, "AAAAAgAAAHcAAABSAw==")); + remoteCmds.put("demosurround", + new IrccRemoteCommand("DemoSurround", IrccRemoteCommand.IRCC, "AAAAAgAAAHcAAAB7Aw==")); + remoteCmds.put("*ad", new IrccRemoteCommand("*AD", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAAA7Aw==")); + remoteCmds.put("audiomixup", + new IrccRemoteCommand("AudioMixUp", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAAA8Aw==")); + remoteCmds.put("audiomixdown", + new IrccRemoteCommand("AudioMixDown", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAAA9Aw==")); + remoteCmds.put("tv_radio", new IrccRemoteCommand("Tv_Radio", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAABXAw==")); + remoteCmds.put("syncmenu", new IrccRemoteCommand("SyncMenu", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAABYAw==")); + remoteCmds.put("hdmi1", new IrccRemoteCommand("Hdmi1", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAABaAw==")); + remoteCmds.put("hdmi2", new IrccRemoteCommand("Hdmi2", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAABbAw==")); + remoteCmds.put("hdmi3", new IrccRemoteCommand("Hdmi3", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAABcAw==")); + remoteCmds.put("hdmi4", new IrccRemoteCommand("Hdmi4", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAABdAw==")); + remoteCmds.put("topmenu", new IrccRemoteCommand("TopMenu", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAABgAw==")); + remoteCmds.put("popupmenu", new IrccRemoteCommand("PopUpMenu", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAABhAw==")); + remoteCmds.put("onetouchtimerec", + new IrccRemoteCommand("OneTouchTimeRec", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAABkAw==")); + remoteCmds.put("onetouchview", + new IrccRemoteCommand("OneTouchView", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAABlAw==")); + remoteCmds.put("dux", new IrccRemoteCommand("DUX", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAABzAw==")); + remoteCmds.put("footballmode", + new IrccRemoteCommand("FootballMode", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAAB2Aw==")); + remoteCmds.put("imanual", new IrccRemoteCommand("iManual", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAAB7Aw==")); + remoteCmds.put("netflix", new IrccRemoteCommand("Netflix", IrccRemoteCommand.IRCC, "AAAAAgAAABoAAAB8Aw==")); + remoteCmds.put("assists", new IrccRemoteCommand("Assists", IrccRemoteCommand.IRCC, "AAAAAgAAAMQAAAA7Aw==")); + remoteCmds.put("actionmenu", + new IrccRemoteCommand("ActionMenu", IrccRemoteCommand.IRCC, "AAAAAgAAAMQAAABLAw==")); + remoteCmds.put("help", new IrccRemoteCommand("Help", IrccRemoteCommand.IRCC, "AAAAAgAAAMQAAABNAw==")); + remoteCmds.put("tvsatellite", + new IrccRemoteCommand("TvSatellite", IrccRemoteCommand.IRCC, "AAAAAgAAAMQAAABOAw==")); + remoteCmds.put("wirelesssubwoofer", + new IrccRemoteCommand("WirelessSubwoofer", IrccRemoteCommand.IRCC, "AAAAAgAAAMQAAAB+Aw==")); + return remoteCmds; + } + + /** + * Constructs a new {@link IrccRemoteCommands} that is a merge between this object and the given + * {@link IrccCodeList}. If the {@link IrccCodeList} is null, the current instance is returned + * + * @param codeList a possibly null {@link IrccCodeList} + * @return a non-null {@link IrccRemoteCommands} + */ + public IrccRemoteCommands withCodeList(final @Nullable IrccCodeList codeList) { + if (codeList == null) { + return this; + } + + final Map cmds = new HashMap<>(); + cmds.putAll(remoteCmds); + for (final IrccCode cmd : codeList.getCommands()) { + final String cmdName = cmd.getCommand(); + final String cmdValue = cmd.getValue(); + + if (!cmdName.isEmpty() && !cmdValue.isEmpty()) { + final Optional existingCmd = cmds.values().stream() + .filter(rc -> cmdValue.equals(rc.getCmd())).findFirst(); + if (existingCmd.isPresent()) { + logger.debug("Cannot add code list {} to commands as command {} already exists for {}", cmdName, + cmdValue, existingCmd.get().getName()); + } else { + cmds.put(cmdName.toLowerCase(), new IrccRemoteCommand(cmdName, IrccRemoteCommand.IRCC, cmdValue)); + } + } + } + + return new IrccRemoteCommands(cmds); + } + + /** + * Gets the remote commands by the remote command name (lowercased) + * + * @return the non-null, non-empty map of remote commands + */ + public Map getRemoteCommands() { + return Collections.unmodifiableMap(remoteCmds); + } + + /** + * Gets the {@link IrccRemoteCommand} that represents the power on command + * + * @return the {@link IrccRemoteCommand} representing power on or null if not found + */ + public @Nullable IrccRemoteCommand getPowerOn() { + if (remoteCmds.containsKey("power on")) { + return remoteCmds.get("power on"); + } + + if (remoteCmds.containsKey("poweron")) { + return remoteCmds.get("poweron"); + } + + if (remoteCmds.containsKey("tvpower")) { + return remoteCmds.get("tvpower"); + } + + if (remoteCmds.containsKey("powermain")) { + return remoteCmds.get("powermain"); + } + + if (remoteCmds.containsKey("power")) { + return remoteCmds.get("power"); + } + + if (remoteCmds.containsKey("sleep")) { + return remoteCmds.get("sleep"); + } + + // make a guess if something contains power on or power + IrccRemoteCommand powerCmd = null; + for (final IrccRemoteCommand cmd : remoteCmds.values()) { + final String name = cmd.getName(); + if (name.toLowerCase().contains("power on")) { + return cmd; + } else if (name.toLowerCase().contains("power")) { + powerCmd = cmd; + break; + } + } + + return powerCmd; + } + + /** + * Gets the {@link IrccRemoteCommand} that represents the power off command + * + * @return the {@link IrccRemoteCommand} representing power off or null if not found + */ + public @Nullable IrccRemoteCommand getPowerOff() { + if (remoteCmds.containsKey("power off")) { + return remoteCmds.get("power off"); + } + + if (remoteCmds.containsKey("poweroff")) { + return remoteCmds.get("poweroff"); + } + + if (remoteCmds.containsKey("tvpower")) { + return remoteCmds.get("tvpower"); + } + + if (remoteCmds.containsKey("powermain")) { + return remoteCmds.get("powermain"); + } + + if (remoteCmds.containsKey("power")) { + return remoteCmds.get("power"); + } + + // make a guess if something contains power off or power + IrccRemoteCommand powerCmd = null; + for (final IrccRemoteCommand cmd : remoteCmds.values()) { + final String name = cmd.getName(); + if (name.toLowerCase().contains("power on")) { + return cmd; + } else if (name.toLowerCase().contains("power")) { + powerCmd = cmd; + break; + } + } + + return powerCmd; + } + + /** + * The converter used to unmarshal the {@link IrccRemoteCommands}. Please note this should only be used to unmarshal + * XML (marshaling will throw a {@link UnsupportedOperationException}) + * + * @author Tim Roberts - Initial contribution + */ + static class IrccRemoteCommandsConverter implements Converter { + @Override + public boolean canConvert(@SuppressWarnings("rawtypes") final @Nullable Class clazz) { + return IrccRemoteCommands.class.equals(clazz); + } + + @Override + public void marshal(final @Nullable Object obj, final @Nullable HierarchicalStreamWriter writer, + final @Nullable MarshallingContext context) { + throw new UnsupportedOperationException(); + } + + @Override + public @Nullable Object unmarshal(final @Nullable HierarchicalStreamReader reader, + final @Nullable UnmarshallingContext context) { + Objects.requireNonNull(reader, "reader cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + final Map cmds = new HashMap<>(); + while (reader.hasMoreChildren()) { + reader.moveDown(); + final IrccRemoteCommand cmd = (IrccRemoteCommand) context.convertAnother(this, IrccRemoteCommand.class); + final String cmdName = cmd == null ? null : cmd.getName(); + + if (cmd != null && cmdName != null) { + cmds.put(cmdName.toLowerCase(), cmd); + } + + reader.moveUp(); + } + return new IrccRemoteCommands(cmds); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccRoot.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccRoot.java new file mode 100644 index 0000000000000..f2c34ffadc9b8 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccRoot.java @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * This class represents the root element in the XML for a IRCC device. The XML that will be deserialized will look like + * + *
+ * {@code
+ 
+  
+    
+      1
+      0
+    
+    
+      urn:schemas-upnp-org:device:Basic:1
+      XBR-55X900E
+      Sony Corporation
+      http://www.sony.net/
+      XBR-55X900E
+      uuid:25257d9e-41b1-43df-9332-ffe401305cf4
+
+      
+        
+          image/jpeg
+          120
+          120
+          24
+          /sony/webapi/ssdp/icon/dlna_tv_120.jpg
+        
+        
+          image/png
+          120
+          120
+          24
+          /sony/webapi/ssdp/icon/dlna_tv_120.png
+        
+        
+          image/jpeg
+          32
+          32
+          24
+          /sony/webapi/ssdp/icon/dlna_tv_32.jpg
+        
+        
+          image/png
+          32
+          32
+          24
+          /sony/webapi/ssdp/icon/dlna_tv_32.png
+        
+        
+          image/jpeg
+          48
+          48
+          24
+          /sony/webapi/ssdp/icon/dlna_tv_48.jpg
+        
+        
+          image/png
+          48
+          48
+          24
+          /sony/webapi/ssdp/icon/dlna_tv_48.png
+        
+        
+          image/jpeg
+          60
+          60
+          24
+          /sony/webapi/ssdp/icon/dlna_tv_60.jpg
+        
+        
+          image/png
+          60
+          60
+          24
+          /sony/webapi/ssdp/icon/dlna_tv_60.png
+        
+      
+      
+        
+          urn:schemas-sony-com:service:ScalarWebAPI:1
+          urn:schemas-sony-com:serviceId:ScalarWebAPI
+          /sony/webapi/ssdp/scpd/WebApiSCPD.xml
+          http://192.168.1.190/sony
+          
+        
+        
+          urn:schemas-sony-com:service:IRCC:1
+          urn:schemas-sony-com:serviceId:IRCC
+          http://192.168.1.190/sony/ircc/IRCCSCPD.xml
+          http://192.168.1.190/sony/ircc
+          
+        
+      
+      
+        1.0
+        http://192.168.1.190/sony
+        
+          guide        avContent        cec        audio        accessControl        system        appControl        videoScreen        encryption        contentshare
+        
+      
+      
+        1.0
+        
+          
+            AAEAAAAB
+          
+          
+            AAIAAACk
+          
+          
+            AAIAAACX
+          
+          
+            AAIAAAB3
+          
+          
+            AAIAAAAa
+          
+        
+      
+      
+        AAAAAQAAAAEAAAAVAw==
+      
+      
+        1.0
+        true
+        false
+        59095
+      
+    
+  
+ * }
+ * 
+ * + * Please note this class is used strictly in the deserialization process and retrieval of the {@link IrccDevice} + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@XStreamAlias("root") +public class IrccRoot { + /** The IRCC device that is part of the root */ + @XStreamAlias("device") + private @Nullable IrccDevice device; + + /** + * Returns the IRCC device or null if none found + * + * @return a possibly null {@link IrccDevice} + */ + public @Nullable IrccDevice getDevice() { + return device; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccStatus.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccStatus.java new file mode 100644 index 0000000000000..7b077821e7a66 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccStatus.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents the status element in the XML for a IRCC device. The XML that will be deserialized will look + * like + * + *
+ * {@code
+    
+        
+        
+        
+        
+        
+    
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@XStreamAlias("status") +public class IrccStatus { + + /** The constant where the status is on a text input */ + public static final String TEXTINPUT = "textInput"; + + /** The constant where the status is on a disc */ + public static final String DISC = "disc"; + + /** The constant where the status is on a browser */ + public static final String WEBBROWSER = "webBrowse"; + + /** The constant where the status is a cursor */ + public static final String CURSORDISPLAY = "cursorDisplay"; + + /** The constant where the status is viewing something */ + public static final String VIEWING = "viewing"; + + /** The name for the status (one of the constants above) */ + @XStreamAsAttribute + @XStreamAlias("name") + private @Nullable String name; + + /** The various items making up the status */ + @Nullable + @XStreamImplicit + private List<@Nullable IrccStatusItem> items; + + /** + * Gets the status name + * + * @return the possibly null, possibly empty status name + */ + public @Nullable String getName() { + return name; + } + + /** + * Gets the the particular status item value + * + * @param name the non-null, non-empty item field name + * @return the possibly null value for the field + */ + public @Nullable String getItemValue(final String name) { + SonyUtil.validateNotEmpty(name, "name cannot be empty"); + + final List<@Nullable IrccStatusItem> localItems = items; + if (localItems != null) { + for (final IrccStatusItem item : localItems) { + if (item != null && name.equalsIgnoreCase(item.getField())) { + return item.getValue(); + } + } + } + return null; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccStatusItem.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccStatusItem.java new file mode 100644 index 0000000000000..a603167531138 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccStatusItem.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; + +/** + * This class represents the status element item in the XML for a IRCC device. The XML that will be deserialized will + * look like: + * + *
+ * {@code
+    
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@XStreamAlias("statusItem") +public class IrccStatusItem { + + /** The constant for the class field (only valid for a status of {@link IrccStatus#VIEWING}) */ + public static final String CLASS = "class"; + + /** The constant for the identifier field (only valid for a status of {@link IrccStatus#VIEWING}) */ + public static final String ID = "id"; + + /** The constant for the title field (only valid for a status of {@link IrccStatus#VIEWING}) */ + public static final String TITLE = "title"; + + /** The constant for the source field (only valid for a status of {@link IrccStatus#VIEWING}) */ + public static final String SOURCE = "source"; + + /** The constant for the source2 field (only valid for a status of {@link IrccStatus#VIEWING}) */ + public static final String SOURCE2 = "zone2Source"; + + /** The constant for the duration field (only valid for a status of {@link IrccStatus#VIEWING}) */ + public static final String DURATION = "duration"; + + // fyi - DISC has "type", "mediatype", "mediaformat" - not used + + /** The field name identifing the status item */ + @XStreamAsAttribute + @XStreamAlias("field") + private @Nullable String field; + + /** The value of the field */ + @XStreamAsAttribute + @XStreamAlias("value") + private @Nullable String value; + + /** + * Gets the field name + * + * @return the possibly null, possibly empty field name + */ + public @Nullable String getField() { + return field; + } + + /** + * Gets the field value + * + * @return the possibly null, possibly empty field name + */ + public @Nullable String getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccStatusList.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccStatusList.java new file mode 100644 index 0000000000000..43c61eb637032 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccStatusList.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents the status list element in the XML for a IRCC device. The XML that will be deserialized will + * look like: + * + *
+ * {@code
+    
+    
+      
+        
+        
+        
+      
+    
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@XStreamAlias("statusList") +public class IrccStatusList { + + /** The list of statuses */ + @XStreamImplicit + private @Nullable List<@Nullable IrccStatus> statuses; + + /** + * Parses's the {@link IrccStatusList} from the XML + * + * @param xml the non-null, non-empty XML to parse + * @return a possibly null {@link IrccStatusList} + */ + public static @Nullable IrccStatusList get(final String xml) { + SonyUtil.validateNotEmpty(xml, "xml cannot be empty"); + return IrccXmlReader.STATUS.fromXML(xml); + } + + /** + * Returns true if the status is a {@link IrccStatus#TEXTINPUT} + * + * @return true if a text input, false otherwise + */ + public boolean isTextInput() { + final List<@Nullable IrccStatus> localStatuses = statuses; + if (localStatuses != null) { + for (final IrccStatus status : localStatuses) { + if (status != null && IrccStatus.TEXTINPUT.equalsIgnoreCase(status.getName())) { + return true; + } + } + } + return false; + } + + /** + * Returns true if the status is a {@link IrccStatus#WEBBROWSER} + * + * @return true if a web browser, false otherwise + */ + public boolean isWebBrowse() { + final List<@Nullable IrccStatus> localStatuses = statuses; + if (localStatuses != null) { + for (final IrccStatus status : localStatuses) { + if (status != null && IrccStatus.WEBBROWSER.equalsIgnoreCase(status.getName())) { + return true; + } + } + } + return false; + } + + /** + * Returns true if the status is a {@link IrccStatus#DISC} + * + * @return true if a disk, false otherwise + */ + public boolean isDisk() { + final List<@Nullable IrccStatus> localStatuses = statuses; + if (localStatuses != null) { + for (final IrccStatus status : localStatuses) { + if (status != null && IrccStatus.DISC.equalsIgnoreCase(status.getName())) { + return true; + } + } + } + return false; + } + + /** + * Gets the viewing status + * + * @return the possibly null (if not {@link IrccStatus#VIEWING}) {@link IrccStatus} + */ + public @Nullable IrccStatus getViewing() { + final List<@Nullable IrccStatus> localStatuses = statuses; + if (localStatuses != null) { + for (final IrccStatus status : localStatuses) { + if (status != null && IrccStatus.VIEWING.equalsIgnoreCase(status.getName())) { + return status; + } + } + } + return null; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccSystemInformation.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccSystemInformation.java new file mode 100644 index 0000000000000..6a361454b8c77 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccSystemInformation.java @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents the deserialized results of an IRCC system information query. The following is an example of + * the results that will be deserialized: + * + *
+ * {@code
+    
+    
+        BDPlayer
+        2017
+        RMT-B119A
+        RMT-B120A
+        RMT-B122A
+        RMT-B123A
+        RMT-B126A
+        RMT-B119J
+        RMT-B127J
+        RMT-B119P
+        RMT-B120P
+        RMT-B121P
+        RMT-B122P
+        RMT-B127P
+        RMT-B119C
+        RMT-B120C
+        RMT-B122C
+        RMT-B127C
+        RMT-B127T
+        RMT-B115A
+        
+        
+            video
+            music
+        
+        
+            BD
+            DVD
+            CD
+            Net
+        
+        
+            
+            
+                
+            
+        
+    
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@XStreamAlias("systemInformation") +public class IrccSystemInformation { + + /** The action header for the system information */ + @XStreamAlias("actionHeader") + @XStreamAsAttribute + private @Nullable String actionHeader; + + /** The supported functions */ + @XStreamAlias("supportFunction") + private @Nullable SupportedFunction supportedFunction; + + /** + * Gets the action header or returns "CERS-DEVICE-ID" if none found + * + * @return the action header or "CERS-DEVICE-ID" if none found + */ + public String getActionHeader() { + final String ah = actionHeader; + return ah != null && !ah.isEmpty() ? ah : "CERS-DEVICE-ID"; + } + + /** + * Gets the WOL mac address + * + * @return a possibly null (if not found), never empty MAC address + */ + public @Nullable String getWolMacAddress() { + final SupportedFunction sf = supportedFunction; + if (sf == null) { + return null; + } + + final List funcs = sf.functions; + if (funcs != null) { + for (final Function func : funcs) { + if ("wol".equalsIgnoreCase(func.name)) { + final List<@Nullable FunctionItem> localItems = func.items; + if (localItems != null) { + for (final FunctionItem fi : localItems) { + if (fi != null && "mac".equalsIgnoreCase(fi.field) && fi.value != null + && fi.value.isEmpty()) { + return fi.value; + } + } + } + } + } + } + return null; + } + + /** + * The supported function class that contains a list of functions + * + * @author Tim Roberts - Initial Contribution + */ + class SupportedFunction { + @XStreamImplicit + private @Nullable List functions; + } + + /** + * The function class that provides the name of the function and the list of items the function supports + * + * @author Tim Roberts - Initial Contribution + */ + @XStreamAlias("function") + class Function { + @XStreamAlias("name") + @XStreamAsAttribute + private @Nullable String name; + + @XStreamImplicit + private @Nullable List<@Nullable FunctionItem> items; + } + + /** + * The function item class that describes the item by it's field and value + * + * @author Tim Roberts - Initial Contribution + */ + @XStreamAlias("functionItem") + class FunctionItem { + @XStreamAlias("field") + @XStreamAsAttribute + private @Nullable String field; + + @XStreamAlias("value") + @XStreamAsAttribute + private @Nullable String value; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccText.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccText.java new file mode 100644 index 0000000000000..67313bf3f191f --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccText.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * Class that represents the text in a text field. The XML that will be deserialized will + * look like: + * + *
+ * {@code
+    
+    the text in the field
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ +@NonNullByDefault +@XStreamAlias(IrccText.TEXTROOT) +public class IrccText { + + /** The dummy root name that will be wrapped around the text element to allow parsing */ + protected static final String TEXTROOT = "textroot"; + + /** The text. */ + @XStreamAlias("text") + private @Nullable String text; + + /** + * Gets the text. + * + * @return the text + */ + public @Nullable String getText() { + return text; + } + + /** + * Get's the IRCC text from the specified XML + * + * @param xml the non-null, non-empty XML + * @return the non-null IrccText + */ + public @Nullable static IrccText get(final String xml) { + SonyUtil.validateNotEmpty(xml, "xml cannot be empty"); + + final StringBuilder sb = new StringBuilder(xml); + final int idx = sb.indexOf("= 0) { + sb.insert(idx, "<" + TEXTROOT + ">"); + sb.append(""); + } + + return IrccXmlReader.TEXT.fromXML(sb.toString()); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccUnrDeviceInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccUnrDeviceInfo.java new file mode 100644 index 0000000000000..5fc227355b959 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccUnrDeviceInfo.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * The Class IrccUnrDeviceInfo. + * + *
+ * {@code
+    
+        1.2
+        http://192.168.1.100:80/cers/ActionList.xml
+    
+  *  }
+ * 
+ * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + */ + +@NonNullByDefault +public class IrccUnrDeviceInfo { + + /** The Constant NOTSPECIFIED. */ + public static final String NOTSPECIFIED = "Not Specified"; + + /** The version. */ + @XStreamAlias("X_UNR_Version") + private final @Nullable String version; + + /** The action list url. */ + @XStreamAlias("X_CERS_ActionList_URL") + private @Nullable String actionListUrl; + + /** + * Constructs a blank UNR device info with a {@link #NOTSPECIFIED} version + */ + public IrccUnrDeviceInfo() { + version = NOTSPECIFIED; + } + + /** + * Gets the action list url. + * + * @return the action list url + */ + public @Nullable String getActionListUrl() { + return actionListUrl; + } + + /** + * Gets the version. + * + * @return the version + */ + public @Nullable String getVersion() { + return version != null && version.isEmpty() ? NOTSPECIFIED : version; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccXmlReader.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccXmlReader.java new file mode 100644 index 0000000000000..d25e85b3843d7 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/ircc/models/IrccXmlReader.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.ircc.models; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.UpnpService; +import org.openhab.binding.sony.internal.SonyHandlerFactory; +import org.openhab.binding.sony.internal.upnp.models.UpnpServiceList; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.io.xml.StaxDriver; +import com.thoughtworks.xstream.security.NoTypePermission; + +/** + * This class represents creates the various XML readers (using XStream) to deserialize various calls. + * + * @author Tim Roberts - Initial contribution + * @author andan - Adaptions for OH3 + * + * @param the generic type to cast the XML to + */ +@NonNullByDefault +public class IrccXmlReader { + /** The XStream instance */ + private final XStream xstream = new XStream(new StaxDriver()); + + /** The various reader functions */ + public static final IrccXmlReader ROOT = new IrccXmlReader<>( + new Class[] { IrccRoot.class, IrccDevice.class, IrccUnrDeviceInfo.class, IrccCodeList.class, IrccCode.class, + UpnpServiceList.class, UpnpService.class }, + new IrccCode.IrccCodeConverter()); + + public static final IrccXmlReader ACTIONS = new IrccXmlReader<>( + new Class[] { IrccActionList.class, IrccActionList.IrccAction.class }); + + public static final IrccXmlReader SYSINFO = new IrccXmlReader<>( + new Class[] { IrccSystemInformation.class, IrccSystemInformation.SupportedFunction.class, + IrccSystemInformation.Function.class, IrccSystemInformation.FunctionItem.class }); + + public static final IrccXmlReader REMOTECOMMANDS = new IrccXmlReader<>( + new Class[] { IrccRemoteCommands.class, IrccRemoteCommand.class, }, + new IrccRemoteCommand.IrccRemoteCommandConverter(), new IrccRemoteCommands.IrccRemoteCommandsConverter()); + + static final IrccXmlReader CONTENTINFO = new IrccXmlReader<>( + new Class[] { IrccContentInformation.class, IrccInfoItem.class, }); + + static final IrccXmlReader STATUS = new IrccXmlReader<>( + new Class[] { IrccStatusList.class, IrccStatus.class, IrccStatusItem.class }); + + static final IrccXmlReader TEXT = new IrccXmlReader<>(new Class[] { IrccText.class }); + static final IrccXmlReader CONTENTURL = new IrccXmlReader<>(new Class[] { IrccContentUrl.class }); + + /** + * Constructs the reader using the specified classes to process annotations with + * + * @param classes a non-null, non-empty array of classes + * @param converters a possibly empty list of converters + */ + private IrccXmlReader(@SuppressWarnings("rawtypes") final Class[] classes, final Converter... converters) { + Objects.requireNonNull(classes, "classes cannot be null"); + + // XStream.setupDefaultSecurity(xstream); + xstream.addPermission(NoTypePermission.NONE); + xstream.allowTypesByWildcard(new String[] { SonyHandlerFactory.class.getPackageName() + ".**" }); + xstream.setClassLoader(getClass().getClassLoader()); + xstream.ignoreUnknownElements(); + xstream.processAnnotations(classes); + for (final Converter conv : converters) { + xstream.registerConverter(conv); + } + } + + /** + * Will translate the XML and casts to the specified class + * + * @param xml the non-null, possibly empty XML to process + * @return the possibly null translation + */ + @SuppressWarnings("unchecked") + @Nullable + public T fromXML(final String xml) { + Objects.requireNonNull(xml, "xml cannot be null"); + + if (!xml.isEmpty()) { + return (T) this.xstream.fromXML(xml); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/FilterOption.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/FilterOption.java new file mode 100644 index 0000000000000..36daa40259035 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/FilterOption.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.net; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * The class provides a key/value option for a filter + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class FilterOption { + /** Key for the filter option */ + private final String key; + + /** Value of the filter option */ + private final Object value; + + /** + * Constructs the option from the key/value + * + * @param key a non-null, non-empty key + * @param value a non-null value + */ + public FilterOption(final String key, final Object value) { + SonyUtil.validateNotEmpty(key, "key cannot be empty"); + Objects.requireNonNull(value, "value cannot be null"); + + this.key = key; + this.value = value; + } + + /** + * Returns the key for the filter option + * + * @return a non-null, non-empty key + */ + public String getKey() { + return key; + } + + /** + * Returns the value for the filter option + * + * @return a non-null value + */ + public Object getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/Header.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/Header.java new file mode 100644 index 0000000000000..89602817c4c77 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/Header.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.net; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * The class simply represents a header as a single entity + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Header { + + /** The name of the header */ + private final String name; + + /** The value of the header */ + private final String value; + + /** + * Instantiates a new header based on the name and value + * + * @param name the non-null, non-empty name + * @param value the non-null, non-empty value + */ + public Header(final String name, final String value) { + SonyUtil.validateNotEmpty(name, "name cannot be empty"); + SonyUtil.validateNotEmpty(value, "value cannot be empty"); + + this.name = name; + this.value = value; + } + + /** + * Gets the header name + * + * @return the non-null, non-empty name + */ + public String getName() { + return name; + } + + /** + * Gets the header value + * + * @return the non-null, non-empty value + */ + public String getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/HttpRequest.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/HttpRequest.java new file mode 100644 index 0000000000000..263bf3a5ca90c --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/HttpRequest.java @@ -0,0 +1,261 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.net; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The class wrapps an HttpRequest to provide additional functionality and centeralized utility features + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class HttpRequest implements AutoCloseable { + // TODO: Those constants are Jersey specific - once we move away from Jersey, + // this must be changed to https://stackoverflow.com/a/49736022 (assuming we have a JAX-RS 2.1 implementation). + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(HttpRequest.class); + + /** The client used in communication */ + private final Client client; + + /** The headers to include in each request */ + private final Map headers = new HashMap<>(); + + /** + * Instantiates a new http request + */ + public HttpRequest(ClientBuilder clientBuilder) { + // NOTE: assumes jersey client (no JAX compliant way of doing this) + // NOTE2: jax 2.1 has a way but we don't use that + /* + * final ClientConfig configuration = new ClientConfig(); + * configuration.property(ClientProperties.CONNECT_TIMEOUT, 15000); + * configuration.property(ClientProperties.READ_TIMEOUT, 15000); + */ + + // client = ClientBuilder.newClient().property(CONNECT_TIMEOUT, 15000).property(READ_TIMEOUT, 15000); + client = clientBuilder.connectTimeout(10, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build(); + if (logger.isDebugEnabled()) { + // client.register(new LoggingFilter(new Slf4LoggingAdapter(logger), true)); + } + } + + /** + * Register a new filter with the underlying {@link Client} + * + * @param obj the non object to register + */ + public void register(final Object obj) { + Objects.requireNonNull(obj, "obj cannot be null"); + client.register(obj); + } + + /** + * Send a get command to the specified URL, adding any headers for this request + * + * @param url the non-null, non-empty url + * @param rqstHeaders the list of {@link Header} to add to the request + * @return the non-null http response + */ + public HttpResponse sendGetCommand(final String url, final Header... rqstHeaders) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + try { + final Builder request = addHeaders(client.target(url).request(), rqstHeaders); + try (final Response response = request.get()) { + // Sony may report ill-formed content response + final MultivaluedMap metadata = response.getMetadata(); + final List content = metadata.get("Content-Type"); + if (content != null) { + for (int index = 0; index < content.size(); index++) { + if (content.get(index) instanceof String entry) { + content.set(index, entry.replaceAll(".+:", "").trim()); + } + } + metadata.put("Content-Type", content); + } + return new HttpResponse(Response.fromResponse(response).replaceAll(metadata).build()); + } + } catch (ProcessingException | IllegalStateException | IOException e) { + logger.debug("Exception in sendGetCommand: {}", e.getMessage()); + return new HttpResponse(HttpStatus.SERVICE_UNAVAILABLE_503, e.getMessage()); + } + } + + /** + * Send post command for a body comprised of XML to a URL with potentially some request headers + * + * @param url the non-null, non-empty URL + * @param body the non-null, possibly empty body (of XML) + * @param rqstHeaders the list of {@link Header} to add to the request + * @return the non-null http response + */ + public HttpResponse sendPostXmlCommand(final String url, final String body, final Header... rqstHeaders) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + Objects.requireNonNull(body, "body cannot be null"); + + return sendPostCommand(url, body, MediaType.TEXT_XML + ";charset=utf-8", rqstHeaders); + } + + /** + * Send post command for a body comprised of JSON to a URL with potentially some request headers + * + * @param url the non-null, non-empty URL + * @param body the non-null, possibly empty body (of JSON) + * @param rqstHeaders the list of {@link Header} to add to the request + * @return the non-null http response + */ + public HttpResponse sendPostJsonCommand(final String url, final String body, final Header... rqstHeaders) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + Objects.requireNonNull(body, "body cannot be null"); + + return sendPostCommand(url, body, MediaType.APPLICATION_JSON, rqstHeaders); + } + + /** + * Send post command to the specified URL with the body/media type and potentially some request headers + * + * @param url the non-null, non-empty URL + * @param body the non-null, possibly empty body (of JSON) + * @param mediaType the non-null, non-empty media type + * @param rqstHeaders the list of {@link Header} to add to the request + * @return the non-null http response + */ + private HttpResponse sendPostCommand(final String url, final String body, final String mediaType, + final Header... rqstHeaders) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + Objects.requireNonNull(body, "body cannot be null"); + SonyUtil.validateNotEmpty(mediaType, "mediaType cannot be empty"); + + try { + final Builder rqst = addHeaders(client.target(url).request(mediaType), rqstHeaders); + final Response content = rqst.post(Entity.entity(body, mediaType)); + + try { + final HttpResponse httpResponse = new HttpResponse(content); + return httpResponse; + } finally { + content.close(); + } + } catch (IOException | ProcessingException e) { + return new HttpResponse(HttpStatus.SERVICE_UNAVAILABLE_503, e.getMessage()); + } + } + + /** + * Send delete command to the specified URL with the body and potentially request headers + * + * @param url the non-null, non-empty URL + * @param rqstHeaders the list of {@link Header} to add to the request + * @return the non-null http response + */ + public HttpResponse sendDeleteCommand(final String url, final Header... rqstHeaders) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + try { + final Builder rqst = addHeaders(client.target(url).request(), rqstHeaders); + final Response content = rqst.delete(); + + try { + final HttpResponse httpResponse = new HttpResponse(content); + return httpResponse; + } finally { + content.close(); + } + } catch (final IOException e) { + return new HttpResponse(HttpStatus.SERVICE_UNAVAILABLE_503, e.getMessage()); + } + } + + /** + * Helper method to add the headers (preventing duplicates) to the builder + * + * @param bld a non-null builder + * @param hdrs a possibly empty list of headers + * @return the builder with the headers added + */ + private Builder addHeaders(final Builder bld, final Header... hdrs) { + Objects.requireNonNull(bld, "bld cannot be null"); + + Builder localBuilder = bld; + final Set hdrNames = new HashSet<>(); + + for (final Header hdr : hdrs) { + final String hdrName = hdr.getName(); + if (!hdrNames.contains(hdrName)) { + hdrNames.add(hdrName); + localBuilder = localBuilder.header(hdrName, hdr.getValue()); + } + } + + for (final Entry h : headers.entrySet()) { + final String hdrName = h.getKey(); + if (!hdrNames.contains(hdrName)) { + hdrNames.add(hdrName); + localBuilder = localBuilder.header(hdrName, h.getValue()); + } + } + return localBuilder; + } + + /** + * Adds a header to ALL request made + * + * @param name the non-null, non-empty header na,e + * @param value the non-null, non-empty value + */ + public void addHeader(final String name, final String value) { + SonyUtil.validateNotEmpty(name, "name cannot be empty"); + SonyUtil.validateNotEmpty(value, "value cannot be empty"); + + headers.put(name, value); + } + + /** + * Adds a header to ALL request made + * + * @param header the non-null header to add + */ + public void addHeader(final Header header) { + Objects.requireNonNull(header, "header cannot be null"); + addHeader(header.getName(), header.getValue()); + } + + @Override + public void close() { + client.close(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/HttpResponse.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/HttpResponse.java new file mode 100644 index 0000000000000..e5ecb3b2ba60d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/HttpResponse.java @@ -0,0 +1,293 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.net; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import javax.ws.rs.core.Response; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyUtil; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * The class encapsulates an http response and provides helper methods + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class HttpResponse { + + /** The relation constant for NEXT */ + public static final String REL_NEXT = "next"; + + /** The encoding being used */ + private static final String ENCODING = "utf-8"; + + /** The character set for the encoding */ + private static final Charset CHARSET = Charset.forName(ENCODING); + + /** The error pattern identifying a SOAP error (and provides groups to get the code/desc) */ + private static final Pattern SOAPERRORPATTERN = Pattern.compile( + ".*(.*).*(.*).*", + Pattern.DOTALL | Pattern.MULTILINE); + + /** The http status code */ + private final int httpStatus; + + /** The http reason */ + private final @Nullable String httpReason; + + /** The headers from the response */ + private final Map headers = new HashMap<>(); + + /** The contents of the response */ + private final byte @Nullable [] contents; + + /** Map of relation to URI for any links shown (may be empty but never null) */ + private final Map links; + + /** + * Instantiates a new http response from the specified {@link Response} + * + * @param response the non-null response + * @throws IOException if an IO exception occurs reading from the client + */ + HttpResponse(final Response response) throws IOException { + Objects.requireNonNull(response, "response cannot be null"); + + httpStatus = response.getStatus(); + httpReason = response.getStatusInfo().getReasonPhrase(); + + if (response.hasEntity()) { + final InputStream is = response.readEntity(InputStream.class); + contents = is.readAllBytes(); + } else { + contents = null; + } + + for (final String key : response.getHeaders().keySet()) { + headers.put(key, response.getHeaderString(key)); + } + + links = response.getLinks().stream().collect(Collectors.toMap(k -> k.getRel(), v -> v.getUri())); + } + + /** + * Instantiates a new http response from the given http code and message + * + * @param httpCode the http code + * @param msg the possibly null, possibly empty msg + */ + public HttpResponse(final int httpCode, final @Nullable String msg) { + httpStatus = httpCode; + httpReason = msg; + contents = null; + links = new HashMap<>(); + } + + /** + * Gets the http status code + * + * @return the http status code + */ + public int getHttpCode() { + return httpStatus; + } + + /** + * Gets the response header corresponding to the name + * + * @param headerName the non-null, non-empty header name + * @return the response header or null if none + */ + public @Nullable String getResponseHeader(final String headerName) { + SonyUtil.validateNotEmpty(headerName, "headerName cannot be empty"); + return headers.get(headerName); + } + + /** + * Gets the content or an empty string if no content + * + * @return the content or an empty string if no content + */ + public String getContent() { + if (contents == null) { + return ""; + } + + return new String(Objects.requireNonNull(contents), CHARSET); + } + + /** + * Returns the http reason + * + * @return the http reason or null if none + */ + public @Nullable String getHttpReason() { + return httpReason; + } + + /** + * Gets the content as bytes or null if no content + * + * @return a possibly null content as bytes + */ + public byte @Nullable [] getContentAsBytes() { + return contents; + } + + /** + * Gets the content as xml. + * + * @return the content as xml + * @throws ParserConfigurationException the parser configuration exception + * @throws SAXException the SAX exception + * @throws IOException Signals that an I/O exception has occurred. + */ + public Document getContentAsXml() throws ParserConfigurationException, SAXException, IOException { + if (getHttpCode() != HttpStatus.OK_200) { + throw createException(); + } + + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setIgnoringElementContentWhitespace(true); + factory.setIgnoringComments(true); + final DocumentBuilder builder = factory.newDocumentBuilder(); + + final String content = getContent(); + if (content.isEmpty()) { + return builder.newDocument(); + } + + final InputSource inputSource = new InputSource(new StringReader(content)); + return builder.parse(inputSource); + } + + /** + * A poor mans attempt to parse out the error code/error description from a SOAP response (don't need the full SOAP + * stack) + * + * @return a SOAPError if found, null otherwise + */ + public @Nullable SOAPError getSOAPError() { + if (httpReason != null && httpReason.isEmpty()) { + return null; + } + + final Matcher m = SOAPERRORPATTERN.matcher(httpReason); + if (m.find() && m.groupCount() >= 2) { + final String code = m.group(1); + final String desc = m.group(2); + + if (!code.isEmpty() && !desc.isEmpty()) { + return new SOAPError(code, desc); + } + } + final Matcher m2 = SOAPERRORPATTERN.matcher(getContent()); + if (m2.find() && m2.groupCount() >= 2) { + final String code = m2.group(1); + final String desc = m2.group(2); + + if (!code.isEmpty() && !desc.isEmpty()) { + return new SOAPError(code, desc); + } + } + return null; + } + + /** + * Returns the link associated with the relation + * + * @param rel a non-null, non-empty relation + * @return a possibly null URI associated with the relation + */ + public @Nullable URI getLink(final String rel) { + SonyUtil.validateNotEmpty(rel, "rel cannot be empty"); + return links == null ? null : links.get(rel); + } + + /** + * Creates the exception from the http reason + * + * @return the IO exception representing the http reason + */ + public IOException createException() { + return new IOException(httpReason); + } + + @Override + public String toString() { + return getHttpCode() + " (" + (contents == null ? ("http reason: " + httpReason) : getContent()) + ")"; + } + + /** + * This class represents a SOAP error + */ + public class SOAPError { + /** The soap error code */ + private final String soapCode; + + /** The soap error description */ + private final String soapDescription; + + /** + * Creates the soap error from the code/description + * + * @param soapCode the non-null, non-empty SOAP error code + * @param soapDescription the non-null, non-empty SOAP error description + */ + private SOAPError(final String soapCode, final String soapDescription) { + SonyUtil.validateNotEmpty(soapCode, "soapCode cannot be empty"); + SonyUtil.validateNotEmpty(soapDescription, "soapDescription cannot be empty"); + this.soapCode = soapCode; + this.soapDescription = soapDescription; + } + + /** + * Returns the SOAP error code + * + * @return the non-null, non-empty SOAP error code + */ + public String getSoapCode() { + return soapCode; + } + + /** + * Returns the SOAP error description + * + * @return the non-null, non-empty SOAP error description + */ + public String getSoapDescription() { + return soapDescription; + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/NetUtil.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/NetUtil.java new file mode 100644 index 0000000000000..602bdf6a37a3f --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/NetUtil.java @@ -0,0 +1,341 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.net; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URL; +import java.util.Base64; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.core.library.types.RawType; + +/** + * This class provides utility methods related to general network activities + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class NetUtil { + /** + * Gets the remote device identifier. Sony only requires it to be similar to a mac address and constant across + * sessions. Sony AVs require the "MediaRemote:" part of the device ID (all other devices don't care). + * + * @return the non-null, non-empty device id + */ + public static String getDeviceId() { + return "MediaRemote:00-11-22-33-44-55"; + } + + /** + * Gets the remote device name. The remote name will simply be "openHab({{getDeviceId()}})" + * + * @return the non-null, non-empty device name + */ + public static String getDeviceName() { + return "openHAB (" + getDeviceId() + ")"; + } + + /** + * Creates an authorization header using the specified access code + * + * @param accessCode a non-null, non-empty access code + * @return the non-null header + */ + public static Header createAuthHeader(final String accessCode) { + SonyUtil.validateNotEmpty(accessCode, "accessCode cannot be empty"); + // left padding with "0" to size 4 + final String accessCodePadded = SonyUtil.leftPad(accessCode, 4, '0'); + final String authCode = Base64.getEncoder().encodeToString((":" + accessCodePadded).getBytes()); + return new Header("Authorization", "Basic " + authCode); + } + + /** + * Creates an access code header using the specified access code + * + * @param accessCode a non-null, non-empty access code + * @return the non-null header + */ + public static Header createAccessCodeHeader(final String accessCode) { + SonyUtil.validateNotEmpty(accessCode, "accessCode cannot be empty"); + return new Header("X-Auth-PSK", accessCode); + } + + /** + * Returns the base url (protocol://domain{:port}) for a given url + * + * @param url a non-null URL + * @return the base URL + */ + public static String toBaseUrl(final URL url) { + Objects.requireNonNull(url, "url cannot be null"); + + final String protocol = url.getProtocol(); + final String host = url.getHost(); + final int port = url.getPort(); + + return port == -1 ? String.format("%s://%s", protocol, host) + : String.format("%s://%s:%d", protocol, host, port); + } + + /** + * Creates a 'sony' URI out of a base URI and a service name + * + * @param baseUri a non-null base URI + * @param serviceName a non-null, non-empty service name + * @return a string representing the 'sony' URI for the service + */ + public static String getSonyUri(final URI baseUri, final String serviceName) { + Objects.requireNonNull(baseUri, "baseUri cannot be null"); + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + + final String protocol = baseUri.getScheme(); + final String host = baseUri.getHost(); + + return String.format("%s://%s/sony/%s", protocol, host, serviceName); + } + + /** + * Creates a 'sony' URL out of a base URL and a service name + * + * @param baseUrl a non-null base URL + * @param serviceName a non-null, non-empty service name + * @return a string representing the 'sony' URL for the service + */ + public static String getSonyUrl(final URL baseUrl, final String serviceName) { + Objects.requireNonNull(baseUrl, "baseUrl cannot be null"); + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + + // Note: we repeat the getSonyUri logic here - we don't use toUri() + // because it will introduce a stupid exception we need to catch + final String protocol = baseUrl.getProtocol(); + final String host = baseUrl.getHost(); + + return String.format("%s://%s/sony/%s", protocol, host, serviceName); + } + + /** + * Send a wake on lan (WOL) packet to the specified ipAddress and macAddress + * + * @param ipAddress the non-null, non-empty ip address + * @param macAddress the non-null, non-empty mac address + * @throws IOException if an IO exception occurs sending the WOL packet + */ + public static void sendWol(final String ipAddress, final String macAddress) throws IOException { + SonyUtil.validateNotEmpty(ipAddress, "ipAddress cannot be empty"); + SonyUtil.validateNotEmpty(macAddress, "macAddress cannot be empty"); + + final byte[] macBytes = new byte[6]; + final String[] hex = macAddress.split("(\\:|\\-)"); + if (hex.length != 6) { + throw new IllegalArgumentException("Invalid MAC address."); + } + try { + for (int i = 0; i < 6; i++) { + macBytes[i] = (byte) Integer.parseInt(hex[i], 16); + } + } catch (final NumberFormatException e) { + throw new IllegalArgumentException("Invalid hex digit in MAC address."); + } + + final byte[] bytes = new byte[6 + 16 * macBytes.length]; + for (int i = 0; i < 6; i++) { + bytes[i] = (byte) 0xff; + } + for (int i = 6; i < bytes.length; i += macBytes.length) { + System.arraycopy(macBytes, 0, bytes, i, macBytes.length); + } + + // logger.debug("Sending WOL to " + ipAddress + " (" + macAddress + ")"); + + // Resolve the ipaddress (in case it's a name) + final InetAddress address = InetAddress.getByName(ipAddress); + + final byte[] addrBytes = address.getAddress(); + addrBytes[addrBytes.length - 1] = (byte) 0xff; + final InetAddress broadcast = InetAddress.getByAddress(addrBytes); + + final DatagramPacket packet = new DatagramPacket(bytes, bytes.length, broadcast, 9); + final DatagramSocket socket = new DatagramSocket(); + socket.send(packet); + socket.close(); + } + + /** + * Determines if the specified address is potentially formatted as a mac address or not + * + * @param potentialMacAddress a possibly null, possibly empty mac address + * @return true if formatted like a mac address, false otherwise + */ + public static boolean isMacAddress(final @Nullable String potentialMacAddress) { + if (potentialMacAddress == null || potentialMacAddress.length() != 17) { + return false; + } + for (int i = 5; i >= 1; i--) { + final char c = potentialMacAddress.charAt(i * 3 - 1); + if (c != ':' && c != '-') { + return false; + } + } + return true; + } + + /** + * Returns the mac address represented by the byte array or null if not a WOL representation + * + * @param wakeOnLanBytes the possibly null wake on lan bytes + * @return the mac address or null if not a mac address + */ + public @Nullable static String getMacAddress(final byte @Nullable [] wakeOnLanBytes) { + if (wakeOnLanBytes != null && wakeOnLanBytes.length >= 12) { + final StringBuffer macAddress = new StringBuffer(16); + for (int i = 6; i < 12; i++) { + // left padding with "0" to size 2 + macAddress.append(SonyUtil.leftPad(Integer.toHexString(wakeOnLanBytes[i]), 2, '0')); + macAddress.append(Integer.toHexString(wakeOnLanBytes[i])); + macAddress.append(":"); + } + macAddress.deleteCharAt(macAddress.length() - 1); + return macAddress.toString(); + } + return null; + } + + /** + * Get's the URL that is relative to another URL + * + * @param baseUrl the non-null base url + * @param otherUrl the non-null, non-empty other url + * @return the combined URL or null if a malformed + */ + public static @Nullable URL getUrl(final URL baseUrl, final String otherUrl) { + Objects.requireNonNull(baseUrl, "baseUrl cannot be null"); + SonyUtil.validateNotEmpty(otherUrl, "otherUrl cannot be empty"); + + try { + return new URL(baseUrl, otherUrl); + } catch (final MalformedURLException e) { + return null; + } + } + + /** + * Gets an raw type from the given transport and url + * + * @param transport a non-null http transport to use + * @param url a possibly null, possibly empty URL to use + * @return a rawtype (with correct mime) or null if not found (or URL was null/empty) + */ + public static @Nullable RawType getRawType(final SonyHttpTransport transport, final @Nullable String url) { + Objects.requireNonNull(transport, "transport is not null"); + + byte[] iconData = null; + String mimeType = RawType.DEFAULT_MIME_TYPE; + + if (url != null && !url.isEmpty()) { + final HttpResponse resp = transport.executeGet(url); + if (resp.getHttpCode() == HttpStatus.OK_200) { + iconData = resp.getContentAsBytes(); + mimeType = resp.getResponseHeader(HttpHeader.CONTENT_TYPE.asString()); + if (SonyUtil.isEmpty(mimeType)) { + // probably a 'content' header of value 'Content-Type: image/png' instead + mimeType = resp.getResponseHeader("content"); + if (mimeType != null) { + final int idx = mimeType.indexOf(":"); + if (idx >= 0) { + mimeType = mimeType.substring(idx + 1).trim(); + } + } + } + } + } + return iconData == null ? null : new RawType(iconData, mimeType == null ? RawType.DEFAULT_MIME_TYPE : mimeType); + } + + /** + * Send a request to the specified ipaddress/port using a socket connection. Any results will be sent back via the + * callback. + * + * @param ipAddress the non-null, non-empty ip address + * @param port the port + * @param request the non-null, non-empty request + * @param callback the non-null callback + * @throws IOException if an IO exception occurs sending the request + */ + public static void sendSocketRequest(final String ipAddress, final int port, final String request, + final SocketSessionListener callback) throws IOException { + SonyUtil.validateNotEmpty(ipAddress, "ipAddress cannot be empty"); + SonyUtil.validateNotEmpty(request, "request cannot be empty"); + Objects.requireNonNull(callback, "callback cannot be null"); + + final Socket socket = new Socket(); + try { + socket.setSoTimeout(10000); + socket.connect(new InetSocketAddress(ipAddress, port)); + + final PrintStream ps = new PrintStream(socket.getOutputStream()); + final BufferedReader bf = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + ps.print(request + "\n"); + ps.flush(); + + int c; + final StringBuilder sb = new StringBuilder(100); + while (true) { + try { + c = bf.read(); + if (c == -1) { + final String str = sb.toString(); + callback.responseReceived(str); + break; + } + final char ch = (char) c; + if (ch == '\n') { + final String str = sb.toString(); + sb.setLength(0); + if (callback.responseReceived(str)) { + break; + } + } + sb.append(ch); + } catch (final SocketTimeoutException e) { + final String str = sb.toString(); + callback.responseReceived(str); + break; + + } catch (final IOException e) { + callback.responseException(e); + break; + } + } + } finally { + socket.close(); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/Slf4LoggingAdapter.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/Slf4LoggingAdapter.java new file mode 100644 index 0000000000000..9c4b5ceeba90b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/Slf4LoggingAdapter.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.net; + +import java.util.Objects; +import java.util.logging.LogRecord; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; + +/** + * Logging adapter to use for Slf4j + * + * @author Tim Roberts - Initial Contribution + */ +@NonNullByDefault +class Slf4LoggingAdapter extends java.util.logging.Logger { + /** The logger */ + private final Logger logger; + + /** + * Creates the logging adapter from the given logger + * + * @param logger a non-null logger to use + */ + protected Slf4LoggingAdapter(final Logger logger) { + super("jersey", null); + Objects.requireNonNull(logger, "logger cannot be null"); + this.logger = logger; + } + + @Override + public void log(final @Nullable LogRecord record) { + if (record != null) { + logger.debug("{}", record.getMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/SocketChannelSession.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/SocketChannelSession.java new file mode 100644 index 0000000000000..f7df7e5cbc189 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/SocketChannelSession.java @@ -0,0 +1,328 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.net; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousCloseException; +import java.nio.channels.SocketChannel; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents a restartable socket connection to the underlying telnet session. Commands can be sent via + * {@link #sendCommand(String)} and responses will be received on any {@link SocketSessionListener}. This implementation + * of {@link SocketSession} communicates using a {@link SocketChannel} connection. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SocketChannelSession implements SocketSession { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(SocketChannelSession.class); + + /** The host/ip address to connect to */ + private final String host; + + /** The port to connect to */ + private final int port; + + /** + * The actual socket being used. Will be null if not connected + */ + private final AtomicReference<@Nullable SocketChannel> socketChannel = new AtomicReference<>(); + + /** The responses read from the {@link #responseThread}. */ + private final BlockingQueue responses = new ArrayBlockingQueue(50); + + /** The {@link SocketSessionListener} that the {@link #dispatchingThread} will call. */ + private final List sessionListeners = new CopyOnWriteArrayList<>(); + + /** Lock controlling access to dispatching/response threads */ + private final Lock threadLock = new ReentrantLock(); + + /** The thread dispatching responses - will be null if not connected. */ + private @Nullable Thread dispatchingThread = null; + + /** The thread processing responses - will be null if not connected. */ + private @Nullable Thread responseThread = null; + + /** + * Creates the socket session from the given host and port. + * + * @param host a non-null, non-empty host/ip address + * @param port the port number between 1 and 65535 + */ + public SocketChannelSession(final String host, final int port) { + SonyUtil.validateNotEmpty(host, "host cannot be null"); + + if (port < 1 || port > 65535) { + throw new IllegalArgumentException("Port must be between 1 and 65535"); + } + this.host = host; + this.port = port; + } + + @Override + public void addListener(final SocketSessionListener listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + sessionListeners.add(listener); + } + + @Override + public void clearListeners() { + sessionListeners.clear(); + } + + @Override + public boolean removeListener(final SocketSessionListener listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + return sessionListeners.remove(listener); + } + + @Override + public void connect() throws IOException { + connect(2000); + } + + @Override + public void connect(final int timeout) throws IOException { + threadLock.lock(); + try { + disconnect(); + + final SocketChannel channel = SocketChannel.open(); + channel.configureBlocking(true); + + logger.debug("Connecting to {}:{}", host, port); + channel.socket().connect(new InetSocketAddress(host, port), timeout); + + socketChannel.set(channel); + + responses.clear(); + + dispatchingThread = new Thread(new Dispatcher()); + responseThread = new Thread(new ResponseReader()); + + dispatchingThread.setDaemon(true); + responseThread.setDaemon(true); + + dispatchingThread.start(); + responseThread.start(); + } finally { + threadLock.unlock(); + } + } + + @Override + public void disconnect() throws IOException { + if (isConnected()) { + logger.debug("Disconnecting from {}:{}", host, port); + + final SocketChannel channel = socketChannel.getAndSet(null); + if (channel != null) { + channel.close(); + } + + threadLock.lock(); + try { + if (dispatchingThread != null) { + dispatchingThread.interrupt(); + dispatchingThread = null; + } + + if (responseThread != null) { + responseThread.interrupt(); + responseThread = null; + } + } finally { + threadLock.unlock(); + } + + responses.clear(); + } + } + + @Override + public boolean isConnected() { + final SocketChannel channel = socketChannel.get(); + return channel != null && channel.isConnected(); + } + + @Override + public synchronized void sendCommand(final String command) throws IOException { + Objects.requireNonNull(command, "command cannot be empty"); + + if (!isConnected()) { + throw new IOException("Cannot send message - disconnected"); + } + + final ByteBuffer toSend = ByteBuffer.wrap((command + "\r\n").getBytes()); + + final SocketChannel channel = socketChannel.get(); + if (channel == null) { + logger.debug("Cannot send command '{}' - socket channel was closed", command); + } else { + logger.debug("Sending Command: '{}'", command); + channel.write(toSend); + } + } + + /** + * This is the runnable that will read from the socket and add messages to the responses queue (to be processed by + * the dispatcher). + * + * @author Tim Roberts + */ + private class ResponseReader implements Runnable { + + /** + * Runs the logic to read from the socket until interrupted. A 'response' is anything that ends + * with a carriage-return/newline combo. Additionally, the special "Login: " and "Password: " prompts are + * treated as responses for purposes of logging in. + */ + @Override + public void run() { + final StringBuilder sb = new StringBuilder(100); + final ByteBuffer readBuffer = ByteBuffer.allocate(1024); + + responses.clear(); + + while (!Thread.currentThread().isInterrupted()) { + try { + // if reader is null, sleep and try again + if (readBuffer == null) { + Thread.sleep(250); + continue; + } + + final SocketChannel channel = socketChannel.get(); + if (channel == null) { + // socket was closed + Thread.currentThread().interrupt(); + break; + } + + final int bytesRead = channel.read(readBuffer); + if (bytesRead == -1) { + responses.put(new IOException("server closed connection")); + break; + } else if (bytesRead == 0) { + readBuffer.clear(); + continue; + } + + readBuffer.flip(); + while (readBuffer.hasRemaining()) { + final char ch = (char) readBuffer.get(); + if (ch == '\n') { + final String str = sb.toString(); + sb.setLength(0); + responses.put(str.trim()); + } else { + sb.append(ch); + } + } + + readBuffer.flip(); + + } catch (final InterruptedException e) { + // Ending thread execution + Thread.currentThread().interrupt(); // sets isInterrupted field + } catch (final AsynchronousCloseException e) { + // socket was closed by another thread but interrupt our loop anyway + Thread.currentThread().interrupt(); + } catch (final IOException e) { + // set before pushing the response since we'll likely call back our disconnect + Thread.currentThread().interrupt(); + + try { + responses.put(e); + break; + } catch (final InterruptedException e1) { + // Do nothing - probably shutting down + break; + } + } + } + logger.debug("Response thread ending"); + } + } + + /** + * The dispatcher runnable is responsible for reading the response queue and dispatching it to the current callable. + * Since the dispatcher is ONLY started when a callable is set, responses may pile up in the queue and be dispatched + * when a callable is set. Unlike the socket reader, this can be assigned to another thread (no state outside of the + * class). + * + * @author Tim Roberts + */ + private class Dispatcher implements Runnable { + /** + * Runs the logic to dispatch any responses to the current listeners until interrupted + */ + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + final SocketSessionListener[] listeners = sessionListeners.toArray(new SocketSessionListener[0]); + + // if no listeners, we don't want to start dispatching yet. + if (listeners.length == 0) { + Thread.sleep(250); + continue; + } + + final Object response = responses.poll(1, TimeUnit.SECONDS); + + if (response != null) { + if (response instanceof String) { + logger.debug("Dispatching response: {}", response); + for (final SocketSessionListener listener : listeners) { + listener.responseReceived((String) response); + } + } else if (response instanceof IOException) { + logger.debug("Dispatching exception: {}", response); + for (final SocketSessionListener listener : listeners) { + listener.responseException((IOException) response); + } + } else { + logger.debug("Unknown response class: {}", response); + } + } + } catch (final InterruptedException e) { + // Ending thread execution + Thread.currentThread().interrupt(); // sets isInterrupted field + } catch (final Exception e) { + logger.debug("Uncaught exception {}", e.getMessage(), e); + Thread.currentThread().interrupt(); + } + } + logger.debug("Dispatch thread ending"); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/SocketSession.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/SocketSession.java new file mode 100644 index 0000000000000..746f7c7478a76 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/SocketSession.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.net; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This is a socket session interface that defines the contract for a socket session. A socket session will initiate + * communications with the underlying device and provide message back via the {@link SocketSessionListener} + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface SocketSession { + + /** + * Adds a {@link SocketSessionListener} to call when responses/exceptions have been received. + * + * @param listener a non-null {@link SocketSessionListener} to use + */ + void addListener(SocketSessionListener listener); + + /** + * Clears all listeners. + */ + void clearListeners(); + + /** + * Removes a {@link SocketSessionListener} from this session. + * + * @param listener a non-null {@link SocketSessionListener} to remove + * @return true if removed, false otherwise + */ + boolean removeListener(SocketSessionListener listener); + + /** + * Will attempt to connect to the underlying host/port with default timeout + * + * @throws IOException Signals that an I/O exception has occurred. + */ + void connect() throws IOException; + + /** + * Will attempt to connect to the underlying host/port + * + * @param timeout a connection timeout (in milliseconds) + * @throws IOException Signals that an I/O exception has occurred. + */ + void connect(int timeout) throws IOException; + + /** + * Disconnects from the host/port + * + * @throws IOException Signals that an I/O exception has occurred. + */ + void disconnect() throws IOException; + + /** + * Returns true if connected, false otherwise + * + * @return true if connected, false otherwise + */ + boolean isConnected(); + + /** + * Sends the specified command to the underlying socket. + * + * @param command a non-null, possibly empty command + * @throws IOException Signals that an I/O exception has occurred. + */ + void sendCommand(String command) throws IOException; +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/SocketSessionListener.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/SocketSessionListener.java new file mode 100644 index 0000000000000..d7a842f00db84 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/SocketSessionListener.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.net; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Interface defining a listener to a {@link SocketSession} that will receive responses and/or exceptions from the + * socket. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface SocketSessionListener { + /** + * Called when a command has completed with the response for the command. + * + * @param response a non-null, possibly empty response + * @return true if processed, false otherwise + */ + public boolean responseReceived(String response); + + /** + * Called when a command finished with an exception or a general exception occurred while reading. + * + * @param e a non-null exception + */ + public void responseException(IOException e); +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyDefinitionProvider.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyDefinitionProvider.java new file mode 100644 index 0000000000000..2e0f1d71c8b0e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyDefinitionProvider.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers; + +import java.util.function.Predicate; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.providers.models.SonyDeviceCapability; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.ThingTypeProvider; +import org.openhab.core.thing.type.ChannelGroupTypeProvider; + +/** + * Defines the contract for a sony definition provider. A definition provider create thing types, channel group types + * and is used to record device capabilities and things + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface SonyDefinitionProvider extends ThingTypeProvider, ChannelGroupTypeProvider, SonyModelProvider { + /** + * Method to write out a models device capabilities + * + * @param deviceCapability a non-null device capability + */ + void writeDeviceCapabilities(SonyDeviceCapability deviceCapability); + + /** + * Helper method to write a thing/thing type to a the source(s) + * + * @param service a non-null, non-empty service + * @param configUri a non-null, non-empty configUri + * @param modelName a non-null, non-empty model name + * @param thing a non-null thing to use + * @param channelFilter a non-null channel filter to use + */ + void writeThing(String service, String configUri, String modelName, Thing thing, Predicate channelFilter); +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyDefinitionProviderImpl.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyDefinitionProviderImpl.java new file mode 100644 index 0000000000000..7430825d6d138 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyDefinitionProviderImpl.java @@ -0,0 +1,386 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.providers.models.SonyDeviceCapability; +import org.openhab.binding.sony.internal.providers.models.SonyThingChannelDefinition; +import org.openhab.binding.sony.internal.providers.models.SonyThingDefinition; +import org.openhab.binding.sony.internal.providers.models.SonyThingStateDefinition; +import org.openhab.binding.sony.internal.providers.sources.SonyFolderSource; +import org.openhab.binding.sony.internal.providers.sources.SonySource; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingTypeProvider; +import org.openhab.core.thing.type.ChannelGroupType; +import org.openhab.core.thing.type.ChannelGroupTypeProvider; +import org.openhab.core.thing.type.ChannelGroupTypeUID; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeRegistry; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.openhab.core.types.StateOption; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This implementation of {@link SonyDefinitionProvider} will manage the various + * {@link SonySource} and provide data to and from them + * + * @author Tim Roberts - Initial contribution + */ +@Component(service = { DynamicStateDescriptionProvider.class, SonyDynamicStateProvider.class, + SonyDefinitionProvider.class, ThingTypeProvider.class, ChannelGroupTypeProvider.class, + SonyModelProvider.class }, properties = "OSGI-INF/SonyDefinitionProviderImpl.properties", configurationPid = "sony.sources") +@NonNullByDefault +public class SonyDefinitionProviderImpl implements SonyDefinitionProvider, SonyDynamicStateProvider { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(getClass()); + + /** The list of sources (created in activate, cleared in deactivate) */ + private final List sources; + + /** The list of dynamic state overrides by channel uid */ + private final Map stateOverride = new HashMap<>(); + + /** The thing registry used to lookup things */ + private final ThingRegistry thingRegistry; + + /** The thing registry used to lookup things */ + private final ThingTypeRegistry thingTypeRegistry; + + /** Scheduler used to schedule events */ + private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("SonyDefinitionProviderImpl"); + + /** + * Constructs the sony definition provider implmentation + * + * @param thingRegistry a non-null thing registry + * @param thingTypeRegistry a non-null thing type registry + * @param properties the OSGI properties + */ + @Activate + public SonyDefinitionProviderImpl(final @Reference ThingRegistry thingRegistry, + final @Reference ThingTypeRegistry thingTypeRegistry, final Map properties) { + Objects.requireNonNull(thingRegistry, "thingRegistry cannot be null"); + Objects.requireNonNull(thingTypeRegistry, "thingTypeRegistry cannot be null"); + Objects.requireNonNull(properties, "properties cannot be null"); + + this.thingRegistry = thingRegistry; + this.thingTypeRegistry = thingTypeRegistry; + + // local is currently the only implemented provider + final List srcs = new ArrayList<>(); + if (!Boolean.FALSE.equals(SonyUtil.toBooleanObject(properties.get("local")))) { + srcs.add(new SonyFolderSource(scheduler, properties)); + } + this.sources = Collections.unmodifiableList(srcs); + } + + @Override + public @Nullable ChannelGroupType getChannelGroupType(final ChannelGroupTypeUID channelGroupTypeUID, + final @Nullable Locale locale) { + Objects.requireNonNull(channelGroupTypeUID, "thingTypeUID cannot be null"); + if (SonyBindingConstants.BINDING_ID.equalsIgnoreCase(channelGroupTypeUID.getBindingId())) { + for (final SonySource src : sources) { + final ChannelGroupType groupType = src.getChannelGroupType(channelGroupTypeUID); + if (groupType != null) { + return groupType; + } + } + } + return null; + } + + @Override + public Collection getChannelGroupTypes(final @Nullable Locale locale) { + final Map groupTypes = new HashMap<>(); + for (final SonySource src : sources) { + final Collection localGroupTypes = src.getChannelGroupTypes(); + if (localGroupTypes != null) { + for (final ChannelGroupType gt : localGroupTypes) { + if (!groupTypes.containsKey(gt.getUID())) { + groupTypes.put(gt.getUID(), gt); + } + } + } + } + return groupTypes.values(); + } + + @Override + public Collection getThingTypes(final @Nullable Locale locale) { + final Map thingTypes = new HashMap<>(); + for (final SonySource src : sources) { + for (final ThingType tt : src.getThingTypes()) { + if (!thingTypes.containsKey(tt.getUID())) { + thingTypes.put(tt.getUID(), tt); + } + } + } + return thingTypes.values(); + } + + @Override + public @Nullable ThingType getThingType(final ThingTypeUID thingTypeUID, final @Nullable Locale locale) { + Objects.requireNonNull(thingTypeUID, "thingTypeUID cannot be null"); + if (SonyBindingConstants.BINDING_ID.equalsIgnoreCase(thingTypeUID.getBindingId())) { + for (final SonySource src : sources) { + final ThingType thingType = src.getThingType(thingTypeUID); + if (thingType != null) { + return thingType; + } + } + } + return null; + } + + @Override + public void addStateOverride(final ThingUID thingUID, final String channelId, + final StateDescription stateDescription) { + Objects.requireNonNull(thingUID, "thingUID cannot be null"); + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + Objects.requireNonNull(stateDescription, "stateDescription cannot be null"); + + final ChannelUID id = new ChannelUID(thingUID, channelId); + stateOverride.put(id, stateDescription); + } + + @Override + public @Nullable StateDescription getStateDescription(final Channel channel, + final @Nullable StateDescription originalStateDescription, final @Nullable Locale locale) { + Objects.requireNonNull(channel, "channel cannot be null"); + + if (SonyBindingConstants.BINDING_ID.equalsIgnoreCase(channel.getUID().getBindingId())) { + return getStateDescription(channel.getUID().getThingUID(), channel.getUID().getId(), + originalStateDescription); + } + return null; + } + + @Override + public @Nullable StateDescription getStateDescription(final ThingUID thingUID, final String channelId) { + return getStateDescription(thingUID, channelId, null); + } + + /** + * This is a helper method to get a state description for a specific thingUID + * and channel ID. This will intelligenly merge the original state description + * (from a thing definition) with any overrides that have been added + * + * @param thingUID a non-null thing uid + * @param channelId a non-null, non-empty channel id + * @param originalStateDescription a potentially null (if none) original state + * description + * @return the state definition for the thing/channel or the original if none found + */ + private @Nullable StateDescription getStateDescription(final ThingUID thingUID, final String channelId, + final @Nullable StateDescription originalStateDescription) { + Objects.requireNonNull(thingUID, "thingUID cannot be null"); + SonyUtil.validateNotEmpty(channelId, "channelID cannot be empty"); + + final ThingRegistry localThingRegistry = thingRegistry; + if (localThingRegistry != null) { + final Thing thing = localThingRegistry.get(thingUID); + final ChannelUID id = new ChannelUID(thingUID, channelId); + + if (thing != null) { + BigDecimal min = null, max = null, step = null; + String pattern = null; + Boolean readonly = null; + List options = null; + + // First use any specified override (if found) + // Note since compiler thinks overrideDesc cannot be null + // it flags the 'readonly' below as can't be null (which is incorrect) + final StateDescription overrideDesc = stateOverride.get(id); + if (overrideDesc != null) { + min = overrideDesc.getMinimum(); + max = overrideDesc.getMaximum(); + step = overrideDesc.getStep(); + pattern = overrideDesc.getPattern(); + readonly = overrideDesc.isReadOnly(); + options = overrideDesc.getOptions(); + } + + // Finally use the original values + if (originalStateDescription != null) { + if (min == null) { + min = originalStateDescription.getMinimum(); + } + + if (max == null) { + max = originalStateDescription.getMaximum(); + } + + if (step == null) { + step = originalStateDescription.getStep(); + } + + if (pattern == null) { + pattern = originalStateDescription.getPattern(); + } + + if (readonly == null) { + readonly = originalStateDescription.isReadOnly(); + } + + if (options == null) { + options = originalStateDescription.getOptions(); + } + } + + // If anything is specified, create a new state description and go with it + if (min != null || max != null || step != null || pattern != null || readonly != null + || (options != null && !options.isEmpty())) { + StateDescriptionFragmentBuilder bld = StateDescriptionFragmentBuilder.create(); + if (min != null) { + bld = bld.withMinimum(min); + } + if (max != null) { + bld = bld.withMaximum(max); + } + if (step != null) { + bld = bld.withStep(step); + } + if (pattern != null) { + bld = bld.withPattern(pattern); + } + if (readonly != null) { + bld = bld.withReadOnly(readonly); + } + if (!options.isEmpty()) { + bld = bld.withOptions(options); + } + return bld.build().toStateDescription(); + } + } + } + return null; + } + + @Override + public void writeDeviceCapabilities(final SonyDeviceCapability deviceCapability) { + Objects.requireNonNull(deviceCapability, "deviceCapability cannot be null"); + for (final SonySource src : sources) { + src.writeDeviceCapabilities(deviceCapability); + } + } + + @Override + public void writeThing(final String service, final String configUri, final String modelName, final Thing thing, + final Predicate channelFilter) { + SonyUtil.validateNotEmpty(service, "service cannot be empty"); + SonyUtil.validateNotEmpty(configUri, "configUri cannot be empty"); + SonyUtil.validateNotEmpty(modelName, "modelName cannot be empty"); + Objects.requireNonNull(thing, "thing cannot be null"); + Objects.requireNonNull(channelFilter, "channelFilter cannot be null"); + + final ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (!thingTypeUID.getId().equalsIgnoreCase(service)) { + logger.debug("Could not write thing type - already a specific thing type (not generic)"); + return; + } + + final ThingTypeRegistry localThingTypeRegistry = thingTypeRegistry; + if (localThingTypeRegistry == null) { + logger.debug("Could not write thing type - thing type registry was null"); + return; + } + + final ThingType thingType = localThingTypeRegistry.getThingType(thingTypeUID); + if (thingType == null) { + logger.debug("Could not write thing type - thing type was not found in the sony sources"); + return; + } + + // Get the state channel that have a type (with no mapping) + // ignore null warning as the filter makes sure it's not null + @Nullable + final List chls = thing.getChannels().stream().filter(channelFilter).map(chl -> { + final ChannelTypeUID ctuid = chl.getChannelTypeUID(); + // return emtpy SonyThingChannelDefinition with null channelID if ctuid is null + Optional stcd = Optional.empty(); + if (ctuid != null) { + stcd = Optional.of(new SonyThingChannelDefinition(chl.getUID().getId(), ctuid.getId(), + new SonyThingStateDefinition(getStateDescription(chl, null, null)), chl.getProperties())); + } + return stcd; + }).filter(Optional::isPresent).map(Optional::get) + .sorted((f, l) -> f.getChannelId().compareToIgnoreCase(Objects.requireNonNull(l.getChannelId()))) + .collect(Collectors.toList()); + + final String label = SonyUtil.defaultIfEmpty(thing.getLabel(), thingType.getLabel()); + if (label == null || label.isEmpty()) { + logger.debug("Could not write thing type - no label was found"); + return; + } + + final String desc = thingType.getDescription(); + + // hardcoded service groups for now + final SonyThingDefinition ttd = new SonyThingDefinition(service, configUri, modelName, "Sony " + label, + SonyUtil.defaultIfEmpty(desc, label), ScalarWebService.getServiceLabels(), chls); + + for (final SonySource src : sources) { + src.writeThingDefinition(ttd); + } + } + + @Deactivate + public void deactivate() { + for (final SonySource src : sources) { + src.close(); + } + } + + @Override + public void addListener(final String modelName, final ThingTypeUID currentThingTypeUID, + final SonyModelListener listener) { + sources.forEach(s -> s.addListener(modelName, currentThingTypeUID, listener)); + } + + @Override + public boolean removeListener(final SonyModelListener listener) { + return sources.stream().map(s -> s.removeListener(listener)).anyMatch(e -> e); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyDynamicStateProvider.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyDynamicStateProvider.java new file mode 100644 index 0000000000000..400d8312cf4ff --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyDynamicStateProvider.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.openhab.core.types.StateDescription; + +/** + * This interface extends the {@link DynamicStateDescriptionProvider} to allow adding of state overrides and retrieval + * of a state description by thingUID/channelID + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface SonyDynamicStateProvider extends DynamicStateDescriptionProvider { + /** + * Adds a state override for the given thingUID/channelID + * + * @param thingUID a non-null thing uid + * @param channelId a non-null, non-empty channel ID + * @param stateDescription a non-null state description to add + */ + public void addStateOverride(ThingUID thingUID, String channelId, StateDescription stateDescription); + + /** + * Returns a state description for a thing UID/channel ID. Please note this will only return those items that were + * added via {@link #addStateOverride(ThingUID, String, StateDescription)} + * + * @param thingUID a non-null thing UID + * @param channelId a non-null, non-empty channel ID + * @return null if no state description found or non-null if found + */ + public @Nullable StateDescription getStateDescription(ThingUID thingUID, String channelId); +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyModelListener.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyModelListener.java new file mode 100644 index 0000000000000..7915f5366a2bc --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyModelListener.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * This interface should be implemented by any listener of model changes (ie new thing types for a specific model). The + * {@link #thingTypeFound(ThingTypeUID)} will be called back when a new thing type is found for the related model. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface SonyModelListener { + /** + * The call back when a new thing type is found + * + * @param uid a non-null thing type uid + */ + void thingTypeFound(ThingTypeUID uid); +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyModelProvider.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyModelProvider.java new file mode 100644 index 0000000000000..2272802017485 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/SonyModelProvider.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * Defines the contract for any provider that wished to manage model to thing type relations (and provide callback to a + * listener when that relationship changes) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface SonyModelProvider { + /** + * Adds a listener for the given model name (that uses the current thingtypeuid) + * + * @param modelName a non-null, non-empty model name + * @param currentThingTypeUID a non-null current thing type uid + * @param listener a non-null listener + */ + void addListener(String modelName, ThingTypeUID currentThingTypeUID, SonyModelListener listener); + + /** + * Removes a listener from this source + * + * @param listener a non-null listener to remove + * @return true if removed, false otherwise + */ + boolean removeListener(SonyModelListener listener); +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyDeviceCapability.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyDeviceCapability.java new file mode 100644 index 0000000000000..45af755031da5 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyDeviceCapability.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.models; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * The class represents a sony device capability. The capability describes the device and then describes the services + * within the device. The class will only be used to serialize the definition. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyDeviceCapability { + /** The model name of the device */ + private @Nullable String modelName; + + /** The base URL to the device */ + private @Nullable URL baseURL; + + /** A list of service capabilities */ + private @Nullable List<@Nullable SonyServiceCapability> services; + + /** + * Empty constructor for deserialization + */ + public SonyDeviceCapability() { + } + + /** + * Constructs the capability from the parameters + * + * @param modelName a non-null, non-empty model name + * @param baseURL a non-null base url + * @param services a non-null, possibly empty list of services + */ + public SonyDeviceCapability(final String modelName, final URL baseURL, final List services) { + SonyUtil.validateNotEmpty(modelName, "modelName cannot be empty"); + Objects.requireNonNull(baseURL, "baseURL cannot be null"); + Objects.requireNonNull(services, "services cannot be null"); + + this.modelName = modelName; + this.baseURL = baseURL; + this.services = new ArrayList<>(services); + } + + /** + * Returns the model name of the device + * + * @return the possibly null, possibly empty model name + */ + public @Nullable String getModelName() { + return modelName; + } + + /** + * Returns the service capabilities + * + * @return a non-null, possibly empy list of service capabilities + */ + public List getServices() { + return Collections.unmodifiableList(SonyUtil.convertNull(services)); + } + + @Override + public String toString() { + return "SonyDeviceCapability [modelName=" + modelName + ", baseURL=" + baseURL + ", services=" + services + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyServiceCapability.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyServiceCapability.java new file mode 100644 index 0000000000000..e3c69b5481b74 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyServiceCapability.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.models; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; + +/** + * The class represents a sony device service capability. The capability describes the service and the + * methods/notifications for the service. The class will only be used to serialize the definition. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyServiceCapability { + /** The service name */ + private @Nullable String serviceName; + + /** The service version */ + private @Nullable String version; + + /** The transport used for this service */ + private @Nullable String transport; + + /** The methods defined for the service */ + private @Nullable List<@Nullable ScalarWebMethod> methods; + + /** The notifications that can be sent from the service */ + private @Nullable List<@Nullable ScalarWebMethod> notifications; + + /** + * Empty constructor for deserialization + */ + public SonyServiceCapability() { + } + + /** + * Constructs the capability from the parameters + * + * @param serviceName a non-null, non-empty service name + * @param version a non-null, non-empty service version + * @param transport a non-null, non-empty transport + * @param methods a non-null, possibly empty list of methods + * @param notifications a non-null, possibly empty list of notifications + */ + public SonyServiceCapability(final String serviceName, final String version, final String transport, + final List methods, final List notifications) { + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + SonyUtil.validateNotEmpty(version, "version cannot be empty"); + SonyUtil.validateNotEmpty(transport, "transport cannot be empty"); + Objects.requireNonNull(methods, "methods cannot be null"); + Objects.requireNonNull(notifications, "notifications cannot be null"); + + this.serviceName = serviceName; + this.version = version; + this.transport = transport; + this.methods = new ArrayList<>(methods); + this.notifications = new ArrayList<>(notifications); + } + + /** + * Returns the service name for this capability + * + * @return a possibly null, possibly empty service name + */ + public @Nullable String getServiceName() { + return serviceName; + } + + /** + * Returns the service version for this capability + * + * @return a possibly null, possibly empty service version + */ + public @Nullable String getVersion() { + return version; + } + + /** + * Returns the transport to use with this capability + * + * @return a possibly null, possibly empty transport to use + */ + public @Nullable String getTransport() { + return transport; + } + + /** + * Returns the methods (if any) for this service + * + * @return a non-null, but possibly empty list of methods + */ + public List getMethods() { + return Collections.unmodifiableList(SonyUtil.convertNull(methods)); + } + + /** + * Returns the notifications (if any) for this service + * + * @return a non-null, but possibly empty list of notifications + */ + public List getNotifications() { + return Collections.unmodifiableList(SonyUtil.convertNull(notifications)); + } + + @Override + public String toString() { + return "SonyServiceCapability [serviceName=" + serviceName + ", version=" + version + ", transport=" + transport + + ", methods=" + methods + ", notifications=" + notifications + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyThingChannelDefinition.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyThingChannelDefinition.java new file mode 100644 index 0000000000000..3558a0c6f2e03 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyThingChannelDefinition.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.models; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the thing type channel definition that will be used to serialize/deserialize channel + * information. + * + * Note that this can be either created via the constructor (which will enforce null/empty checks) or via + * deserialization (which can provide null/empty variables) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyThingChannelDefinition { + /** The channel identifier */ + private @Nullable String channelId; + + /** The channel type identifier */ + private @Nullable String channelType; + + /** The channel properties */ + private @Nullable Map<@Nullable String, @Nullable String> properties; + + /** The channel options */ + private @Nullable SonyThingStateDefinition state; + + /** + * Empty constructor used for deserialization + */ + public SonyThingChannelDefinition() { + } + + /** + * Constructs the definition from the passed arguments. + * + * @param channelId the non-null, non-empty channel identifier + * @param channelType the non-null, non-empty channel type + * @param state the non-null thing state definition + * @param properties the non-null, possibly empty properties + */ + public SonyThingChannelDefinition(final String channelId, final String channelType, + final SonyThingStateDefinition state, final Map properties) { + SonyUtil.validateNotEmpty(channelId, "channelId must not be empty"); + SonyUtil.validateNotEmpty(channelType, "channelType must not be empty"); + Objects.requireNonNull(state, "state cannot be null"); + Objects.requireNonNull(properties, "properties cannot be null"); + + this.channelId = channelId; + this.channelType = channelType; + this.properties = new HashMap<>(properties); + this.state = state; + } + + /** + * Returns the channel identifier + * + * @return a possibly null, possibly empty channel identifier + */ + public @Nullable String getChannelId() { + return channelId; + } + + /** + * Returns the channel type identifier + * + * @return a possibly null, possibly empty channel type identifier + */ + public @Nullable String getChannelType() { + return channelType; + } + + /** + * Returns a new properties map + * + * @return a non-null, possibly empty map of properties + */ + public Map<@Nullable String, @Nullable String> getProperties() { + final Map<@Nullable String, @Nullable String> localProp = properties; + return Collections.unmodifiableMap(localProp == null ? Collections.emptyMap() : localProp); + } + + /** + * Returns a new thing state definition + * + * @return a possibly null thing state definition + */ + public @Nullable SonyThingStateDefinition getState() { + return state; + } + + @Override + public String toString() { + return "SonyThingChannelDefinition [channelId=" + channelId + ", channelType=" + channelType + ", properties=" + + properties + ", state=" + state + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyThingDefinition.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyThingDefinition.java new file mode 100644 index 0000000000000..e1a3a8e0ba2da --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyThingDefinition.java @@ -0,0 +1,215 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.models; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.google.gson.annotations.Expose; +import com.google.gson.reflect.TypeToken; + +/** + * This class represents the thing definition that will be used to serialize/deserialize. A thing definition is really a + * cross between a thing and a thing type. The thing definition will be used to generate thing types specific to sony + * models and whose channels will use well-known (already defined) channel types. However, it will also be used in the + * thing initialization to customize the channels the model has and the options (via a dynamic state provider) those + * channels will have. + * + * In other words, a thing definition will create a new thing type specific to a model and will dynamically create + * channels for that model and use state options specific to that sony model. Confusing eh? + * + * Note that this can be either created via the constructor (which will enforce null/empty checks) or via + * deserialization (which can provide null/empty variables) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyThingDefinition { + /** List type token for gson deserialization */ + @Expose(serialize = false) + public static final Type LISTTYPETOKEN = new TypeToken>() { + }.getType(); + + /** The associated service (scalar, etc) */ + private @Nullable String service; + + /** The associated configuration uri */ + private @Nullable String configUri; + + /** The associated model name */ + private @Nullable String modelName; + + /** The label for the thing type */ + private @Nullable String label; + + /** The description for the thing type */ + private @Nullable String description; + + /** The channel group id to label for any channel groups */ + private @Nullable Map<@Nullable String, @Nullable String> channelGroups; + + /** The channel definitions for the thing type */ + private @Nullable List<@Nullable SonyThingChannelDefinition> channels; + + /** + * Empty constructor used for deserialization + */ + public SonyThingDefinition() { + } + + /** + * Constructs the definition from the passed arguments + * + * @param service a non-null, non-empty service + * @param configUri a non-null, non-empty config uri + * @param modelName a non-null, non-empty model name + * @param label a non-null, non-empty label + * @param description a non-null, non-empty description + * @param channelGroups a non-null, possibly empty map of channel groups + * @param channels a non-null, possibly empty list of channels + */ + public SonyThingDefinition(final String service, final String configUri, final String modelName, final String label, + final String description, final Map channelGroups, + final List channels) { + SonyUtil.validateNotEmpty(service, "service cannot be empty"); + SonyUtil.validateNotEmpty(configUri, "configUri cannot be empty"); + SonyUtil.validateNotEmpty(modelName, "modelName cannot be empty"); + SonyUtil.validateNotEmpty(label, "label cannot be empty"); + SonyUtil.validateNotEmpty(description, "description cannot be empty"); + Objects.requireNonNull(channelGroups, "channelGroups cannot be null"); + Objects.requireNonNull(channels, "channels cannot be null"); + + this.service = service; + this.configUri = configUri; + this.modelName = modelName; + this.label = label; + this.description = description; + this.channelGroups = Collections.unmodifiableMap(new HashMap<>(channelGroups)); + this.channels = Collections.unmodifiableList(new ArrayList<>(channels)); + } + + /** + * Returns the service associated with this thing type + * + * @return a possibly null, possibly empty service + */ + public @Nullable String getService() { + return service; + } + + /** + * Returns the config URI associated with this thing type + * + * @return a possibly null, possibly empty config URI + */ + public @Nullable String getConfigUri() { + return configUri; + } + + /** + * Returns the model name associated with this type + * + * @return a possibly null, possibly empty model name + */ + public @Nullable String getModelName() { + return modelName; + } + + /** + * Returns a label associated with this type + * + * @return a non-null, non-empty label + */ + public String getLabel() { + return SonyUtil.defaultIfEmpty(label, "Sony " + modelName); + } + + /** + * Returns a description associated with this type + * + * @return a non-null, non-empty description + */ + public String getDescription() { + return SonyUtil.defaultIfEmpty(description, "Sony " + modelName); + } + + /** + * Returns the channel groups for this type + * + * @return a non-null, possibly empty map of channel groups + */ + public Map getChannelGroups() { + final @Nullable Map<@Nullable String, @Nullable String> localChannelGroups = channelGroups; + if (localChannelGroups == null) { + return new HashMap<>(); + } + + // ugly nullable work around + final Map rc = new HashMap<>(); + localChannelGroups.entrySet().stream().forEach(e -> { + final String localKey = e.getKey(); + final String localValue = e.getValue(); + if (localKey != null && !localKey.isEmpty() && localValue != null && !localValue.isEmpty()) { + rc.put(localKey, localValue); + } + }); + + return rc; + } + + /** + * Returns the channel definitions for this type + * + * @return a non-null, possibly empty list of {@link SonyThingChannelDefinition} + */ + public List getChannels() { + final List<@Nullable SonyThingChannelDefinition> localChannels = channels; + return localChannels == null ? new ArrayList<>() + : (List) localChannels.stream().filter(chl -> chl != null) + .collect(Collectors.toList()); + } + + /** + * Returns the channel definition for a given channel id + * + * @param channelId a non-null, non-empty channel id + * @return a channel definition or null if not found + */ + public @Nullable SonyThingChannelDefinition getChannel(final String channelId) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + final List<@Nullable SonyThingChannelDefinition> localChannels = channels; + if (localChannels != null) { + return localChannels.stream().filter( + chl -> chl != null && chl.getChannelId() != null && chl.getChannelId().equalsIgnoreCase(channelId)) + .findFirst().orElse(null); + } + return null; + } + + @Override + public String toString() { + return "SonyThingDefinition [service=" + service + ", configUri=" + configUri + ", modelName=" + modelName + + ", label=" + label + ", description=" + description + ", channelGroups=" + channelGroups + + ", channels=" + channels + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyThingStateDefinition.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyThingStateDefinition.java new file mode 100644 index 0000000000000..6ba8cc662678d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/models/SonyThingStateDefinition.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.models; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.types.StateDescription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.annotations.Expose; + +/** + * Defines a thing state definition. This class will be used to serialize any state description from the underlying + * thing + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyThingStateDefinition { + /** The logger */ + @Expose(serialize = false, deserialize = false) + protected Logger logger = LoggerFactory.getLogger(getClass()); + + /** The stepping */ + private @Nullable BigDecimal step; + + /** The minimum */ + private @Nullable BigDecimal minimum; + + /** The maximum */ + private @Nullable BigDecimal maximum; + + /** Any pattern to apply */ + private @Nullable String pattern; + + /** Whether it is readonly or not */ + private @Nullable Boolean readonly; + + /** The channel options */ + private @Nullable Map<@Nullable String, @Nullable String> options; + + /** + * Empty constructor used for deserialization + */ + public SonyThingStateDefinition() { + } + + /** + * Constructs the thing state definition from the state description + * + * @param desc a possibly null state description + */ + public SonyThingStateDefinition(final @Nullable StateDescription desc) { + this.maximum = desc == null ? null : desc.getMaximum(); + this.minimum = desc == null ? null : desc.getMinimum(); + this.step = desc == null ? null : desc.getStep(); + this.pattern = desc == null ? null : desc.getPattern(); + this.readonly = desc == null ? null : desc.isReadOnly(); + + this.options = new HashMap<>(); + if (desc != null) { + desc.getOptions().stream().forEach(so -> { + final String key = so.getValue(); + final String val = so.getLabel(); + if (this.options.containsKey(key)) { + // argh - stupid device has multiple values for the same key (bug on device) + logger.trace("Multiple values for key: {} - ignoring {}", key, val); + } else { + this.options.put(key, val); + } + }); + } + } + + @Override + public String toString() { + return "SonyThingStateDefinition [maximum=" + maximum + ", minimum=" + minimum + ", step=" + step + ", pattern=" + + pattern + ", readonly=" + readonly + ", options=" + options + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/AbstractSonySource.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/AbstractSonySource.java new file mode 100644 index 0000000000000..78eb54418bb0b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/AbstractSonySource.java @@ -0,0 +1,681 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.sources; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.providers.SonyModelListener; +import org.openhab.binding.sony.internal.providers.models.SonyThingChannelDefinition; +import org.openhab.binding.sony.internal.providers.models.SonyThingDefinition; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.gson.GsonUtilities; +import org.openhab.core.common.AbstractUID; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.type.ChannelDefinition; +import org.openhab.core.thing.type.ChannelDefinitionBuilder; +import org.openhab.core.thing.type.ChannelGroupDefinition; +import org.openhab.core.thing.type.ChannelGroupType; +import org.openhab.core.thing.type.ChannelGroupTypeBuilder; +import org.openhab.core.thing.type.ChannelGroupTypeUID; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonSyntaxException; + +/** + * An implementation of a {@link SonySource} that will source thing types from + * json files within the user data folder + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractSonySource implements SonySource { + /** The logger */ + protected Logger logger = LoggerFactory.getLogger(getClass()); + + /** The json file extension we are looking for */ + protected static final String JSONEXT = "json"; + + /** The GSON that will be used for deserialization */ + protected final Gson gson = GsonUtilities.getDefaultGson(); + + /** THe lock protecting the state (thingTypeDefinitions, thingTypes and groupTypes - not listeners) */ + private final ReentrantReadWriteLock stateLock = new ReentrantReadWriteLock(); + + /** Our reference of thing type uids to thing type definitions */ + private final Map thingTypeDefinitions = new HashMap<>(); + + /** Our reference of thing type uids to thing types */ + private final Map thingTypes = new HashMap<>(); + + /** Our reference of thing type uids to thing types */ + private final Map groupTypes = new HashMap<>(); + + /** The lock used to manage listeners */ + private final ReadWriteLock listenerLock = new ReentrantReadWriteLock(); + + /** The list of listeners */ + private final Map> listeners = new HashMap<>(); + + @Override + public Collection getThingTypes() { + final Lock readLock = stateLock.readLock(); + readLock.lock(); + try { + return thingTypes.values(); + } finally { + readLock.unlock(); + } + } + + @Override + public @Nullable ThingType getThingType(final ThingTypeUID thingTypeUID) { + Objects.requireNonNull(thingTypeUID, "thingTypeUID cannot be null"); + + final Lock readLock = stateLock.readLock(); + readLock.lock(); + try { + return thingTypes.get(thingTypeUID); + } finally { + readLock.unlock(); + } + } + + @Override + public @Nullable ChannelGroupType getChannelGroupType(final ChannelGroupTypeUID channelGroupTypeUID) { + Objects.requireNonNull(channelGroupTypeUID, "channelGroupTypeUID cannot be null"); + final Lock readLock = stateLock.readLock(); + readLock.lock(); + try { + return groupTypes.get(channelGroupTypeUID); + } finally { + readLock.unlock(); + } + } + + @Override + public @Nullable Collection getChannelGroupTypes() { + final Lock readLock = stateLock.readLock(); + readLock.lock(); + try { + return groupTypes.values(); + } finally { + readLock.unlock(); + } + } + + @Override + public @Nullable SonyThingDefinition getSonyThingTypeDefinition(final ThingTypeUID thingTypeUID) { + Objects.requireNonNull(thingTypeUID, "thingTypeUID cannot be null"); + final Lock readLock = stateLock.readLock(); + readLock.lock(); + try { + return thingTypeDefinitions.get(thingTypeUID); + } finally { + readLock.unlock(); + } + } + + /** + * Will read all files in the specified folder and store the related thing types + * + * @param folder a non-null, non-empty folder (within userdata) + * @throws IOException if an IO exception occurs reading the files + * @throws JsonSyntaxException if a json syntax error occurs + */ + protected void readFiles(final String folder) throws IOException, JsonSyntaxException { + SonyUtil.validateNotEmpty(folder, "folder cannot be empty"); + + logger.debug("Reading all files in {}", folder); + + final Lock writeLock = stateLock.writeLock(); + writeLock.lock(); + try { + // clear out prior entries + groupTypes.clear(); + thingTypes.clear(); + thingTypeDefinitions.clear(); + + for (final File file : new File(folder).listFiles()) { + if (file.isFile()) { + readFile(file.getAbsolutePath()); + } + } + } finally { + writeLock.unlock(); + } + } + + /** + * Reads the specified file path, validates the syntax and stores the new thing + * type + * + * @param filePath a possibly null, possibly empty file path to read + * @return a non-null, potentially empty list of thing definitions to their thing type + * @throws IOException if an IO Exception occurs reading the file + * @throws JsonSyntaxException if a json syntax error occurs + */ + protected List> readFile(final @Nullable String filePath) + throws IOException, JsonSyntaxException { + final List ttds = readThingDefinitions(filePath); + if (ttds.isEmpty()) { + return Collections.emptyList(); + } + + final String fileName = Path.of(filePath).getFileName().toString(); + return addThingDefinitions(fileName, ttds); + } + + /** + * Reads the specified file path and returns the thing definitions within it + * + * @param filePath a possibly null, possibly empty file path to read + * @return a non-null, possibly empty list of sony thing defintions found in the file + * @throws IOException if an IO Exception occurs reading the file + * @throws JsonSyntaxException if a json syntax error occurs + */ + protected List readThingDefinitions(final @Nullable String filePath) + throws IOException, JsonSyntaxException { + if (filePath != null && filePath.isEmpty()) { + logger.debug("Unknown file: {}", filePath); + return Collections.emptyList(); + } + + final Path path = Path.of(filePath); + final String fileName = path.getFileName().toString(); + if (!fileName.toLowerCase().endsWith("." + JSONEXT)) { + logger.debug("Ignoring {} since it's not a .{} file", fileName, JSONEXT); + return Collections.emptyList(); + } + + logger.debug("Reading file {} as a SonyThingDefinition[]", filePath); + final String contents = Files.readString(path); + if (contents == null || contents.isEmpty()) { + logger.debug("Ignoring {} since it was an empty file", JSONEXT); + return Collections.emptyList(); + } + + final JsonElement def = Objects.requireNonNull(gson.fromJson(contents, JsonElement.class)); + if (def.isJsonArray()) { + return Objects.requireNonNull(gson.fromJson(def, SonyThingDefinition.LISTTYPETOKEN)); + } else { + final SonyThingDefinition ttd = Objects.requireNonNull(gson.fromJson(def, SonyThingDefinition.class)); + return Collections.singletonList(ttd); + } + } + + /** + * Adds thing definition(s) for the reference name + * + * @param referenceName a non-null, non-empty reference name + * @param ttds a non-null, possibly empty list of thing definitions + * @return a non-null, possibly empty list of thingtypes to thing definitions + */ + protected List> addThingDefinitions(final String referenceName, + final List ttds) { + SonyUtil.validateNotEmpty(referenceName, "referenceName cannot be empty"); + Objects.requireNonNull(ttds, "ttds cannot be null"); + + logger.debug("Processing {}", referenceName); + int idx = 0; + + final List> results = new ArrayList<>(); + + final Lock writeLock = stateLock.writeLock(); + writeLock.lock(); + try { + for (final SonyThingDefinition ttd : ttds) { + idx++; + + final List validationMessage = new ArrayList<>(); + final Map channelGroups = ttd.getChannelGroups(); + + final String service = ttd.getService(); + if (service == null || service.isEmpty()) { + validationMessage.add("Invalid/missing service element"); + } else if (!AbstractUID.isValid(service)) { + validationMessage.add("Invalid service element (must be a valid UID): " + service); + } + + final String modelName = ttd.getModelName(); + if (modelName == null || modelName.isEmpty()) { + validationMessage.add("Invalid/missing modelName element"); + } else if (!AbstractUID.isValid(modelName)) { + validationMessage.add("Invalid modelName element (must be a valid UID): " + modelName); + } + + final String label = ttd.getLabel(); + final String desc = ttd.getDescription(); + + final List chls = ttd.getChannels(); + + final Map> cds = new HashMap<>(); + for (final SonyThingChannelDefinition chl : chls) { + + final List channelValidationMessage = new ArrayList<>(); + + final String channelId = chl.getChannelId(); + if (channelId == null || channelId.isEmpty()) { + channelValidationMessage.add("Missing channelID element"); + continue; + } + + final String groupId = SonyUtil.substringBefore(channelId, "#"); + if (groupId == null || groupId.isEmpty()) { + channelValidationMessage.add("Missing groupID from channelId: " + channelId); + continue; + } + + final String idWithoutGroup = SonyUtil.substringAfter(channelId, "#"); + + final String channelType = chl.getChannelType(); + if (channelType == null || channelType.isEmpty()) { + channelValidationMessage.add("Missing channelType element"); + continue; + } else if (!AbstractUID.isValid(channelType)) { + channelValidationMessage + .add("Invalid channelType element (must be a valid UID): " + channelType); + continue; + } + + final Map props = new HashMap<>(); + for (final Entry<@Nullable String, @Nullable String> entry : chl.getProperties().entrySet()) { + final @Nullable String propKey = entry.getKey(); + final @Nullable String propValue = entry.getValue(); + if (propKey == null || propKey.isEmpty()) { + channelValidationMessage.add("Missing property key value"); + } else { + props.put(propKey, propValue == null ? "" : propValue); + } + } + + props.put(ScalarWebChannel.CNL_BASECHANNELID, channelId); + + if (channelValidationMessage.isEmpty()) { + List chlDefs = cds.get(groupId); + if (chlDefs == null) { + chlDefs = new ArrayList<>(); + cds.put(groupId, chlDefs); + } + + chlDefs.add(new ChannelDefinitionBuilder(idWithoutGroup, + new ChannelTypeUID(SonyBindingConstants.BINDING_ID, channelType)).withProperties(props) + .build()); + } else { + validationMessage.addAll(channelValidationMessage); + } + } + + if (chls.isEmpty()) { + validationMessage.add("Has no valid channels"); + continue; + } + + final String configUriStr = ttd.getConfigUri(); + if (configUriStr == null || configUriStr.isEmpty()) { + validationMessage.add("Invalid thing definition - missing configUri string"); + } + + final String thingTypeId = service + "-" + modelName; + if (validationMessage.isEmpty()) { + try { + final Map cgd = cds.entrySet().stream() + .collect(Collectors.toMap(k -> k.getKey(), e -> { + final String groupId = e.getKey(); + final List channels = e.getValue(); + final String groupLabel = channelGroups.getOrDefault(groupId, groupId); + final String groupTypeId = thingTypeId + "-" + groupId; + + return ChannelGroupTypeBuilder.instance( + new ChannelGroupTypeUID(SonyBindingConstants.BINDING_ID, groupTypeId), + groupLabel).withChannelDefinitions(channels).build(); + })); + + final List gDefs = cgd.entrySet().stream() + .map(gt -> new ChannelGroupDefinition(gt.getKey(), gt.getValue().getUID())) + .collect(Collectors.toList()); + + final URI configUri = new URI(configUriStr); + final ThingType thingType = ThingTypeBuilder + .instance(SonyBindingConstants.BINDING_ID, thingTypeId, label) + .withConfigDescriptionURI(configUri).withDescription(desc) + .withChannelGroupDefinitions(gDefs).build(); + + final ThingTypeUID uid = thingType.getUID(); + + groupTypes.putAll(cgd.values().stream().collect(Collectors.toMap(k -> k.getUID(), v -> v))); + thingTypes.put(uid, thingType); + thingTypeDefinitions.put(uid, ttd); + + results.add(new AbstractMap.SimpleEntry<>(thingType, ttd)); + + fireThingTypeFound(uid); + + logger.debug("Successfully created a thing type {} from {}", thingType.getUID(), referenceName); + + } catch (final URISyntaxException e) { + validationMessage.add("Configuration URI (" + configUriStr + ") was not a valid URI"); + } + } + + if (!validationMessage.isEmpty()) { + logger.debug("Error creating a thing type from element #{} ({}) in {}:", idx, modelName, + referenceName); + for (final String msg : validationMessage) { + logger.debug(" {}", msg); + } + } + } + return results; + } finally { + writeLock.unlock(); + } + } + + @Override + public void addListener(final String modelName, final ThingTypeUID currentThingTypeUID, + final SonyModelListener listener) { + SonyUtil.validateNotEmpty(modelName, "modelName cannot be empty"); + Objects.requireNonNull(currentThingTypeUID, "currentThingTypeUID cannot be null"); + Objects.requireNonNull(listener, "listener cannot be null"); + + final String serviceName = SonyUtil.getServiceName(currentThingTypeUID); + final ServiceModelName srvModelName = new ServiceModelName(serviceName, modelName); + + final Lock writeLock = listenerLock.writeLock(); + try { + writeLock.lock(); + + List list = listeners.get(srvModelName); + if (list == null) { + list = new ArrayList<>(); + listeners.put(srvModelName, list); + } + if (!list.contains(listener)) { + list.add(listener); + } + } finally { + writeLock.unlock(); + } + + final ThingTypeUID uidForModel = findLatestThingTypeUID(srvModelName); + if (uidForModel != null && !Objects.equals(uidForModel, currentThingTypeUID)) { + listener.thingTypeFound(uidForModel); + } + } + + @Override + public boolean removeListener(final SonyModelListener listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + final Lock writeLock = listenerLock.writeLock(); + try { + writeLock.lock(); + return this.listeners.values().stream().map(e -> { + return e.remove(listener); + }).anyMatch(e -> e); + } finally { + writeLock.unlock(); + } + } + + /** + * Helper method to get all {@link ServiceModelName} that are being listened for + * + * @return a non-null, possibly empty set + */ + protected Set getListeningServiceModelNames() { + final Lock readLock = listenerLock.readLock(); + try { + readLock.lock(); + return listeners.keySet(); + } finally { + readLock.unlock(); + } + } + + /** + * Get's all the listeners for a given {@link ServiceModelName} + * + * Developer note: if a subclass overrides this method, you'll need to modify the addListener (which has + * listener.get to call this instead) + * + * @param srvModelname a non-null service model name + * @return a list of listeners or null if no listeners found for service/model name + */ + protected @Nullable List getListeners(final ServiceModelName srvModelname) { + Objects.requireNonNull(srvModelname, "srvModelname cannot be empty"); + + final Lock readLock = listenerLock.readLock(); + try { + readLock.lock(); + return listeners.get(srvModelname); + } finally { + readLock.unlock(); + } + } + + /** + * Finds the latest thing type uid for a given service/model + * + * @param srvModelName a non-null service model name + * @return the latest ThingTypeUID or null if none found + */ + protected @Nullable ThingTypeUID findLatestThingTypeUID(final ServiceModelName srvModelName) { + Objects.requireNonNull(srvModelName, "srvModelName cannot be empty"); + + final Lock readLock = stateLock.readLock(); + try { + readLock.lock(); + + ThingTypeUID max = null; + Integer maxVers = null; + for (final ThingTypeUID uid : thingTypes.keySet()) { + if (SonyUtil.isModelMatch(uid, srvModelName.getServiceName(), srvModelName.getModelName())) { + final Integer vers = SonyUtil.getModelVersion(uid); + if (maxVers == null || vers > maxVers) { + max = uid; + maxVers = vers; + } + } + } + + return max; + } finally { + readLock.unlock(); + } + } + + /** + * Fires a thing type found message to all listeners for that model + * + * @param uid a non-null thing type UID + */ + private void fireThingTypeFound(final ThingTypeUID uid) { + Objects.requireNonNull(uid, "uid cannot be null"); + + final Integer vers = SonyUtil.getModelVersion(uid); + + final Lock readLock = listenerLock.readLock(); + try { + readLock.lock(); + this.listeners.entrySet().forEach(e -> { + final ServiceModelName srvModelName = e.getKey(); + if (SonyUtil.isModelMatch(uid, srvModelName.getServiceName(), srvModelName.getModelName())) { + final ThingTypeUID maxUid = findLatestThingTypeUID(srvModelName); + final Integer maxVers = maxUid == null ? null : SonyUtil.getModelVersion(maxUid); + + if (maxVers == null || vers >= maxVers) { + e.getValue().stream().forEach(f -> f.thingTypeFound(uid)); + } + } + }); + } finally { + readLock.unlock(); + } + } + + /** + * Helper method to simply get a property as an integer from a property map for a given key + * + * @param properties a non-null properties map + * @param key a non-null, non-empty key + * @return the property value as an integer + * @throws IllegalArgumentException if the property wasn't found or cannot be converted to an integer + */ + protected static int getPropertyInt(final Map properties, final String key) { + Objects.requireNonNull(properties, "properties cannot be null"); + SonyUtil.validateNotEmpty(key, "key cannot be empty"); + + final String prop = getProperty(properties, key); + try { + return Integer.parseInt(prop); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException("Property key " + key + " was not a valid number: " + prop); + } + } + + /** + * Helper method to simply get a property from a property map for a given key + * + * @param properties a non-null properties map + * @param key a non-null, non-empty key + * @return the property value + * @throws IllegalArgumentException if the property wasn't found or is empty + */ + protected static String getProperty(final Map properties, final String key) { + Objects.requireNonNull(properties, "properties cannot be null"); + SonyUtil.validateNotEmpty(key, "key cannot be empty"); + + String prop = null; + if (properties.containsKey(key)) { + prop = properties.get(key); + } + + if (prop == null || prop.isEmpty()) { + throw new IllegalArgumentException("Property key " + key + " was not found"); + + } + return prop; + } + + /** + * Creates a folder if it doesn't already exist + * + * @param path a non-null, non-empty path + * @return true if created, false if not (which means it already existed) + */ + protected static boolean createFolder(final String path) { + SonyUtil.validateNotEmpty(path, "path cannot be empty"); + final File filePath = new File(path); + if (!filePath.exists()) { + filePath.mkdirs(); + return true; + } + return false; + } + + /** + * Helper class that represents a service name/model pair and provide equals/hashcode services for them + */ + protected class ServiceModelName { + /** The service name */ + private final String serviceName; + + /** The model name */ + private final String modelName; + + /** + * Constructs the model from the two attributes + * + * @param serviceName a non-null, non-empty service name + * @param modelName a non-null, non-empty model name + */ + public ServiceModelName(final String serviceName, final String modelName) { + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + SonyUtil.validateNotEmpty(modelName, "modelName cannot be empty"); + + this.serviceName = serviceName; + this.modelName = modelName; + } + + /** + * Returns the service name + * + * @return a non-null, non-empty service name + */ + public String getServiceName() { + return serviceName; + } + + /** + * Returns the model name + * + * @return a non-null non-empty model name + */ + public String getModelName() { + return modelName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((modelName == null) ? 0 : modelName.hashCode()); + result = prime * result + ((serviceName == null) ? 0 : serviceName.hashCode()); + return result; + } + + @Override + public boolean equals(final @Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ServiceModelName other = (ServiceModelName) obj; + return modelName.equals(other.modelName) && serviceName.equals(other.serviceName); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaConvert.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaConvert.java new file mode 100644 index 0000000000000..03d3cbf899541 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaConvert.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.sources; + +import java.util.Objects; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * Helper class representing a name conversion and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +class MetaConvert { + /** The old name pattern */ + private final Pattern oldName; + + /** The new name */ + private final String newName; + + /** + * Constructs the conversion from the old name (pattern) to a new name + * + * @param oldName a non-null old name pattern + * @param newName a non-null, non-empty new name to convert to + */ + MetaConvert(final Pattern oldName, final String newName) { + Objects.requireNonNull(oldName, "oldName cannot be null"); + SonyUtil.validateNotEmpty(newName, "newName cannot be empty"); + + this.oldName = oldName; + this.newName = newName; + } + + /** + * The old name pattern + * + * @return a possibly null old name pattern + */ + public Pattern getOldName() { + return oldName; + } + + /** + * The new name + * + * @return a possibly null, possibly empty name + */ + public @Nullable String getNewName() { + return newName; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaConvertDeserializer.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaConvertDeserializer.java new file mode 100644 index 0000000000000..321464087614c --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaConvertDeserializer.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.sources; + +import java.lang.reflect.Type; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * Deserializer used to deserialize {@link MetaConvert} classes + * + * @author Tim Roberts - Initial contribution + */ +public class MetaConvertDeserializer implements JsonDeserializer { + public MetaConvert deserialize(final @Nullable JsonElement je, final @Nullable Type type, + final @Nullable JsonDeserializationContext context) throws JsonParseException { + Objects.requireNonNull(je, "je cannot be null"); + Objects.requireNonNull(type, "type cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + if (je instanceof JsonObject) { + final JsonObject jo = je.getAsJsonObject(); + + final JsonElement oldName = jo.get("oldName"); + if (oldName == null) { + throw new JsonParseException("oldName must be specified"); + } + + final String pattern = oldName.getAsString(); + Pattern oldNamePattern; + try { + oldNamePattern = Pattern.compile(pattern); + } catch (final PatternSyntaxException e) { + throw new JsonParseException("oldName '" + pattern + "' was not a valid pattern", e); + } + + final JsonElement newName = jo.get("newName"); + if (newName == null) { + throw new JsonParseException("newName must be specified"); + } + + final String newNameStr = newName.getAsString(); + if (newNameStr == null || newNameStr.isEmpty()) { + throw new JsonParseException("newName cannot be empty"); + } + + return new MetaConvert(oldNamePattern, newNameStr); + } + throw new JsonParseException("The json element isn't a JsonObject and cannot be deserialized"); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaInfo.java new file mode 100644 index 0000000000000..52d32e1d42053 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaInfo.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.sources; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class holds meta information (such as ignoring certain model names or converting one model name to + * another) and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +class MetaInfo { + /** Whether the information is enabled or not */ + private final boolean enabled; + + /** The list of model names to ignore */ + private final List ignoreModelName; + + /** The list of channel ids to ignore */ + private final List ignoreChannelId; + + /** The list of model names to convert */ + private final List modelNameConvert; + + /** The list of channel ids to convert (in case of a rename) */ + private final List channelIdConvert; + + /** + * Constructs a default metainfo with nothing ignored or converted + */ + MetaInfo() { + this.enabled = true; + this.ignoreModelName = Collections.emptyList(); + this.ignoreChannelId = Collections.emptyList(); + this.modelNameConvert = Collections.emptyList(); + this.channelIdConvert = Collections.emptyList(); + } + + /** + * Constructs the meta info from the lists + * + * @param enabled if the information is enabled or not + * @param ignoreModelName a non-null, possibly empty list of model names to ignore + * @param ignoreChannelId a non-null, possibly empty list of channel names to ignore + * @param modelNameConvert a non-null, possibly empty list of model names to convert + * @param channelIdConvert a non-null, possibly empty list of channel names to convert + */ + MetaInfo(boolean enabled, final List ignoreModelName, final List ignoreChannelId, + final List modelNameConvert, final List channelIdConvert) { + Objects.requireNonNull(ignoreModelName, "ignoreModelName cannot be null"); + Objects.requireNonNull(ignoreChannelId, "ignoreChannelId cannot be null"); + Objects.requireNonNull(modelNameConvert, "modelNameConvert cannot be null"); + Objects.requireNonNull(channelIdConvert, "channelIdConvert cannot be null"); + + this.enabled = enabled; + this.ignoreModelName = Collections.unmodifiableList(ignoreModelName); + this.ignoreChannelId = Collections.unmodifiableList(ignoreChannelId); + this.modelNameConvert = Collections.unmodifiableList(modelNameConvert); + this.channelIdConvert = Collections.unmodifiableList(channelIdConvert); + } + + /** + * Returns whether the information is enabled (true) or not (false) + * + * @return true if enabled, false otherwise + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Returns true if the specified model name should be ignored + * + * @param modelName a non-null, non-empty model name + * @return true if it should be ignored, false otherwise + */ + public boolean isIgnoredModelName(final String modelName) { + SonyUtil.validateNotEmpty(modelName, "modelName cannot be empty"); + return ignoreModelName.stream().anyMatch(s -> s.matcher(modelName).matches()); + } + + /** + * Returns true if the specified channel id should be ignored + * + * @param channelId a non-null, non-empty channel id + * @return true if it should be ignored, false otherwise + */ + public boolean isIgnoredChannelId(final String channelId) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + return ignoreChannelId.stream().anyMatch(s -> s.matcher(channelId).matches()); + } + + /** + * Provides the converted model name (or the original if not converted) + * + * @param modelName a non-null, non-empty model name + * @return a non-null, non-empty converted model name (or the original if it shouldn't be converted) + */ + public String getModelName(final String modelName) { + SonyUtil.validateNotEmpty(modelName, "modelName cannot be empty"); + + return getNewName(modelName, modelNameConvert); + } + + /** + * Provides the converted channel id (or the original if not converted) + * + * @param channelId a non-null, non-empty channel id + * @return a non-null, non-empty converted channel id (or the original if it shouldn't be converted) + */ + public String getChannelId(final String channelId) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + + return getNewName(channelId, channelIdConvert); + } + + /** + * Provides the converted name (or the original if not converted) + * + * @param name a non-null, non-empty name + * @param convert a possibly null list of converts + * @return a non-null, non-empty converted name (or the original if it shouldn't be converted) + */ + public String getNewName(final String name, final List convert) { + SonyUtil.validateNotEmpty(name, "name cannot be empty"); + + for (final MetaConvert mc : convert) { + final String newName = mc.getNewName(); + final Pattern oldName = mc.getOldName(); + if (oldName.matcher(name).matches() && newName != null) { + return newName; + } + } + return name; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaInfoDeserializer.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaInfoDeserializer.java new file mode 100644 index 0000000000000..29f202feae71f --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/MetaInfoDeserializer.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.sources; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * Deserializer used to deserialize {@link MetaInfo} classes + * + * @author Tim Roberts - Initial contribution + */ +public class MetaInfoDeserializer implements JsonDeserializer { + public MetaInfo deserialize(final @Nullable JsonElement je, final @Nullable Type type, + final @Nullable JsonDeserializationContext context) throws JsonParseException { + Objects.requireNonNull(je, "je cannot be null"); + Objects.requireNonNull(type, "type cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + if (je instanceof JsonObject) { + final JsonObject jo = je.getAsJsonObject(); + + boolean enabled = false; + final List ignoreModelName = new ArrayList<>(); + final List ignoreChannelId = new ArrayList<>(); + final List modelNameConvert = new ArrayList<>(); + final List channelIdConvert = new ArrayList<>(); + + final JsonElement enb = jo.get("enabled"); + if (enb != null) { + enabled = Boolean.parseBoolean(enb.getAsString()); + } + + final JsonElement imnList = jo.get("ignoreModelName"); + if (imnList != null) { + if (!imnList.isJsonArray()) { + throw new JsonParseException("ignoreModelName element is not an array"); + } + + for (final JsonElement elm : imnList.getAsJsonArray()) { + final String pattern = elm.getAsString(); + try { + ignoreModelName.add(Pattern.compile(pattern)); + } catch (PatternSyntaxException e) { + throw new JsonParseException("ignoreModelName '" + pattern + "' was not a valid pattern", e); + } + } + } + + final JsonElement iciList = jo.get("ignoreChannelId"); + if (iciList != null) { + if (!iciList.isJsonArray()) { + throw new JsonParseException("ignoreChannelId element is not an array"); + } + + for (final JsonElement elm : iciList.getAsJsonArray()) { + final String pattern = elm.getAsString(); + try { + ignoreChannelId.add(Pattern.compile(pattern)); + } catch (PatternSyntaxException e) { + throw new JsonParseException("ignoreChannelId '" + pattern + "' was not a valid pattern", e); + } + } + } + + final JsonElement mncList = jo.get("modelNameConvert"); + if (mncList != null) { + if (!mncList.isJsonArray()) { + throw new JsonParseException("modelNameConvert element is not an array"); + } + + for (final JsonElement elm : mncList.getAsJsonArray()) { + final MetaConvert conv = context.deserialize(elm, MetaConvert.class); + if (conv != null) { + modelNameConvert.add(conv); + } + } + } + + final JsonElement cicList = jo.get("channelIdConvert"); + if (cicList != null) { + if (!cicList.isJsonArray()) { + throw new JsonParseException("channelIdConvert element is not an array"); + } + + for (final JsonElement elm : cicList.getAsJsonArray()) { + final MetaConvert conv = context.deserialize(elm, MetaConvert.class); + if (conv != null) { + channelIdConvert.add(conv); + } + } + } + + return new MetaInfo(enabled, ignoreModelName, ignoreChannelId, modelNameConvert, channelIdConvert); + } + throw new JsonParseException("The json element isn't a JsonObject and cannot be deserialized"); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/SonyDeviceCapabilitySerializer.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/SonyDeviceCapabilitySerializer.java new file mode 100644 index 0000000000000..773964f1bfebc --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/SonyDeviceCapabilitySerializer.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.sources; + +import java.lang.reflect.Type; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.providers.models.SonyDeviceCapability; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * This class represents the serializer for a SonyDeviceCapability that will remove the baseURL (private information) + * from the serialized bit. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyDeviceCapabilitySerializer implements JsonSerializer { + public JsonElement serialize(SonyDeviceCapability src, @Nullable Type typeOfSrc, + @Nullable JsonSerializationContext context) { + if (context == null) { + return JsonNull.INSTANCE; + } + + final JsonElement je = context.serialize(src, typeOfSrc); + if (je != null && je instanceof JsonObject) { + final JsonObject jo = je.getAsJsonObject(); + jo.remove("baseURL"); + return jo; + } + return JsonNull.INSTANCE; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/SonyFolderSource.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/SonyFolderSource.java new file mode 100644 index 0000000000000..72b846b44f7ae --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/SonyFolderSource.java @@ -0,0 +1,214 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.sources; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.providers.models.SonyDeviceCapability; +import org.openhab.binding.sony.internal.providers.models.SonyThingDefinition; +import org.openhab.core.OpenHAB; + +import com.google.gson.JsonSyntaxException; + +/** + * An implementation of a {@link SonySource} that will source thing types from + * json files within the user data folder + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyFolderSource extends AbstractSonySource { + /** The base folder to use */ + private static final String FOLDERBASE = OpenHAB.getUserDataFolder() + File.separator + "sony"; + + /** Various property keys used by this source */ + private static final String PROP_WATCHDOG_INTERVAL = "SonyFolderSource.WatchDog.Interval"; + private static final String PROP_THINGTYPES = "SonyFolderSource.Folder.ThingTypes"; + private static final String PROP_DEFTYPES = "SonyFolderSource.Folder.DefinitionTypes"; + private static final String PROP_DEFCAPS = "SonyFolderSource.Folder.DefinitionCapabilities"; + + /** Folder paths */ + private final String folderThingTypes; + private final String folderDefThingTypes; + private final String folderDefCapability; + + /** The folder watcher (null if none being watched) */ + private final AtomicReference<@Nullable Future> watcher = new AtomicReference<>(null); + + /** The folder watch dog (null if none being watched) */ + private final AtomicReference<@Nullable Future> watchDog = new AtomicReference<>(null); + + /** + * Constructs the source and starts the various threads + * + * @param scheduler a non-null scheduler to use + * @param properties a non-null, possibly empty map of properties + */ + public SonyFolderSource(final ScheduledExecutorService scheduler, final Map properties) { + Objects.requireNonNull(scheduler, "scheduler cannot be null"); + Objects.requireNonNull(properties, "properties cannot be null"); + + // create the folders we will use + folderThingTypes = FOLDERBASE + getProperty(properties, PROP_THINGTYPES); + folderDefThingTypes = FOLDERBASE + getProperty(properties, PROP_DEFTYPES); + folderDefCapability = FOLDERBASE + getProperty(properties, PROP_DEFCAPS); + + createFolder(folderThingTypes); + createFolder(folderDefThingTypes); + createFolder(folderDefCapability); + + // Setup our watcher + watcher.set(scheduler.submit(new Watcher())); + + // Keeps the watcher alive in case it encounters an error + final int watchDogTime = getPropertyInt(properties, PROP_WATCHDOG_INTERVAL); + watchDog.set(scheduler.schedule(() -> { + final Future fut = watcher.get(); + if (fut == null || fut.isDone()) { + SonyUtil.cancel(watcher.getAndSet(scheduler.submit(new Watcher()))); + } + }, watchDogTime, TimeUnit.SECONDS)); + } + + @Override + public void writeThingDefinition(final SonyThingDefinition thingTypeDefinition) { + Objects.requireNonNull(thingTypeDefinition, "thingTypeDefinition cannot be null"); + + final Path filePath = Path + .of(folderDefThingTypes + File.separator + thingTypeDefinition.getModelName() + "." + JSONEXT); + + if (filePath.toFile().exists()) { + logger.debug("File for thing definition already exists (write ignored): {}", filePath.toFile()); + return; + } + + final String json = gson.toJson(new SonyThingDefinition[] { thingTypeDefinition }); + + try { + Files.writeString(filePath, json); + } catch (final IOException e) { + logger.debug("IOException writing thing defintion to {}: {}", filePath.toFile(), e.getMessage(), e); + } + } + + @Override + public void writeDeviceCapabilities(final SonyDeviceCapability deviceCapability) { + Objects.requireNonNull(deviceCapability, "deviceCapability cannot be null"); + + final String modelName = deviceCapability.getModelName(); + if (modelName == null || modelName.isEmpty()) { + logger.debug("Cannot write device capabilities because it has no model name: {}", deviceCapability); + return; + } + + final Path filePath = Path.of(folderDefCapability + File.separator + modelName + "." + JSONEXT); + if (filePath.toFile().exists()) { + logger.debug("File for thing definition already exists (write ignored): {}", filePath.toFile()); + return; + } + + final String json = gson.toJson(deviceCapability); + + try { + Files.writeString(filePath, json); + } catch (final IOException e) { + logger.debug("IOException writing methods to {}: {}", filePath.toFile(), e.getMessage(), e); + } + } + + @Override + public void close() { + SonyUtil.cancel(watchDog.get()); + SonyUtil.cancel(watcher.get()); + } + + /** + * + * This private helper class will watch the file system for changes and recreate + * thing types if a file is added/changed/deleted + * + * @author Tim Roberts - Initial contribution + */ + private class Watcher implements Runnable { + @Override + public void run() { + try { + if (SonyUtil.isInterrupted()) { + return; + } + + readFiles(folderThingTypes); + + logger.debug("Starting watch for new/modified entries {}", folderThingTypes); + try (WatchService watchService = FileSystems.getDefault().newWatchService()) { + final Path path = Paths.get(folderThingTypes); + + if (SonyUtil.isInterrupted()) { + return; + } + + path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); + + WatchKey key; + while ((key = watchService.take()) != null) { + if (SonyUtil.isInterrupted()) { + return; + } + + for (final WatchEvent event : key.pollEvents()) { + if (SonyUtil.isInterrupted()) { + return; + } + + if (event.kind().equals(StandardWatchEventKinds.ENTRY_DELETE)) { + logger.debug("File deletion occurred, reloading ALL definitions"); + readFiles(folderThingTypes); + } else { + final Path eventPath = (Path) event.context(); + if (eventPath == null) { + logger.debug("Watch notification without an path in the context: {}", event); + } else { + readFile(eventPath.toAbsolutePath().toString()); + } + } + } + key.reset(); + } + } + } catch (JsonSyntaxException | IOException e) { + logger.debug("Watcher encountered an exception: {}", e.getMessage(), e); + } catch (final InterruptedException e) { + logger.debug("Watcher was interrupted"); + } + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/SonySource.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/SonySource.java new file mode 100644 index 0000000000000..9b76bb2c0248b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/providers/sources/SonySource.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.providers.sources; + +import java.util.Collection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.providers.SonyModelProvider; +import org.openhab.binding.sony.internal.providers.models.SonyDeviceCapability; +import org.openhab.binding.sony.internal.providers.models.SonyThingDefinition; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.type.ChannelGroupType; +import org.openhab.core.thing.type.ChannelGroupTypeUID; +import org.openhab.core.thing.type.ThingType; + +/** + * This interface defines the contract for a sony thing type source. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface SonySource extends AutoCloseable, SonyModelProvider { + /** + * Returns a collection of thing types within the source + * + * @return a non-null but possibly empty collection of thing types + */ + Collection getThingTypes(); + + /** + * Returns a thing type for the given thing type UID + * + * @param thingTypeUID a non-null thing type UID + * @return the associated thing type or null if not found in this source + */ + @Nullable + ThingType getThingType(ThingTypeUID thingTypeUID); + + /** + * Returns a thing type definition for the given thing type UID + * + * @param thingTypeUID a non-null thing type UID + * @return the associated thing type definition or null if not found in this source + */ + @Nullable + SonyThingDefinition getSonyThingTypeDefinition(ThingTypeUID thingTypeUID); + + /** + * Returns a channel group type for the given channel group type uid + * + * @param channelGroupTypeUID a non-null channel group type UID + * @return the associated channel group type or null if not found in this source + */ + @Nullable + ChannelGroupType getChannelGroupType(ChannelGroupTypeUID channelGroupTypeUID); + + /** + * Returns all channel group types + * + * @return a non-null, possibly empty collection of channel group types + */ + @Nullable + Collection getChannelGroupTypes(); + + /** + * This method will be called to write the thing definition to the underlying source + * + * @param thingTypeDefinition a non-null thing definition + */ + void writeThingDefinition(SonyThingDefinition thingTypeDefinition); + + /** + * Method to write out a device capability + * + * @param deviceCapability a non-null device capability to write + */ + void writeDeviceCapabilities(SonyDeviceCapability deviceCapability); + + @Override + public void close(); +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannel.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannel.java new file mode 100644 index 0000000000000..be8ec3a76e3e1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannel.java @@ -0,0 +1,355 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; + +/** + * This class represents a channel for the scalar web component. The channel + * will provide management of the various data items required by the scalar web + * service and provide a mechanism to convert those properties to/from a + * channel. + * + * The service, category, id makes the channel unique and will create a channel + * id like: {service}#{category}-{id} or {service}#{id} (if id=category for + * simple services) + * + * Where the service is the group and the id/category will be the channel ID. + * + * The id, category, base channel id and paths are then stored/retrieved in/from + * the channel's properties. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebChannel { + /** The separator for a group */ + public static final char CATEGORYSEPARATOR = '-'; + + /** + * Channel ID identifier of the dummy dynamic channel (MUST MATCH scalar.thing) + */ + private static final String DYNAMIC = "dynamic"; + + /** The service identifier for the channel (group id) */ + private final String service; + + /** The category of the channel */ + private final String category; + + /** The identifier for the path (channel id) */ + private final String id; + + /** Non-null, never empty path to the sony item (channel properties) */ + private final String[] paths; + + /** Hash map of custom properties */ + private final Map properties = new HashMap<>(); + + // Channel property keys + private static final String CNL_PROPPREFIX = "prop-"; + private static final String CNL_PROPLEN = "propLen"; + private static final String CNL_CHANNELID = "channelId"; + private static final String CNL_CHANNELCATEGORY = "channelCategory"; + public static final String CNL_BASECHANNELID = "baseChannelId"; + + /** + * Creates the web channel from the service, channel ID and possibly some + * additional paths + * + * @param service the non-null, non-empty service + * @param category the non-null, non-empty channel category + * @param id the non-null, non-empty channel id + * @param paths the possibly null, possibly empty list of paths + */ + public ScalarWebChannel(final String service, final String category, final String id, + final @Nullable String @Nullable [] paths) { + SonyUtil.validateNotEmpty(service, "service cannot be empty"); + SonyUtil.validateNotEmpty(category, "category cannot be empty"); + SonyUtil.validateNotEmpty(id, "id cannot be empty"); + + this.service = service; + this.category = category; + this.id = id; + this.paths = paths == null ? new String[0] : SonyUtil.convertNull(paths).toArray(new String[0]); + } + + /** + * Creates the web channel from a thing channel + * + * @param channel a non-null thing channel + */ + public ScalarWebChannel(final Channel channel) { + // no (easy) way to prevent an NPE if we get a null channel + this(channel.getUID(), channel); + } + + /** + * Instantiates a new scalar web channel based on the channel. Note that the + * channelUID may disagree with the channel.getUID in the case of mapping + * channels. + * + * @param channelUID the non-null channel UID + * @param channel the non-null channel + */ + public ScalarWebChannel(final ChannelUID channelUID, final Channel channel) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + Objects.requireNonNull(channel, "channel cannot be null"); + + if (DYNAMIC.equals(channelUID.getIdWithoutGroup())) { + category = DYNAMIC; + id = DYNAMIC; + paths = new String[0]; + service = DYNAMIC; + } else { + final String groupId = channelUID.getGroupId(); + if (groupId == null) { + throw new IllegalArgumentException("ChannelUID must have a group: " + channel); + } + service = groupId; + + this.properties.putAll(channel.getProperties()); + + // Remove our internal properties and use them + final String idr = this.properties.remove(ScalarWebChannel.CNL_CHANNELID); + if (idr == null || idr.isEmpty()) { + throw new IllegalArgumentException("Channel must contain a ID: " + channel); + } else { + id = idr; + } + + final String categoryr = this.properties.remove(ScalarWebChannel.CNL_CHANNELCATEGORY); + if (categoryr == null || categoryr.isEmpty()) { + throw new IllegalArgumentException("Channel must contain a category: " + channel); + } else { + category = categoryr; + } + + final String pathLenStr = this.properties.remove(ScalarWebChannel.CNL_PROPLEN); + if (pathLenStr != null) { + try { + final Integer propLen = Integer.parseInt(pathLenStr); + final List tempPath = new ArrayList<>(); + for (int x = 0; x < propLen; x++) { + final String prefixr = this.properties.remove(ScalarWebChannel.CNL_PROPPREFIX + x); + if (prefixr != null) { + tempPath.add(prefixr); + } + } + + paths = tempPath.toArray(new String[tempPath.size()]); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException( + "Channel path length is not numeric: " + pathLenStr + " - " + channel); + } + } else { + throw new IllegalArgumentException("Channel path length is missing (and is required): " + channel); + } + } + } + + /** + * Gets the service identifier + * + * @return the service identifier + */ + public String getService() { + return service; + } + + /** + * Gets the channel category + * + * @return the category + */ + public String getCategory() { + return category; + } + + /** + * Gets the channel id + * + * @return the channel id + */ + public String getId() { + return id; + } + + /** + * Gets the sony path + * + * @return the non-null, non-empty path to the sony item + */ + public String[] getPaths() { + return paths; + } + + /** + * Gets the path part + * + * @return a possibly null, never empty path part + */ + public @Nullable String getPathPart(final int idx) { + return idx >= 0 && idx < paths.length ? paths[idx] : null; + } + + /** + * Adds a property to the channel + * + * @param key a non-null, non-empty key + * @param value a non-null, non-emtpy value + */ + public void addProperty(final String key, final String value) { + SonyUtil.validateNotEmpty(key, "key cannot be empty"); + SonyUtil.validateNotEmpty(value, "value cannot be empty"); + properties.put(key, value); + } + + /** + * Gets a specific property by key + * + * @param key a non-null non-empty key + * @return a possibly null, possibly empty value + */ + public @Nullable String getProperty(final String key) { + SonyUtil.validateNotEmpty(key, "key cannot be empty"); + return properties.get(key); + } + + /** + * Gets a specific property by key returning a default value if not found or + * whose value is null + * + * @param key a non-null, non-empty key + * @param defaultValue a non-null, possibly empty default value + * @return a non-null, possibly empty value + */ + public String getProperty(final String key, final String defaultValue) { + SonyUtil.validateNotEmpty(key, "key cannot be empty"); + Objects.requireNonNull(defaultValue, "defaultValue cannot be null"); + final String propVal = properties.get(key); + return propVal == null ? defaultValue : propVal; + } + + /** + * Determines if a property key has been assigned + * + * @param key a non-null, non-empty key + * @return true if the property has been defined, false otherwise + */ + public boolean hasProperty(final String key) { + SonyUtil.validateNotEmpty(key, "key cannot be empty"); + return properties.containsKey(key); + } + + /** + * Removes a property assignment + * + * @param key a non-null, non-empty key + * @return true if the property was found and removed, false otherwise + */ + public @Nullable String removeProperty(final String key) { + SonyUtil.validateNotEmpty(key, "key cannot be empty"); + return properties.remove(key); + } + + /** + * Returns the properties for this channel based on the paths + * + * @return a non-null, non-empty properties + */ + public Map getProperties() { + final Map props = new HashMap<>(properties); + props.put(ScalarWebChannel.CNL_PROPLEN, String.valueOf(paths.length)); + for (int x = 0; x < paths.length; x++) { + props.put(ScalarWebChannel.CNL_PROPPREFIX + x, paths[x]); + } + props.put(ScalarWebChannel.CNL_CHANNELCATEGORY, category); + props.put(ScalarWebChannel.CNL_CHANNELID, id); + return props; + } + + /** + * Gets the channel id based on group ID and channel id + * + * @return the channel id + */ + public String getChannelId() { + return SonyUtil.createChannelId(service, createChannelId(category, id)); + } + + /** + * Helper method to create a simply channel id from the id (where the category + * is the id) + * + * @param id the non-null, non-empty id + * @return a non-null, non-empty channel id + */ + public static String createChannelId(final String id) { + SonyUtil.validateNotEmpty(id, "id cannot be empty"); + return createChannelId(id, id); + } + + /** + * Helper method to create a channel id from the category/id + * + * @param category the non-null, non-empty category + * @param id the non-null, non-empty id + * @return a non-null, non-empty channel id + */ + public static String createChannelId(final String category, final String id) { + SonyUtil.validateNotEmpty(id, "id cannot be empty"); + return category.equalsIgnoreCase(id) ? id : (category + CATEGORYSEPARATOR + id); + } + + @Override + public String toString() { + return getChannelId() + " (cid=" + id + ", ctgy=" + category + ", path=" + String.join(",", paths) + ")"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + category.hashCode(); + result = prime * result + id.hashCode(); + return result; + } + + @Override + public boolean equals(final @Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + + if (getClass() != obj.getClass()) { + return false; + } + + final ScalarWebChannel other = (ScalarWebChannel) obj; + return category.equals(other.category) && id.equals(other.id); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannelDescriptor.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannelDescriptor.java new file mode 100644 index 0000000000000..15aadc21b9d3b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannelDescriptor.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.type.ChannelTypeUID; + +/** + * This class represents channel descriptor. Channel descriptors will describe the definition of a channel that will be + * created in {@link ScalarWebHandler} + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebChannelDescriptor { + + /** The label for the channel */ + private final @Nullable String label; + + /** The description of the channel */ + private final @Nullable String description; + + /** The accepted item type */ + private final String acceptedItemType; + + /** The channel type */ + private final String channelType; + + /** The underlying channel */ + private final ScalarWebChannel channel; + + /** + * Instantiates a new scalar web channel descriptor. + * + * @param channel the non-null underlying channel + * @param acceptedItemType the non-null, non-empty accepted item type + * @param channelType the non-null, non-empty channel type + * @param label the potentially null, potentially empty label + * @param description the potentially null, potentially empty description + */ + public ScalarWebChannelDescriptor(final ScalarWebChannel channel, final String acceptedItemType, + final String channelType, final @Nullable String label, final @Nullable String description) { + Objects.requireNonNull(channel, "channel cannot be null"); + SonyUtil.validateNotEmpty(acceptedItemType, "acceptedItemType cannot be empty"); + SonyUtil.validateNotEmpty(channelType, "channelType cannot be empty"); + this.channel = channel; + this.channelType = channelType; + this.acceptedItemType = acceptedItemType; + this.label = label; + this.description = description; + } + + /** + * Returns the {@link ScalarWebChannel} from the descriptor + * + * @return a non-null ScalarWebChannel + */ + public ScalarWebChannel getChannel() { + return channel; + } + + /** + * Creates the channel builder from the descriptor + * + * @param thingUid the non-null thing uid to use + * @return the channel builder + */ + public ChannelBuilder createChannel(final ThingUID thingUid) { + Objects.requireNonNull(thingUid, "thingUid canot be null"); + + ChannelBuilder channelBuilder = ChannelBuilder.create(new ChannelUID(thingUid, channel.getChannelId()), + acceptedItemType); + + final ChannelTypeUID stateTypeUid = new ChannelTypeUID(ScalarWebConstants.THING_TYPE_SCALAR.getBindingId(), + channelType); + channelBuilder = channelBuilder.withType(stateTypeUid); + + final String localLabel = label; + if (localLabel != null && !localLabel.isEmpty()) { + channelBuilder = channelBuilder.withLabel(localLabel); + } + + final String localDesc = description; + if (localDesc != null && !localDesc.isEmpty()) { + channelBuilder = channelBuilder.withDescription(localDesc); + } + + channelBuilder.withProperties(channel.getProperties()); + + return channelBuilder; + } + + @Override + public String toString() { + return channel + " accepting " + acceptedItemType + " of type " + channelType; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannelIdFactory.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannelIdFactory.java new file mode 100644 index 0000000000000..eb630d2c35271 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannelIdFactory.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a scalar web channel ID factory. This will create unique channel IDs in the form of + * "{service}#{id}{-nbr}" where "{-nbr}" is only specified if there is a service/id clash and will simply be an + * increasing number. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebChannelIdFactory { + /** The separator for a group */ + public static final char SEPARATOR = '#'; + + /** The channel ids that have already been created */ + private final Set channelIds = new HashSet<>(); + + /** + * Creates a unique channel id from the service name and id + * + * @param serviceName a non-null, non-empty service name + * @param id a non-null, non-empty ID + * @return the non-null, non-empty channel id + */ + public String createChannelId(final String serviceName, final String id) { + String channelId = serviceName + SEPARATOR + id; + + int idx = 0; + while (channelIds.contains(channelId)) { + channelId = serviceName + SEPARATOR + id + "-" + (++idx); + } + + channelIds.add(channelId); + return channelId; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannelTracker.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannelTracker.java new file mode 100644 index 0000000000000..eb0b941fe13f4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebChannelTracker.java @@ -0,0 +1,225 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ChannelUID; + +/** + * This class will track what channels have been linked by category + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebChannelTracker { + /** The lock used to control access to the state */ + private final ReadWriteLock linkLock = new ReentrantReadWriteLock(); + + /** The channel categories that have been linked to which web channels */ + private final Map> linkedChannelIds = new HashMap<>(); + + /** + * Notification that a channel has been linked + * + * @param channel the non-null channel that was linked + */ + public void channelLinked(final ScalarWebChannel channel) { + Objects.requireNonNull(channel, "channel cannot be null"); + + final Lock writeLock = linkLock.writeLock(); + writeLock.lock(); + try { + Set channels = linkedChannelIds.get(channel.getCategory()); + if (channels == null) { + channels = new HashSet<>(); + linkedChannelIds.put(channel.getCategory(), channels); + } + channels.add(channel); + } finally { + writeLock.unlock(); + } + } + + /** + * Notification that a channel has been unlinked + * + * @param channelUID the non-null, non-empty channel UID that was unlinked + * @return true, if successful + */ + public boolean channelUnlinked(final ChannelUID channelUID) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + + final Lock writeLock = linkLock.writeLock(); + writeLock.lock(); + try { + final String channelId = channelUID.getId(); + + boolean found = false; + for (final String id : new HashSet(linkedChannelIds.keySet())) { + final Set channels = linkedChannelIds.get(id); + if (channels.removeIf(ch -> ch.getChannelId().equalsIgnoreCase(channelId))) { + found = true; + if (channels.isEmpty()) { + linkedChannelIds.remove(id); + } + break; + } + } + return found; + } finally { + writeLock.unlock(); + } + } + + /** + * Checks if the specified channel has been linked + * + * @param channel the non-null channel to check + * @return true, if is linked + */ + public boolean isLinked(final ScalarWebChannel channel) { + Objects.requireNonNull(channel, "channel cannot be null"); + + final Lock readLock = linkLock.readLock(); + readLock.lock(); + try { + final String channelId = channel.getChannelId(); + for (final Set chnls : linkedChannelIds.values()) { + for (final ScalarWebChannel chnl : chnls) { + if (chnl.getChannelId().equalsIgnoreCase(channelId)) { + return true; + } + } + } + return false; + } finally { + readLock.unlock(); + } + } + + /** + * Checks if any of the channel category has been linked (atleast one being linked will return true) + * + * @param categories one or more categories to check + * @return true if linked, false otherwise + */ + public boolean isCategoryLinked(final String... categories) { + final Lock readLock = linkLock.readLock(); + readLock.lock(); + try { + for (final String ctgy : categories) { + if (linkedChannelIds.containsKey(ctgy)) { + return true; + } + } + return false; + } finally { + readLock.unlock(); + } + } + + /** + * Checks if any of the channel category has been linked based on a filter + * + * @param ctgyFilter the non-null filter to use + * @return true if linked, false otherwise + */ + public boolean isCategoryLinked(final Parms ctgyFilter) { + Objects.requireNonNull(ctgyFilter, "ctgyFilter cannot be null"); + final Lock readLock = linkLock.readLock(); + readLock.lock(); + try { + for (final String ctgy : linkedChannelIds.keySet()) { + if (ctgyFilter.isMatch(ctgy)) { + return true; + } + } + return false; + } finally { + readLock.unlock(); + } + } + + /** + * Gets the linked channels for the given channel categories + * + * @param categories the categories to get the channels for + * @return the non-null, possibly empty unmodifiable list of linked channels + */ + public Set getLinkedChannelsForCategory(final String... categories) { + final Lock readLock = linkLock.readLock(); + readLock.lock(); + try { + final Set channels = new HashSet<>(); + for (final String ctgy : categories) { + final Set ctgyChannels = linkedChannelIds.get(ctgy); + if (ctgyChannels != null) { + channels.addAll(ctgyChannels); + } + } + return Collections.unmodifiableSet(channels); + } finally { + readLock.unlock(); + } + } + + /** + * Gets the linked channels for any category that passes the filter + * + * @param ctgyFilter the non-null filter to use + * @return the non-null, possibly empty unmodifiable list of linked channels + */ + public Set getLinkedChannelsForCategory(final Parms ctgyFilter) { + Objects.requireNonNull(ctgyFilter, "ctgyFilter cannot be null"); + final Lock readLock = linkLock.readLock(); + readLock.lock(); + try { + final Set channels = new HashSet<>(); + for (final Map.Entry> entry : linkedChannelIds.entrySet()) { + if (ctgyFilter.isMatch(entry.getKey())) { + final Set ctgyChannels = entry.getValue(); + if (ctgyChannels != null) { + channels.addAll(ctgyChannels); + } + } + } + return Collections.unmodifiableSet(channels); + } finally { + readLock.unlock(); + } + } + + /** + * Functional interface to define a category filter + */ + @NonNullByDefault + public interface Parms { + /** + * Returns true if the passed category matches + * + * @param ctgy a non-null category + * @return true if matched, false if not + */ + boolean isMatch(String ctgy); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebClient.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebClient.java new file mode 100644 index 0000000000000..32893353a6909 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebClient.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; + +/** + * This interface represents a scalar web client + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface ScalarWebClient extends AutoCloseable { + + /** + * Gets the device manager + * + * @return the non-null device manager + */ + ScalarWebDeviceManager getDevice(); + + /** + * Gets the service for the specified name + * + * @param serviceName the service name + * @return the service or null if not found + */ + @Nullable + ScalarWebService getService(final String serviceName); +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebClientFactory.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebClientFactory.java new file mode 100644 index 0000000000000..c8b45d2365b46 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebClientFactory.java @@ -0,0 +1,231 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Objects; + +import javax.xml.parsers.ParserConfigurationException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.net.HttpResponse; +import org.openhab.binding.sony.internal.scalarweb.gson.GsonUtilities; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebRequest; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * This class represents a factory to create {@link ScalarWebClient} + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebClientFactory { + /** The likely base URL path to the sony services */ + private static final String LIKELY_PATH = "/sony"; + + /** The likely port for the base URL (ie use default port for protocol [80=http,443=https]) */ + private static final int LIKELY_PORT = -1; + + /** Default audio port for soundbars/receiver (websocket) */ + private static final int HOME_AUDIO_PORT = 10000; + + /** Default audio port for wireless speakers (websocket) */ + private static final int PERSONAL_AUDIO_PORT = 54480; + + /** Websocket guide path */ + private static final String LIKELY_GUIDE_PATH = LIKELY_PATH + "/guide"; + + /** + * Gets a {@link ScalarWebClient} for the given URL and context + * + * @param scalarWebUrl a non-null, non-empty URL + * @param context a non-null context + * @return a {@link ScalarWebClient} + * @throws IOException if an IO Exception occurs + * @throws ParserConfigurationException if a parser configuration exception occurrs + * @throws SAXException if a SAX exception occurs readonly the documents + * @throws URISyntaxException if a URI syntax exception occurs + */ + public static ScalarWebClient get(final String scalarWebUrl, final ScalarWebContext context) + throws IOException, ParserConfigurationException, SAXException, URISyntaxException { + SonyUtil.validateNotEmpty(scalarWebUrl, "scalarWebUrl cannot be empty"); + Objects.requireNonNull(context, "context cannot be null"); + + return get(new URL(scalarWebUrl), context); + } + + /** + * Gets a {@link ScalarWebClient} for the given URL and context + * + * @param scalarWebUrl a non-null URL + * @param context a non-null context + * @return a {@link ScalarWebClient} + * @throws IOException if an IO Exception occurs + * @throws ParserConfigurationException if a parser configuration exception occurrs + * @throws SAXException if a SAX exception occurs readonly the documents + * @throws URISyntaxException if a URI syntax exception occurs + */ + public static ScalarWebClient get(final URL scalarWebUrl, final ScalarWebContext context) + throws IOException, ParserConfigurationException, SAXException, URISyntaxException { + Objects.requireNonNull(scalarWebUrl, "scalarWebUrl cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + final Logger logger = LoggerFactory.getLogger(ScalarWebClientFactory.class); + if (SonyUtil.isEmpty(scalarWebUrl.getPath())) { + return getDefaultClient(scalarWebUrl, context, logger); + } else { + return queryScalarWebSclient(scalarWebUrl, context, logger); + } + } + + /** + * Helper method to attempt to get a 'default' client. Basically if we have just an ip address, try to get a client + * base on querying it + * + * @param scalarWebUrl a non-null URL + * @param context a non-null context + * @param logger a non-null logger + * @return a {@link ScalarWebClient} + * @throws IOException if an IO Exception occurs + * @throws DOMException if a DOM exception occurs readonly the documents + * @throws URISyntaxException if a URI syntax exception occurs + */ + private static ScalarWebClient getDefaultClient(final URL scalarWebUrl, final ScalarWebContext context, + final Logger logger) throws DOMException, IOException, URISyntaxException { + Objects.requireNonNull(scalarWebUrl, "scalarWebUrl cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + Objects.requireNonNull(logger, "logger cannot be null"); + + final ScalarWebClient homeClient = tryDefaultClientUrl(scalarWebUrl, HOME_AUDIO_PORT, context, logger); + if (homeClient != null) { + return homeClient; + } + + final ScalarWebClient personalClient = tryDefaultClientUrl(scalarWebUrl, PERSONAL_AUDIO_PORT, context, logger); + if (personalClient != null) { + return personalClient; + } + + final URL baseUrl = new URL(scalarWebUrl.getProtocol(), scalarWebUrl.getHost(), LIKELY_PORT, LIKELY_PATH); + return new ScalarWebDeviceManager(baseUrl, context); + } + + /** + * Helper method to try a specific port for our scalar url + * + * @param scalarWebUrl a non-null scalar url + * @param port a > 0 port number + * @param context a non-null context + * @param logger a non-null logger + * @return the scalar web client (if found) or null if not + * @throws URISyntaxException if a uri syntax is invalid + * @throws DOMException if a dom exception occurs + * @throws IOException if an IO exception occurs + */ + private static @Nullable ScalarWebClient tryDefaultClientUrl(final URL scalarWebUrl, final int port, + final ScalarWebContext context, final Logger logger) throws URISyntaxException, DOMException, IOException { + Objects.requireNonNull(scalarWebUrl, "scalarWebUrl cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + Objects.requireNonNull(logger, "logger cannot be null"); + if (port <= 0) { + throw new IllegalArgumentException("port cannot be <= 0: " + port); + } + + final URL likelyUrl = new URL(scalarWebUrl.getProtocol(), scalarWebUrl.getHost(), port, LIKELY_GUIDE_PATH); + logger.debug("Testing Default Scalar Web client: {}", likelyUrl); + try (SonyTransport transport = SonyTransportFactory.createHttpTransport(likelyUrl, GsonUtilities.getApiGson(), + context.getClientBuilder())) { + // see ScalarWebRequest id field for explanation of why I used 1 + final ScalarWebResult res = transport + .execute(new ScalarWebRequest(ScalarWebMethod.GETVERSIONS, ScalarWebMethod.V1_0)); + if (res.getHttpResponse().getHttpCode() == HttpStatus.OK_200) { + final URL baseUrl = new URL(scalarWebUrl.getProtocol(), scalarWebUrl.getHost(), port, LIKELY_PATH); + return new ScalarWebDeviceManager(baseUrl, context); + } + } + return null; + } + + /** + * Helper method to get the client to UPNP documents describing the device + * + * @param scalarWebUrl a non-null URL + * @param context a non-null context + * @param logger a non-null logger + * @return a {@link ScalarWebClient} + * @throws IOException if an IO Exception occurs + * @throws ParserConfigurationException if a parser configuration exception occurrs + * @throws SAXException if a SAX exception occurs readonly the documents + * @throws URISyntaxException if a URI syntax exception occurs + */ + public static ScalarWebClient queryScalarWebSclient(final URL scalarWebUrl, final ScalarWebContext context, + final Logger logger) throws URISyntaxException, ParserConfigurationException, SAXException, IOException { + Objects.requireNonNull(scalarWebUrl, "scalarWebUrl cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + Objects.requireNonNull(logger, "logger cannot be null"); + + try (SonyHttpTransport transport = SonyTransportFactory.createHttpTransport(scalarWebUrl.toExternalForm(), + context.getClientBuilder())) { + final HttpResponse resp = transport.executeGet(scalarWebUrl.toExternalForm()); + + if (resp.getHttpCode() == HttpStatus.OK_200) { + final Document scalarWebDocument = resp.getContentAsXml(); + + final NodeList deviceInfos = scalarWebDocument.getElementsByTagNameNS(ScalarWebDeviceManager.SONY_AV_NS, + "X_ScalarWebAPI_DeviceInfo"); + if (deviceInfos.getLength() > 1) { + logger.debug("More than one X_ScalarWebAPI_DeviceInfo found - using the first valid one"); + } + + // Use the first valid one + ScalarWebDeviceManager myDevice = null; + for (int i = deviceInfos.getLength() - 1; i >= 0; i--) { + final Node deviceInfo = deviceInfos.item(i); + + try { + myDevice = ScalarWebDeviceManager.create(deviceInfo, context); + break; + } catch (IOException | DOMException e) { + logger.debug("Exception getting creating scalarwebapi device for {}[{}]: {}", + deviceInfo.getNodeName(), i, e.getMessage(), e); + } + } + if (myDevice == null) { + throw new IOException("No valid scalar web devices found"); + } + + return myDevice; + } else { + // If can't connect - try to connect to the likely websocket server directly using + // the host name and default path + return getDefaultClient(scalarWebUrl, context, logger); + } + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebConfig.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebConfig.java new file mode 100644 index 0000000000000..5d372c05e64ad --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebConfig.java @@ -0,0 +1,199 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.AbstractConfig; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * Configuration class for the scalar web service + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebConfig extends AbstractConfig { + + /** The access code */ + private @Nullable String accessCode; + + /** The commands map file */ + private @Nullable String commandsMapFile; + + /** The URL to the IRCC service */ + private @Nullable String irccUrl; + + /** The model name */ + private @Nullable String modelName; + + /** Flag for configurable presets */ + private @Nullable Boolean configurablePresets; + + // ---- the following properties are not part of the config.xml (and are properties) ---- + + /** The commands map file */ + private @Nullable String discoveredCommandsMapFile; + + /** The commands map file */ + private @Nullable String discoveredModelName; + + /** + * Returns the IP address or host name + * + * @return the IP address or host name + */ + public @Nullable String getIpAddress() { + try { + return new URL(getDeviceAddress()).getHost(); + } catch (final MalformedURLException e) { + return null; + } + } + + /** + * Gets the access code + * + * @return the access code + */ + public @Nullable String getAccessCode() { + return accessCode; + } + + /** + * Sets the access code + * + * @param accessCode the new access code + */ + public void setAccessCode(final String accessCode) { + this.accessCode = accessCode; + } + + /** + * Gets the commands map file + * + * @return the commands map file + */ + public @Nullable String getCommandsMapFile() { + final String localCommandsMapFile = commandsMapFile; + return SonyUtil.defaultIfEmpty(localCommandsMapFile, discoveredCommandsMapFile); + } + + /** + * Sets the commands map file + * + * @param commandsMapFile the new commands map file + */ + public void setCommandsMapFile(final String commandsMapFile) { + this.commandsMapFile = commandsMapFile; + } + + /** + * Sets the discovered commands map file + * + * @param discoveredCommandsMapFile the new commands map file + */ + public void setDiscoveredCommandsMapFile(final String discoveredCommandsMapFile) { + this.discoveredCommandsMapFile = discoveredCommandsMapFile; + } + + /** + * Get the IRCC url to use + * + * @return the ircc url + */ + public @Nullable String getIrccUrl() { + return irccUrl; + } + + /** + * Sets the IRCC url to use + * + * @param irccUrl the ircc url + */ + public void setIrccUrl(final String irccUrl) { + this.irccUrl = irccUrl; + } + + /** + * Get the model name + * + * @return the model name + */ + public @Nullable String getModelName() { + return SonyUtil.defaultIfEmpty(modelName, discoveredModelName); + } + + /** + * Sets the model name + * + * @param modelName the model name + */ + public void setModelName(final String modelName) { + this.modelName = modelName; + } + + /** + * Get the discovered model name + * + * @return the discovered model name + */ + public @Nullable String getDiscoveredModelName() { + return discoveredModelName; + } + + /** + * Sets the discovered model name + * + * @param discoveredModelName the discovered model name + */ + public void setDiscoveredModelName(final @Nullable String discoveredModelName) { + this.discoveredModelName = discoveredModelName; + } + + /** + * + * @return + */ + public @Nullable Boolean isConfigurablePresets() { + return configurablePresets; + } + + /** + * + * @param configurablePresets + */ + public void setConfigurablePresets(final @Nullable Boolean configurablePresets) { + this.configurablePresets = configurablePresets; + } + + @Override + public Map asProperties() { + final Map props = super.asProperties(); + + props.put("discoveredCommandsMapFile", SonyUtil.defaultIfEmpty(discoveredCommandsMapFile, "")); + props.put("discoveredModelName", SonyUtil.defaultIfEmpty(discoveredModelName, "")); + + conditionallyAddProperty(props, "accessCode", accessCode); + conditionallyAddProperty(props, "commandsMapFile", commandsMapFile); + conditionallyAddProperty(props, "irccUrl", irccUrl); + conditionallyAddProperty(props, "modelName", modelName); + conditionallyAddProperty(props, "configurablePresets", configurablePresets); + + return props; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebConstants.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebConstants.java new file mode 100644 index 0000000000000..3191ae8513493 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebConstants.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The class provides all the constants specific to the scalar system. Please note that protocol specific constants are + * found in the individual protocol class + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebConstants { + + // The thing constants + public static final ThingTypeUID THING_TYPE_SCALAR = new ThingTypeUID(SonyBindingConstants.BINDING_ID, + SonyBindingConstants.SCALAR_THING_TYPE_PREFIX); + + // The constant to request an access code + public static final String ACCESSCODE_RQST = "RQST"; + + // The thing properties + public static final String PROP_SERIAL = "Serial #"; + public static final String PROP_MACADDR = "MAC Address"; + public static final String PROP_AREA = "Area"; + public static final String PROP_REGION = "Region"; + public static final String PROP_GENERATION = "Generation"; + public static final String PROP_MODEL = "Model #"; + public static final String PROP_NAME = "Name"; + public static final String PROP_PRODUCT = "Product"; + public static final String PROP_INTERFACEVERSION = "Interface Version"; + public static final String PROP_SERVERNAME = "Server Name"; + public static final String PROP_PRODUCTCATEGORY = "Product Category"; + public static final String PROP_NETIF = "Network Interface"; + public static final String PROP_IPV4 = "IP4 Address"; + public static final String PROP_IPV6 = "IP6 Address"; + public static final String PROP_GATEWAY = "Gateway"; + public static final String PROP_HWADDRESS = "HW Address"; + public static final String PROP_NETMASK = "Net Mask"; + + // The config URI for creating thing types + public static final String CFG_URI = "thing-type:sony:scalarconfig"; +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebContext.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebContext.java new file mode 100644 index 0000000000000..88fce0df239a4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebContext.java @@ -0,0 +1,191 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Supplier; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.openhab.binding.sony.internal.providers.SonyDynamicStateProvider; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.transform.TransformationService; + +/** + * Represents the context for scalar web classes to use. The context will contain various properties that are unique to + * a thing and can be accessed by other objects + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebContext { + /** The supplier of a thing */ + private final Supplier thingSupplier; + + /** The channel tracker */ + private final ScalarWebChannelTracker tracker; + + /** The scheduler to use */ + private final ScheduledExecutorService scheduler; + + /** The dynamic state provider */ + private final SonyDynamicStateProvider stateProvider; + + /** The websocket client to use (if specified) */ + private final @Nullable WebSocketClient webSocketClient; + + /** The http client builder to use (if specified) */ + private final ClientBuilder clientBuilder; + + /** The transformation service to use (if specified) */ + private final @Nullable TransformationService transformService; + + /** The configuration for the thing */ + private final ScalarWebConfig config; + + /** The osgi properties */ + private final Map osgiProperties; + + /** + * Constructs the context from the parameters + * + * @param thingSupplier the non-null thing supplier + * @param config the non-null configuration + * @param tracker the non-null channel tracker + * @param scheduler the non-null scheduler + * @param stateProvider the non-null dynamic state provider + * @param webSocketClient the possibly null websocket client + * @param clientBuilder the possibly null http client builder + * @param transformService the possibly null transformation service + * @param osgiProperties the non-null OSGI properties + */ + public ScalarWebContext(final Supplier thingSupplier, final ScalarWebConfig config, + final ScalarWebChannelTracker tracker, final ScheduledExecutorService scheduler, + final SonyDynamicStateProvider stateProvider, final @Nullable WebSocketClient webSocketClient, + final ClientBuilder clientBuilder, final @Nullable TransformationService transformService, + final Map osgiProperties) { + Objects.requireNonNull(thingSupplier, "thingSupplier cannot be null"); + Objects.requireNonNull(config, "config cannot be null"); + Objects.requireNonNull(tracker, "tracker cannot be null"); + Objects.requireNonNull(clientBuilder, "clientBuilder cannot be null"); + Objects.requireNonNull(scheduler, "scheduler cannot be null"); + Objects.requireNonNull(stateProvider, "stateProvider cannot be null"); + Objects.requireNonNull(osgiProperties, "osgiProperties cannot be null"); + + this.thingSupplier = thingSupplier; + this.config = config; + this.tracker = tracker; + this.scheduler = scheduler; + this.stateProvider = stateProvider; + this.webSocketClient = webSocketClient; + this.clientBuilder = clientBuilder; + this.transformService = transformService; + this.osgiProperties = osgiProperties; + } + + /** + * Returns the thing for the context + * + * @return a non-null thing + */ + public Thing getThing() { + return thingSupplier.get(); + } + + /** + * Returns the thing uid for the context + * + * @return the non-null thing uid + */ + public ThingUID getThingUID() { + return getThing().getUID(); + } + + /** + * Returns the thing configuration + * + * @return the non-null configuration + */ + public ScalarWebConfig getConfig() { + return config; + } + + /** + * Returns the channel tracker for the thing + * + * @return a non-null channel tracker + */ + public ScalarWebChannelTracker getTracker() { + return tracker; + } + + /** + * Returns the thing's scheduler + * + * @return a non-null scheduler + */ + public ScheduledExecutorService getScheduler() { + return scheduler; + } + + /** + * Returns the dynamic state provider + * + * @return the non-null dynamic state provider + */ + public SonyDynamicStateProvider getStateProvider() { + return stateProvider; + } + + /** + * Returns the websocket client to use + * + * @return the possibly null websocket client + */ + public @Nullable WebSocketClient getWebSocketClient() { + return webSocketClient; + } + + /** + * Returns the http client builder to use + * + * @return the http client builder + */ + public ClientBuilder getClientBuilder() { + return clientBuilder; + } + + /** + * Returns the transformation service to use + * + * @return the possibly null transformation service + */ + public @Nullable TransformationService getTransformService() { + return transformService; + } + + /** + * Returns the OSGI properties + * + * @return the non-null, possibly empty OSGI properties + */ + public Map getOsgiProperties() { + return osgiProperties; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebDeviceManager.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebDeviceManager.java new file mode 100644 index 0000000000000..7a8cda354e49e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebDeviceManager.java @@ -0,0 +1,272 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.gson.GsonUtilities; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.ServiceProtocol; +import org.openhab.binding.sony.internal.scalarweb.models.api.ServiceProtocols; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApi; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.DOMException; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import com.google.gson.Gson; + +/** + * This class represents device manager + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebDeviceManager implements ScalarWebClient { + /** The Constant for the sony upnp identifier */ + public static final String SONY_AV_NS = "urn:schemas-sony-com:av"; + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebDeviceManager.class); + + /** The device version */ + private String version; + + /** The URL of the device */ + private URL baseUrl; + + /** The services offered by the device */ + private final Map services; + + /** + * Constructs a device manager from the base URL + * + * @param baseUrl a non-null base url + * @param context a non-null context to use + * @throws IOException if an IOException occurs contacting the device + * @throws DOMException if a DOMException occurs processing the XML from the device + * @throws MalformedURLException if there is a malformed URL (in the device XML) + * @throws URISyntaxException if a URI syntax exception occurs + */ + public ScalarWebDeviceManager(final URL baseUrl, final ScalarWebContext context) + throws IOException, DOMException, MalformedURLException, URISyntaxException { + this(baseUrl, "1.0", new HashSet<>(), context); + } + + /** + * Private contructor to create a device manager from the parameters + * + * @param baseUrl a non-null base URL + * @param version a non-null, non-empty API version + * @param serviceProtocols a non-null, possibly empty list of protocols + * @param context a non-null context + * @throws IOException if an IOException occurs contacting the device + * @throws DOMException if a DOMException occurs processing the XML from the device + * @throws MalformedURLException if there is a malformed URL (in the device XML) + * @throws URISyntaxException if an URI exception occurs + */ + private ScalarWebDeviceManager(final URL baseUrl, final String version, final Set serviceProtocols, + final ScalarWebContext context) + throws IOException, DOMException, MalformedURLException, URISyntaxException { + Objects.requireNonNull(baseUrl, "baseUrl cannot be null"); + SonyUtil.validateNotEmpty(version, "version cannot be empty"); + Objects.requireNonNull(serviceProtocols, "serviceProtocols cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + this.version = version; + this.baseUrl = baseUrl; + final Gson gson = GsonUtilities.getApiGson(); + + final SonyTransportFactory transportFactory = new SonyTransportFactory(baseUrl, gson, + context.getWebSocketClient(), context.getScheduler(), context.getClientBuilder()); + + final Set myServiceProtocols = new HashSet<>(serviceProtocols); + + try (final SonyHttpTransport httpTransport = SonyTransportFactory.createHttpTransport(baseUrl, + ScalarWebService.GUIDE, context.getClientBuilder())) { + // Manually create the guide as it's used to get service protocols and supported methods + // Must use alternative supported api since SupportedApi requires a guide! + final SupportedApi guideApi = SupportedApi.getSupportApiAlternate(ScalarWebService.GUIDE, httpTransport, + logger); + final ScalarWebService guide = new ScalarWebService(transportFactory, + new ServiceProtocol(ScalarWebService.GUIDE, Collections.singleton(SonyTransportFactory.HTTP)), + version, guideApi); + + final ServiceProtocols sps = guide.execute(ScalarWebMethod.GETSERVICEPROTOCOLS).as(ServiceProtocols.class); + for (final ServiceProtocol serviceProtocol : sps.getServiceProtocols()) { + // remove the one from the device descriptor above (keyed by name) + // the add this one (which has protocol information) + myServiceProtocols.remove(serviceProtocol); + myServiceProtocols.add(serviceProtocol); + } + + final Map myServices = new HashMap(); + myServices.put(ScalarWebService.GUIDE, guide); + + for (ServiceProtocol serviceProtocol : myServiceProtocols) { + // Ignore the guid - we already added it above + if (ScalarWebService.GUIDE.equalsIgnoreCase(serviceProtocol.getServiceName())) { + continue; + } + + // Must create a new http transport specific to the service name in case + // getSupportedApi doesn't exist on the guide service and we fallback to using + // an http getversions/getmethodtypes alternative for the service + try (final SonyHttpTransport srvHttpTransport = SonyTransportFactory.createHttpTransport(baseUrl, + serviceProtocol.getServiceName(), context.getClientBuilder())) { + final SupportedApi srvApi = SupportedApi.getSupportedApi(guide, serviceProtocol.getServiceName(), + srvHttpTransport, logger); + final ScalarWebService sws = new ScalarWebService(transportFactory, serviceProtocol, version, + srvApi); + myServices.put(sws.getServiceName(), sws); + } + } + services = Collections.unmodifiableMap(myServices); + } + } + + /** + * Creates a scalar web device manager from the provided information + * + * @param deviceInfo a non-null device info node describing the device + * @param context a non-null context to use + * @return a non-null scalar web device manager + * @throws IOException if an IOException occurs contacting the device + * @throws DOMException if a DOMException occurs processing the XML from the device + * @throws MalformedURLException if there is a malformed URL (in the device XML) + * @throws URISyntaxException if a URI syntax exception occurs + */ + public static ScalarWebDeviceManager create(final Node deviceInfo, final ScalarWebContext context) + throws IOException, DOMException, MalformedURLException, URISyntaxException { + Objects.requireNonNull(deviceInfo, "service cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + String version = "1.0"; // default version + URL baseUrl = null; + + final Set serviceProtocols = new HashSet(); + + final NodeList nodes = deviceInfo.getChildNodes(); + for (int i = nodes.getLength() - 1; i >= 0; i--) { + final Node node = nodes.item(i); + final String nodeName = node.getLocalName(); + + if ("X_ScalarWebAPI_Version".equalsIgnoreCase(nodeName)) { + version = node.getTextContent(); + } else if ("X_ScalarWebAPI_BaseURL".equalsIgnoreCase(nodeName)) { + baseUrl = new URL(node.getTextContent()); + } else if ("X_ScalarWebAPI_ServiceList".equalsIgnoreCase(nodeName)) { + final NodeList sts = ((Element) node).getElementsByTagNameNS(SONY_AV_NS, "X_ScalarWebAPI_ServiceType"); + + for (int j = sts.getLength() - 1; j >= 0; j--) { + // assume auto transport since we don't know + serviceProtocols.add(new ServiceProtocol(sts.item(j).getTextContent(), + Collections.singleton(SonyTransportFactory.AUTO))); + } + } + } + + if (baseUrl == null) { + throw new IOException("X_ScalarWebAPI_BaseURL was not found"); + } + + return new ScalarWebDeviceManager(baseUrl, version, serviceProtocols, context); + } + + /** + * Gets the device version + * + * @return the device version + */ + public String getVersion() { + return version; + } + + /** + * Gets the base url of the device + * + * @return the base url of the device + */ + public URL getBaseUrl() { + return baseUrl; + } + + /** + * Gets the services offered by the device + * + * @return the non-null, possibly empty immutable collection of services + */ + public Collection getServices() { + return services.values(); + } + + /** + * Gets the service for the service name + * + * @param serviceName the service name to try + * @return the service or null if not found + */ + public @Nullable ScalarWebService getService(final String serviceName) { + return services.get(serviceName); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + final String newLine = System.lineSeparator(); + + final Set serviceNames = new TreeSet(services.keySet()); + + for (final String serviceName : serviceNames) { + final ScalarWebService service = services.get(serviceName); + if (service != null) { + sb.append(service); + sb.append(newLine); + } + } + + return sb.toString(); + } + + @Override + public ScalarWebDeviceManager getDevice() { + return this; + } + + @Override + public void close() { + for (final Entry srv : services.entrySet()) { + srv.getValue().close(); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebDiscoveryParticipant.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebDiscoveryParticipant.java new file mode 100644 index 0000000000000..3810f4a118f48 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebDiscoveryParticipant.java @@ -0,0 +1,241 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.model.meta.RemoteDevice; +import org.jupnp.model.meta.RemoteDeviceIdentity; +import org.jupnp.model.meta.RemoteService; +import org.jupnp.model.types.ServiceId; +import org.jupnp.model.types.UDN; +import org.openhab.binding.sony.internal.AbstractDiscoveryParticipant; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.UidUtils; +import org.openhab.binding.sony.internal.providers.SonyDefinitionProvider; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * This implementation of the {@link UpnpDiscoveryParticipant} provides discovery of Sony SCALAR protocol devices. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "discovery.sony-scalar") +public class ScalarWebDiscoveryParticipant extends AbstractDiscoveryParticipant implements UpnpDiscoveryParticipant { + // See footnotes in createResult for the purpose of this field + private static final Map SCALARTOIRCC = new HashMap<>(); + + /** + * Constructs the participant + * + * @param sonyDefinitionProvider a non-null sony definition provider + */ + @Activate + public ScalarWebDiscoveryParticipant(final @Reference SonyDefinitionProvider sonyDefinitionProvider) { + super(SonyBindingConstants.SCALAR_THING_TYPE_PREFIX, sonyDefinitionProvider); + } + + @Override + protected boolean getDiscoveryEnableDefault() { + return true; + } + + @Override + public @Nullable DiscoveryResult createResult(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + + if (!isDiscoveryEnabled()) { + return null; + } + + /** + * We need to handle a bunch of situations + * 1. Scalar service with no IRCC at all + * 2. IRCC service with no Scalar service + * 3. SCALAR and IRCC service together + * 4. SCALAR separate from the IRCC service + * 4a. where the IRCC service comes in BEFORE the scalar service + * 4b. where the IRCC service comes in AFTER the scalar service + * 5. Both 3 and 4 (it advertises both together and separate) + * + * Since the configuration contains an IRCC URL, we need to handle an IRCC service being advertised separately + * (and can come in before or after the scalar notification) + * + * In case of #3, we prefer the URL from the scalar one (and will ignore the IRCC one) + * + * If we receive an IRCC service with no scalar service (maybe #2, #4 or #5 scenarios) + * -- Check to see if we have a prior scalar result (#4b or #5) + * -- -- If we have a prior result and no URL - add our URL and create a new result with the IRCC url (#4b) + * -- -- If we have a prior result and a URL - ignore the IRCC service (#5) + * -- -- If no scalar service - save our URL for future results but do NOT create a result (#2, #4a or #5) + * + * -- If it's a SCALAR service (#1, #3, #4a, #5) + * -- -- If we have no IRCC service and no saved IRCC url - create an entry (#1/#4b/#5) + * -- -- If we have no IRCC service and a null saved IRCC url - use null (#1/#4b/#5) + * -- -- If we have no IRCC service and a non-null saved IRCC url - use the url (#4a/#5) + * -- -- If we have a IRCC service, use scalar URL as IRCC and save it (#3/#5) + * -- -- Create a result + */ + if (!isSonyDevice(device)) { + return null; + } + + final String modelName = getModelName(device); + if (modelName == null || modelName.isEmpty()) { + logger.debug("Found Sony device but it has no model name - ignoring: {}", device); + return null; + } + + final RemoteDeviceIdentity identity = device.getIdentity(); + + final RemoteService irccService = device.findService( + new ServiceId(SonyBindingConstants.SONY_SERVICESCHEMA, SonyBindingConstants.SONY_IRCCSERVICENAME)); + final RemoteService scalarWebService = device.findService( + new ServiceId(SonyBindingConstants.SONY_SERVICESCHEMA, SonyBindingConstants.SONY_SCALARWEBSERVICENAME)); + + if (irccService != null && scalarWebService == null) { + final ThingUID thingUID = getThingUID(device, modelName); + final String irccUrl = identity.getDescriptorURL().toString(); + + if (SCALARTOIRCC.containsKey(thingUID)) { + final String oldIrccUrl = SCALARTOIRCC.get(thingUID); + if (oldIrccUrl == null) { + SCALARTOIRCC.put(thingUID, irccUrl); + return createResult(device, thingUID, irccUrl); + } + } else { + SCALARTOIRCC.put(thingUID, irccUrl); + } + return null; + } + + if (scalarWebService == null) { + logger.debug("Found sony device but ignored because of no scalar service: {}", device); + return null; + } + + final ThingUID uid = getThingUID(device); + if (uid == null) { + // no need for log message as getThingUID spits them out + return null; + } + + String irccUrl = null; + if (irccService == null) { + if (SCALARTOIRCC.containsKey(uid)) { + irccUrl = SCALARTOIRCC.get(uid); + } else { + SCALARTOIRCC.put(uid, null); + } + } else { + irccUrl = identity.getDescriptorURL().toString(); + SCALARTOIRCC.put(uid, irccUrl); + } + + return createResult(device, uid, irccUrl); + } + + @Override + public @Nullable ThingUID getThingUID(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + + if (!isDiscoveryEnabled()) { + return null; + } + + if (isSonyDevice(device)) { + final String modelName = getModelName(device); + if (modelName == null || modelName.isEmpty()) { + logger.debug("Found Sony device but it has no model name - ignoring"); + return null; + } + + final RemoteService scalarWebService = device.findService(new ServiceId( + SonyBindingConstants.SONY_SERVICESCHEMA, SonyBindingConstants.SONY_SCALARWEBSERVICENAME)); + if (scalarWebService != null) { + final RemoteDeviceIdentity identity = device.getIdentity(); + if (identity != null) { + final UDN udn = device.getIdentity().getUdn(); + logger.debug("Found Sony WebScalarAPI service: {}", udn); + final ThingTypeUID modelUID = getThingTypeUID(modelName); + return UidUtils.createThingUID(modelUID == null ? ScalarWebConstants.THING_TYPE_SCALAR : modelUID, + udn); + } else { + logger.debug("Found Sony WebScalarAPI service but it had no identity!"); + } + } + } + + return null; + } + + /** + * Helper method to create a result from the device, uid and possibly irccurl + * + * @param device a non-null device + * @param uid a non-null thing uid + * @param irccUrl a possibly null, possibly empty irccurl + * @return a non-null result + */ + private DiscoveryResult createResult(final RemoteDevice device, final ThingUID uid, + final @Nullable String irccUrl) { + Objects.requireNonNull(device, "device cannot be null"); + Objects.requireNonNull(uid, "uid cannot be null"); + + final RemoteDeviceIdentity identity = device.getIdentity(); + final URL scalarURL = identity.getDescriptorURL(); + + final ScalarWebConfig config = new ScalarWebConfig(); + config.setDeviceAddress(scalarURL.toString()); + config.setIrccUrl(irccUrl == null ? "" : irccUrl); + + config.setDiscoveredCommandsMapFile("scalar-" + uid.getId() + ".map"); + config.setDiscoveredMacAddress(getMacAddress(identity, uid)); + config.setDiscoveredModelName(getModelName(device)); + + final String thingId = UidUtils.getThingId(identity.getUdn()); + return DiscoveryResultBuilder.create(uid).withProperties(config.asProperties()) + .withProperty("ScalarUDN", SonyUtil.defaultIfEmpty(thingId, uid.getId())) + .withRepresentationProperty("ScalarUDN").withLabel(getLabel(device, "Scalar")).build(); + } + + /** + * Helper method to get a thing UID from the device and model name + * + * @param device a non-null device + * @param modelName a non-null, non-empty modelname + * @return + */ + private ThingUID getThingUID(final RemoteDevice device, final String modelName) { + Objects.requireNonNull(device, "device cannot be null"); + SonyUtil.validateNotEmpty(modelName, "modelName cannot be empty"); + + final UDN udn = device.getIdentity().getUdn(); + final ThingTypeUID modelUID = getThingTypeUID(modelName); + return UidUtils.createThingUID(modelUID == null ? ScalarWebConstants.THING_TYPE_SCALAR : modelUID, udn); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebHandler.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebHandler.java new file mode 100644 index 0000000000000..bbbac6442bad2 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/ScalarWebHandler.java @@ -0,0 +1,553 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javax.ws.rs.client.ClientBuilder; +import javax.xml.parsers.ParserConfigurationException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.openhab.binding.sony.internal.AbstractThingHandler; +import org.openhab.binding.sony.internal.AccessResult; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.providers.SonyDefinitionProvider; +import org.openhab.binding.sony.internal.providers.SonyDynamicStateProvider; +import org.openhab.binding.sony.internal.providers.SonyModelListener; +import org.openhab.binding.sony.internal.providers.models.SonyDeviceCapability; +import org.openhab.binding.sony.internal.providers.models.SonyServiceCapability; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.protocols.ScalarWebLoginProtocol; +import org.openhab.binding.sony.internal.scalarweb.protocols.ScalarWebProtocol; +import org.openhab.binding.sony.internal.scalarweb.protocols.ScalarWebProtocolFactory; +import org.openhab.binding.sony.internal.scalarweb.protocols.ScalarWebSystemProtocol; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelKind; +import org.openhab.core.transform.TransformationService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.SAXException; + +/** + * The thing handler for a Sony Webscalar device. This is the entry point provides a full two interaction between + * openhab and the webscalar system. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebHandler extends AbstractThingHandler { + /** + * The logger + */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebHandler.class); + + /** + * The tracker + */ + private final ScalarWebChannelTracker tracker = new ScalarWebChannelTracker(); + + /** + * The protocol handler being used - will be null if not initialized. + */ + private final AtomicReference<@Nullable ScalarWebClient> scalarClient = new AtomicReference<>(null); + + /** + * The protocol handler being used - will be null if not initialized. + */ + private final AtomicReference<@Nullable ScalarWebProtocolFactory> protocolFactory = new AtomicReference<>( + null); + + /** + * The thing callback + */ + private final ThingCallback callback; + + /** + * The transformation service to use + */ + private final @Nullable TransformationService transformationService; + + /** + * The websocket client to use + */ + private final WebSocketClient webSocketClient; + + /** + * The clientBuilder used in HttpRequest + */ + private final ClientBuilder clientBuilder; + + /** + * The definition provider to use + */ + private final SonyDefinitionProvider sonyDefinitionProvider; + + /** + * The dynamic state provider to use + */ + private final SonyDynamicStateProvider sonyDynamicStateProvider; + + /** + * The definition listener + */ + private final DefinitionListener definitionListener = new DefinitionListener(); + + /** + * The OSGI properties for things + */ + private final Map osgiProperties; + + /** + * Constructs the web handler + * + * @param thing a non-null thing + * @param transformationService a possibly null transformation service + * @param webSocketClient a non-null websocket client + * @param clientBuilder a non-null http client builder + * @param sonyDefinitionProvider a non-null definition provider + * @param sonyDynamicStateProvider a non-null dynamic state provider + * @param osgiProperties a non-null, possibly empty list of OSGI properties + */ + public ScalarWebHandler(final Thing thing, final @Nullable TransformationService transformationService, + final WebSocketClient webSocketClient, final ClientBuilder clientBuilder, + final SonyDefinitionProvider sonyDefinitionProvider, + final SonyDynamicStateProvider sonyDynamicStateProvider, final Map osgiProperties) { + super(thing, ScalarWebConfig.class); + + this.transformationService = transformationService; + this.webSocketClient = webSocketClient; + this.clientBuilder = clientBuilder; + this.sonyDefinitionProvider = sonyDefinitionProvider; + this.sonyDynamicStateProvider = sonyDynamicStateProvider; + this.osgiProperties = osgiProperties; + + callback = new ThingCallback() { + @Override + public void statusChanged(final ThingStatus state, final ThingStatusDetail detail, + final @Nullable String msg) { + updateStatus(state, detail, msg); + } + + @Override + public void stateChanged(final String channelId, final State newState) { + final ThingStatus status = getThing().getStatus(); + if (status == ThingStatus.ONLINE) { + updateState(channelId, newState); + } else { + // usually happens when we receive event notification during initialization + logger.trace("Ignoring state update during {}: {}={}", status, channelId, newState); + } + } + + @Override + public void setProperty(final String propertyName, final @Nullable String propertyValue) { + // change meaning of null propertyvalue + // setProperty says remove - here we are ignoring + if (propertyValue != null && !propertyValue.isEmpty()) { + getThing().setProperty(propertyName, propertyValue); + } + + // Update the discovered model name if found + if (propertyName.equals(ScalarWebConstants.PROP_MODEL) && propertyValue != null + && !propertyValue.isEmpty()) { + final ScalarWebConfig swConfig = getSonyConfig(); + swConfig.setDiscoveredModelName(propertyValue); + + final Configuration config = getConfig(); + config.setProperties(swConfig.asProperties()); + + updateConfiguration(config); + } + } + }; + } + + @Override + protected void handleRefreshCommand(final ChannelUID channelUID) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + + doHandleCommand(channelUID, RefreshType.REFRESH); + } + + @Override + protected void handleSetCommand(final ChannelUID channelUID, final Command command) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + doHandleCommand(channelUID, command); + } + + @Override + protected PowerCommand handlePotentialPowerOnCommand(final ChannelUID channelUID, final Command command) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + final Channel channel = getThing().getChannel(channelUID.getId()); + if (channel != null) { + final ScalarWebChannel scalarChannel = new ScalarWebChannel(channelUID, channel); + if (scalarChannel.getService().equals(ScalarWebService.SYSTEM) + && scalarChannel.getCategory().equals(ScalarWebSystemProtocol.POWERSTATUS)) { + if (command instanceof OnOffType) { + if (command == OnOffType.ON) { + SonyUtil.sendWakeOnLan(logger, getSonyConfig().getDeviceIpAddress(), + getSonyConfig().getDeviceMacAddress()); + return PowerCommand.ON; + } else { + return PowerCommand.OFF; + } + } + } + } + return PowerCommand.NON; + } + + /** + * Handles a command from the system. This will determine the protocol to send the command to + * + * @param channelUID a non-null channel UID + * @param command a non-null command + */ + private void doHandleCommand(final ChannelUID channelUID, final Command command) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + if (getThing().getStatus() != ThingStatus.ONLINE) { + logger.debug("Not online. Ignoring command {} {}", channelUID, command); + return; + } + + final Channel channel = getThing().getChannel(channelUID.getId()); + if (channel == null) { + logger.debug("Channel for {} could not be found", channelUID); + return; + } + final ScalarWebChannel scalarChannel = new ScalarWebChannel(channelUID, channel); + + final ScalarWebProtocolFactory localProtocolFactory = protocolFactory.get(); + if (localProtocolFactory == null) { + logger.debug("Trying to handle a channel command before a protocol factory has been created"); + return; + } + + final ScalarWebProtocol protocol = localProtocolFactory.getProtocol(scalarChannel.getService()); + if (protocol == null) { + logger.debug("Unknown channel service: {} for {} and command {}", scalarChannel.getService(), channelUID, + command); + } else { + if (command instanceof RefreshType) { + protocol.refreshChannel(scalarChannel); + } else { + protocol.setChannel(scalarChannel, command); + } + } + } + + @Override + protected void connect() { + final ScalarWebConfig config = getSonyConfig(); + + final String scalarWebUrl = config.getDeviceAddress(); + if (scalarWebUrl == null || scalarWebUrl.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "ScalarWeb URL is missing from configuration"); + return; + } + + logger.debug("Attempting connection to Scalar Web device..."); + try { + SonyUtil.checkInterrupt(); + + // TODO Test + if (scalarClient.get() != null && protocolFactory.get() != null) { + logger.debug("Trying to reuse available client and protocols"); + final @Nullable ScalarWebClient client = scalarClient.get(); + // check if scalar web service is available + if (client != null && client.getService(ScalarWebService.GUIDE) != null) { + final @Nullable ScalarWebService guideService = client.getService(ScalarWebService.GUIDE); + if (guideService != null) { + final ScalarWebResult result = guideService.execute(ScalarWebMethod.GETVERSIONS); + if (!result.isError()) { + logger.debug("Reuse of client and protocols successful"); + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); + this.scheduler.submit(() -> { + // Refresh the state right away + refreshState(true); + }); + return; + } else { + logger.debug("Reuse of client and protocols not successful"); + } + } + } + } + + final ScalarWebContext context = new ScalarWebContext(() -> getThing(), config, tracker, scheduler, + sonyDynamicStateProvider, webSocketClient, clientBuilder, transformationService, osgiProperties); + + final ScalarWebClient client = ScalarWebClientFactory.get(scalarWebUrl, context); + scalarClient.set(client); + + final ScalarWebLoginProtocol loginHandler = new ScalarWebLoginProtocol<>(client, config, + callback, transformationService, clientBuilder); + + final AccessResult result = loginHandler.login(); + SonyUtil.checkInterrupt(); + + if (result.equals(AccessResult.OK)) { + final ScalarWebProtocolFactory factory = new ScalarWebProtocolFactory<>(context, client, + callback); + + SonyUtil.checkInterrupt(); + + final ThingBuilder thingBuilder = editThing(); + thingBuilder.withChannels(getChannels(factory)); + updateThing(thingBuilder.build()); + + SonyUtil.checkInterrupt(); + + SonyUtil.close(protocolFactory.getAndSet(factory)); + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); + logger.debug("Thing status set to online"); + + // add already linked channels to the tracker to enable state refresh + getThing().getChannels().stream().filter(c -> isLinked(c.getUID())) + .forEach(c -> tracker.channelLinked(new ScalarWebChannel(c.getUID(), c))); + + // Add a listener for model updates + final String modelName = getModelName(); + if (modelName != null && !modelName.isEmpty()) { + sonyDefinitionProvider.removeListener(definitionListener); + sonyDefinitionProvider.addListener(modelName, getThing().getThingTypeUID(), definitionListener); + } + + this.scheduler.submit(() -> { + // Refresh the state right away + refreshState(true); + // after state is refreshed - write the definition + // (which could include dynamic state from refresh) + writeThingDefinition(); + writeDeviceCapabilities(client); + + }); + } else { + // If it's a pending access (or code not accepted), update with a configuration error + // this prevents a reconnect (which will cancel any current registration code) + // Note: there are other access code type errors that probably should be trapped here + // as well - but those are the major two (probably represent 99% of the cases) + // and we handle them separately + if (result.equals(AccessResult.PENDING) || result.equals(AccessResult.NOTACCEPTED)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, result.getMsg()); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, result.getMsg()); + } + } + } catch (final InterruptedException e) { + logger.debug("Initialization was interrupted"); + // status would have already been set if interrupted - don't update it + // since another instance of this will occur + } catch (IOException | ParserConfigurationException | SAXException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Error connecting to Scalar Web device (may need to turn it on manually)"); + } catch (final Exception e) { + logger.error("Unhandled exception connecting to Scalar Web device: {} ", e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Unhandled exception connecting to Scalar Web device (may need to turn it on manually): " + + e.getMessage()); + } + } + + /** + * Helper method to get the channels to configure + * + * @param factory the non-null factory to use + * @return a non-null, possibly empty list of channels + */ + private Channel[] getChannels(final ScalarWebProtocolFactory factory) { + Objects.requireNonNull(factory, "factory cannot be null"); + + final ThingUID thingUid = getThing().getUID(); + final ThingTypeUID typeUID = getThing().getThingTypeUID(); + final boolean genericThing = ScalarWebConstants.THING_TYPE_SCALAR.equals(typeUID); + + final Map channels = genericThing ? new HashMap<>() + : getThing().getChannels().stream().collect(Collectors.toMap(chl -> chl.getUID().getId(), chl -> chl)); + + // Get all channel descriptors if we are generic + // Get ONLY the app control descriptors (which are dynamic) if not + for (final ScalarWebChannelDescriptor descriptor : factory.getChannelDescriptors(!genericThing)) { + final Channel channel = descriptor.createChannel(thingUid).build(); + final String channelId = channel.getUID().getId(); + if (channels.containsKey(channelId)) { + logger.debug("Channel definition already exists for {}: {}", channel.getUID().getId(), descriptor); + } else { + logger.debug("Creating channel: {}", descriptor); + channels.put(channelId, channel); + } + } + return channels.values().toArray(new Channel[0]); + } + + /** + * Helper method to get a model name + * + * @return a possibly null model name + */ + private @Nullable String getModelName() { + final String modelName = getSonyConfig().getModelName(); + if (modelName != null && !modelName.isEmpty() && SonyUtil.isValidModelName(modelName)) { + return modelName; + } + + final String thingLabel = thing.getLabel(); + return thingLabel != null && !thingLabel.isEmpty() && SonyUtil.isValidModelName(thingLabel) ? thingLabel : null; + } + + /** + * Helper method to write out a device capability + * + * @param client a non-null client + */ + private void writeDeviceCapabilities(final ScalarWebClient client) { + Objects.requireNonNull(client, "client cannot be null"); + + final String modelName = getModelName(); + if (modelName == null || modelName.isEmpty()) { + logger.debug("Could not write device capabilities file - model name was missing from properties"); + } else { + final URL baseUrl = client.getDevice().getBaseUrl(); + + final List srvCapabilities = client.getDevice().getServices().stream() + .map(srv -> new SonyServiceCapability(srv.getServiceName(), srv.getVersion(), + srv.getTransport().getProtocolType(), + srv.getMethods().stream().sorted(ScalarWebMethod.COMPARATOR).collect(Collectors.toList()), + srv.getNotifications().stream().sorted(ScalarWebMethod.COMPARATOR) + .collect(Collectors.toList()))) + .collect(Collectors.toList()); + + logger.debug("Writing device capability: {}", modelName); + sonyDefinitionProvider + .writeDeviceCapabilities(new SonyDeviceCapability(modelName, baseUrl, srvCapabilities)); + } + } + + /** + * Helper method to write thing definition from our thing + */ + private void writeThingDefinition() { + final String modelName = getModelName(); + if (modelName == null || modelName.isEmpty()) { + logger.debug("Could not write thing type file - model name was missing from properties"); + } else { + // Only write things that are state channels, have a valid channel type and are not + // from the app control service (which is too dynamic - what apps are installed) + final Predicate chlFilter = chl -> chl.getKind() == ChannelKind.STATE + && chl.getChannelTypeUID() != null + && !ScalarWebService.APPCONTROL.equalsIgnoreCase(chl.getUID().getGroupId()); + + logger.debug("Writing thing definition: {}", modelName); + sonyDefinitionProvider.writeThing(SonyBindingConstants.SCALAR_THING_TYPE_PREFIX, ScalarWebConstants.CFG_URI, + modelName, getThing(), chlFilter); + } + } + + @Override + protected void refreshState(boolean initial) { + final ScalarWebProtocolFactory protocolHandler = protocolFactory.get(); + if (protocolHandler == null) { + logger.debug("Protocol factory wasn't set"); + } else { + logger.debug("Refreshing all state"); + protocolHandler.refreshAllState(scheduler, initial); + } + } + + @Override + protected URL getCheckStatusUrl() throws MalformedURLException { + // If using simplifed config (where we discover stuff) + // use the discovered ipaddress/port rather than configured + final ScalarWebClient client = scalarClient.get(); + return client == null ? getSonyConfig().getDeviceUrl() : client.getDevice().getBaseUrl(); + } + + @Override + public void channelUnlinked(final ChannelUID channelUID) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + + tracker.channelUnlinked(channelUID); + super.channelUnlinked(channelUID); + } + + @Override + public void channelLinked(final ChannelUID channelUID) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + final Channel channel = getThing().getChannel(channelUID.getId()); + if (channel == null) { + logger.debug("channel linked called but channelUID {} could not be found", channelUID); + } else { + tracker.channelLinked(new ScalarWebChannel(channelUID, channel)); + } + super.channelLinked(channelUID); + } + + @Override + public void dispose() { + super.dispose(); + sonyDefinitionProvider.removeListener(definitionListener); + SonyUtil.close(protocolFactory.getAndSet(null)); + SonyUtil.close(scalarClient.getAndSet(null)); + } + + /** + * A listener to definition changes (ie thing type changes) + */ + private class DefinitionListener implements SonyModelListener { + @Override + public void thingTypeFound(final ThingTypeUID uid) { + final String modelName = getModelName(); + // if we are resetting back to the generic version + // or if we matched our model (going from generic to specific or updating to a new version of specific) + // then change our thing type + if (ScalarWebConstants.THING_TYPE_SCALAR.equals(uid) || (modelName != null && !modelName.isEmpty() + && SonyUtil.isModelMatch(uid, SonyBindingConstants.SCALAR_THING_TYPE_PREFIX, modelName))) { + changeThingType(uid, getConfig()); + } + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/VersionUtilities.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/VersionUtilities.java new file mode 100644 index 0000000000000..8af5320a464a8 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/VersionUtilities.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This utility class provides utility functions for method versions (which are all strings). Versions are typically + * "x.y" (where X is the major version, and Y is the minor version) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class VersionUtilities { + /** + * Parses a version into a double. + * + * @param version a non-null, non-empty version + * @return a double representing the version or 1.0 if the version cannot be parsed + */ + public static double parse(final String version) { + SonyUtil.validateNotEmpty(version, "version cannot be empty"); + try { + return Double.parseDouble(version); + } catch (final NumberFormatException e) { + return 1.0; + } + } + + /** + * Determines if the specified version is equal to any version in the passed list + * + * @param version a possibly null, possiby empty version (null/empty will always result in a return of false) + * @param otherVersion a list of other versions to compare against + * @return true if the version equals any version in the list or false otherwise + */ + public static boolean equals(final @Nullable String version, final String... otherVersion) { + if (version == null || version.isEmpty()) { + return false; + } + for (final String v : otherVersion) { + if (version.equalsIgnoreCase(v)) { + return true; + } + } + return false; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/GsonUtilities.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/GsonUtilities.java new file mode 100644 index 0000000000000..649f2f1a4ff90 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/GsonUtilities.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.gson; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebEvent; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApi; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApiInfo; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApiVersionInfo; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.annotations.Expose; + +/** + * This utilities class provides standard gson related utility methods + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class GsonUtilities { + /** The default builder */ + private static GsonBuilder defaultBuilder = new GsonBuilder().disableHtmlEscaping() + .addDeserializationExclusionStrategy(new ExposeExclusionStrategy(true)) + .addSerializationExclusionStrategy(new ExposeExclusionStrategy(false)); + + /** The default GSON */ + private static Gson defaultGson = defaultBuilder.create(); + + /** The default API based GSON */ + private static Gson apiGson = defaultBuilder + .registerTypeAdapter(ScalarWebEvent.class, new ScalarWebEventDeserializer()) + .registerTypeAdapter(ScalarWebResult.class, new ScalarWebResultDeserializer()) + .registerTypeAdapter(SupportedApi.class, new SupportedApiDeserializer()) + .registerTypeAdapter(SupportedApiInfo.class, new SupportedApiInfoDeserializer()) + .registerTypeAdapter(SupportedApiVersionInfo.class, new SupportedApiVersionInfoDeserializer()).create(); + + /** + * Creates a generic {@link GsonBuilder} object to use for generic serialization/deserialization + * + * @return a non-null GsonBuilder object + */ + public static GsonBuilder getDefaultGsonBuilder() { + return defaultBuilder; + } + + /** + * Creates a generic {@link Gson} object to use for generic serialization/deserialization + * + * @return a non-null Gson object + */ + public static Gson getDefaultGson() { + return defaultGson; + } + + /** + * Creates a {@link Gson} object suited for API operations and will include a number of custom deserializers + * + * @return a non-null Gson object + */ + public static Gson getApiGson() { + return apiGson; + } + + /** + * Converts the json object into an array based on the element specified + * + * @param jo the non-null json object to convert + * @param elementName the non-null, non-empty element name to use + * @return the array the array returned + */ + public static JsonArray getArray(final JsonObject jo, final String elementName) { + Objects.requireNonNull(jo, "jo cannot be null"); + SonyUtil.validateNotEmpty(elementName, "elementName cannot be empty"); + + final JsonArray ja = new JsonArray(); + + final JsonElement sing = jo.get(elementName); + if (sing != null && sing.isJsonArray()) { + ja.addAll(sing.getAsJsonArray()); + } + + final JsonElement plur = jo.get(elementName + "s"); + if (plur != null && plur.isJsonArray()) { + ja.addAll(plur.getAsJsonArray()); + } + + return ja; + } + + /** + * This class implements an exclusion strategy based on the Expose annotation + */ + private static class ExposeExclusionStrategy implements ExclusionStrategy { + + /** Whether to check deserialization (true) or serialization (false) */ + private final boolean checkDeserialize; + + /** + * Constructs the class for either deserialization or serialization + * + * @param checkDeserialize true to check deserialiation, false to check serialization + */ + private ExposeExclusionStrategy(boolean checkDeserialize) { + this.checkDeserialize = checkDeserialize; + } + + @Override + public boolean shouldSkipClass(@Nullable Class clazz) { + return false; + } + + @Override + public boolean shouldSkipField(@Nullable FieldAttributes field) { + return !(field == null || field.getAnnotation(Expose.class) == null + || (checkDeserialize ? field.getAnnotation(Expose.class).deserialize() + : field.getAnnotation(Expose.class).serialize())); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/ScalarWebEventDeserializer.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/ScalarWebEventDeserializer.java new file mode 100644 index 0000000000000..2ac0cada02b04 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/ScalarWebEventDeserializer.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.gson; + +import java.lang.reflect.Type; +import java.util.Objects; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebEvent; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * This class represents the deserializer to deserialize a json element to a {@link ScalarWebEvent} + * + * @author Tim Roberts - Initial contribution + */ +public class ScalarWebEventDeserializer implements JsonDeserializer { + public ScalarWebEvent deserialize(final @Nullable JsonElement je, final @Nullable Type type, + final @Nullable JsonDeserializationContext context) throws JsonParseException { + Objects.requireNonNull(je, "je cannot be null"); + Objects.requireNonNull(type, "type cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + if (je instanceof JsonObject) { + final JsonObject jo = je.getAsJsonObject(); + + final JsonElement methodElm = jo.get("method"); + final JsonElement versionElm = jo.get("version"); + + return new ScalarWebEvent(methodElm.getAsString(), GsonUtilities.getArray(jo, "params"), + versionElm.getAsString()); + } + throw new JsonParseException("The json element isn't a JsonObject and cannot be deserialized"); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/ScalarWebResultDeserializer.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/ScalarWebResultDeserializer.java new file mode 100644 index 0000000000000..e895401b42378 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/ScalarWebResultDeserializer.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.gson; + +import java.lang.reflect.Type; +import java.util.Objects; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * This class represents the deserializer to deserialize a json element to a {@link ScalarWebResult} + * + * @author Tim Roberts - Initial contribution + */ +public class ScalarWebResultDeserializer implements JsonDeserializer { + public ScalarWebResult deserialize(final @Nullable JsonElement je, final @Nullable Type type, + final @Nullable JsonDeserializationContext context) throws JsonParseException { + Objects.requireNonNull(je, "je cannot be null"); + Objects.requireNonNull(type, "type cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + if (je instanceof JsonObject) { + final JsonObject jo = je.getAsJsonObject(); + + int id = -1; + + final JsonElement idElm = jo.get("id"); + if (idElm != null && idElm.isJsonPrimitive()) { + id = idElm.getAsInt(); + } + + return new ScalarWebResult(id, GsonUtilities.getArray(jo, "result"), GsonUtilities.getArray(jo, "error")); + } + throw new JsonParseException("The json element isn't a JsonObject and cannot be deserialized"); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/SupportedApiDeserializer.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/SupportedApiDeserializer.java new file mode 100644 index 0000000000000..8da6dec487b4e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/SupportedApiDeserializer.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.gson; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApi; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApiInfo; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * This class represents the deserializer to deserialize a json element to a {@link ScalarWebResult} + * + * @author Tim Roberts - Initial contribution + */ +public class SupportedApiDeserializer implements JsonDeserializer { + public SupportedApi deserialize(final @Nullable JsonElement je, final @Nullable Type type, + final @Nullable JsonDeserializationContext context) throws JsonParseException { + Objects.requireNonNull(je, "je cannot be null"); + Objects.requireNonNull(type, "type cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + if (je instanceof JsonObject) { + final JsonObject jo = je.getAsJsonObject(); + + if (!jo.has("service")) { + throw new JsonParseException("service element not found and is required"); + } + + if (!jo.has("protocols")) { + throw new JsonParseException("protocols element not found and is required"); + } + + if (!jo.has("apis")) { + throw new JsonParseException("apis element not found and is required"); + } + + final String service = jo.get("service").getAsString(); + if (service == null || service.isEmpty()) { + throw new JsonParseException("service element was empty and is required"); + } + + final JsonElement protElm = jo.get("protocols"); + if (!protElm.isJsonArray()) { + throw new JsonParseException("protocols element is not an array"); + } + + final Set protocols = new HashSet<>(); + for (final JsonElement elm : protElm.getAsJsonArray()) { + final String proto = elm.getAsString(); + // ignore empty/null elements + if (proto != null && !proto.isEmpty()) { + protocols.add(proto); + } + } + + final JsonElement apisElm = jo.get("apis"); + if (!apisElm.isJsonArray()) { + throw new JsonParseException("apis element is not an array"); + } + + final List apis = new ArrayList<>(); + for (final JsonElement elm : apisElm.getAsJsonArray()) { + apis.add(context.deserialize(elm, SupportedApiInfo.class)); + } + + // notifications are optional + final List notifications = new ArrayList<>(); + if (jo.has("notifications")) { + final JsonElement notElm = jo.get("notifications"); + if (!notElm.isJsonArray()) { + throw new JsonParseException("notifications element is not an array"); + } + + for (final JsonElement elm : notElm.getAsJsonArray()) { + notifications.add(context.deserialize(elm, SupportedApiInfo.class)); + } + } + + return new SupportedApi(service, apis, notifications, protocols); + } + throw new JsonParseException("The json element isn't a JsonObject and cannot be deserialized"); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/SupportedApiInfoDeserializer.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/SupportedApiInfoDeserializer.java new file mode 100644 index 0000000000000..ed0200621d3d1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/SupportedApiInfoDeserializer.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.gson; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApiInfo; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApiVersionInfo; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * This class is responsible for deserializing a {@link SupportedApiInfo} string + * + * @author Tim Roberts - Initial contribution + */ +public class SupportedApiInfoDeserializer implements JsonDeserializer { + public SupportedApiInfo deserialize(final @Nullable JsonElement je, final @Nullable Type type, + final @Nullable JsonDeserializationContext context) throws JsonParseException { + Objects.requireNonNull(je, "je cannot be null"); + Objects.requireNonNull(type, "type cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + if (je instanceof JsonObject) { + final JsonObject jo = je.getAsJsonObject(); + if (!jo.has("name")) { + throw new JsonParseException("name element not found and is required"); + } + + if (!jo.has("versions")) { + throw new JsonParseException("versions element not found and is required"); + } + + final String name = jo.get("name").getAsString(); + if (name == null || name.isEmpty()) { + throw new JsonParseException("name element was empty and is required"); + } + + final JsonElement versElm = jo.get("versions"); + if (!versElm.isJsonArray()) { + throw new JsonParseException("versions element is not an array"); + } + + final List versions = new ArrayList<>(); + for (final JsonElement elm : versElm.getAsJsonArray()) { + versions.add(context.deserialize(elm, SupportedApiVersionInfo.class)); + } + + return new SupportedApiInfo(name, versions); + + } + throw new JsonParseException("The json element isn't a JsonObject and cannot be deserialized"); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/SupportedApiVersionInfoDeserializer.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/SupportedApiVersionInfoDeserializer.java new file mode 100644 index 0000000000000..1154c6625fcb0 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/gson/SupportedApiVersionInfoDeserializer.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.gson; + +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApiVersionInfo; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * This class is responsible for deserializing a {@link SupportedApiVersionInfo} string + * + * @author Tim Roberts - Initial contribution + */ +public class SupportedApiVersionInfoDeserializer implements JsonDeserializer { + public SupportedApiVersionInfo deserialize(final @Nullable JsonElement je, final @Nullable Type type, + final @Nullable JsonDeserializationContext context) throws JsonParseException { + Objects.requireNonNull(je, "je cannot be null"); + Objects.requireNonNull(type, "type cannot be null"); + Objects.requireNonNull(context, "context cannot be null"); + + if (je instanceof JsonObject) { + final JsonObject jo = je.getAsJsonObject(); + if (!jo.has("version")) { + throw new JsonParseException("version element not found and is required"); + } + + // authlevel is optional + final String authLevel = jo.has("authLevel") ? jo.get("authLevel").getAsString() : null; + + // protocols is optional + final Set protocols = new HashSet<>(); + final JsonElement protElm = jo.get("protocols"); + if (jo.has("protocols")) { + if (!protElm.isJsonArray()) { + throw new JsonParseException("protocols element is not an array"); + } + + for (final JsonElement elm : protElm.getAsJsonArray()) { + final String proto = elm.getAsString(); + // ignore empty/null elements + if (proto != null && proto.isEmpty()) { + protocols.add(proto); + } + } + } + + final String version = jo.get("version").getAsString(); + if (version == null || version.isEmpty()) { + throw new JsonParseException("version element is empty and is required"); + } + + return new SupportedApiVersionInfo(authLevel == null ? "" : authLevel, protocols, version); + } + throw new JsonParseException("The json element isn't a JsonObject and cannot be deserialized"); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/AbstractScalarResponse.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/AbstractScalarResponse.java new file mode 100644 index 0000000000000..91702c62220fb --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/AbstractScalarResponse.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.gson.GsonUtilities; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * This abstract class provides common functionality for all scalar responses. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractScalarResponse { + /** + * An abstract method to allow the caller to get the payload of the response + * + * @return a non-null json array + */ + protected abstract @Nullable JsonArray getPayload(); + + /** + * Converts this generic response into the specified type + * + * @param the generic type that will be returned + * @param clazz the class to cast to + * @return the object cast to class + * @throws IOException Signals that an I/O exception has occurred. + */ + public T as(final Class clazz) throws IOException { + Objects.requireNonNull(clazz, "clazz cannot be null"); + + // First see if there is a constructor that takes a ScalarWebResult (us) + // If so - call it with us + // Otherwise try to use GSON to construct the class and set the fields + try { + final Constructor constr = clazz.getConstructor(this.getClass()); + return constr.newInstance(this); + } catch (final NoSuchMethodException e) { + final JsonArray localResults = getPayload(); + if (localResults == null || isBlank(localResults)) { + throw new IllegalArgumentException( + "Cannot convert ScalarWebResult for " + clazz + " with results: " + localResults); + } else if (localResults.size() == 1) { + JsonElement elm = localResults.get(0); + if (elm.isJsonArray()) { + final JsonArray arry = elm.getAsJsonArray(); + if (arry.size() == 1) { + elm = arry.get(0); + } else { + elm = arry; + } + } + final Gson gson = GsonUtilities.getApiGson(); + + if (elm.isJsonObject()) { + final JsonObject jobj = elm.getAsJsonObject(); + return gson.fromJson(jobj, clazz); + } else { + if (SonyUtil.isPrimitive(clazz)) { + return gson.fromJson(elm, clazz); + } else { + throw new IllegalArgumentException( + "Cannot convert ScalarWebResult to " + clazz + " with results: " + localResults, e); + } + } + } + throw new IllegalArgumentException( + "Cannot convert ScalarWebResult to " + clazz + " with results: " + localResults, e); + + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException( + "Cannot convert ScalarWebResult to " + clazz + " for reason: " + e.getMessage(), e); + } + } + + /** + * Converts this generic response into an array of the specified type + * + * @param the generic type that will be returned + * @param clazz the class to cast to + * @return a non-null, possibly empty list of objects converted to the class + * @throws IOException Signals that an I/O exception has occurred. + */ + public List asArray(final Class clazz) throws IOException { + Objects.requireNonNull(clazz, "clazz cannot be null"); + + final JsonArray localResults = getPayload(); + if (localResults == null) { // empty array is okay here - just not null + throw new IllegalArgumentException( + "Cannot convert ScalarWebResult for " + clazz + " with results: " + localResults); + } + + final Gson gson = GsonUtilities.getDefaultGson(); + final List rc = new ArrayList(); + + for (final JsonElement resElm : localResults) { + if (resElm.isJsonArray()) { + for (final JsonElement elm : resElm.getAsJsonArray()) { + rc.add(getObject(gson, elm, clazz)); + } + } else { + rc.add(getObject(gson, resElm, clazz)); + } + } + return rc; + } + + /** + * Helper method to convert an json element to an object + * + * @param gson a non-null GSON instance + * @param elm a non-null element to convert + * @param clazz a non-null class to convert to + * @return a non-null object + * @throws IllegalArgumentException if class cannot be converted + */ + private static T getObject(Gson gson, JsonElement elm, Class clazz) { + Objects.requireNonNull(gson, "gson cannot be null"); + Objects.requireNonNull(elm, "elm cannot be null"); + Objects.requireNonNull(clazz, "clazz cannot be null"); + + if (elm.isJsonObject()) { + final JsonObject jobj = elm.getAsJsonObject(); + return gson.fromJson(jobj, clazz); + } else { + if (SonyUtil.isPrimitive(clazz)) { + return gson.fromJson(elm, clazz); + } else { + throw new IllegalArgumentException( + "Cannot convert ScalarWebResult to " + clazz + " with results: " + elm); + } + } + } + + /** + * Utility method to check if the associated array is empty. This will include if the array consists of ONLY other + * arrays and those arrays are empty + * + * @param arry the json array to check + * @return true if empty or null, false otherwise + */ + protected static boolean isBlank(final @Nullable JsonArray arry) { + if (arry == null || arry.size() == 0) { + return true; + } + for (final JsonElement elm : arry) { + if (elm.isJsonArray()) { + if (!isBlank(elm.getAsJsonArray())) { + return false; + } + } else { + return false; + } + } + return true; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebError.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebError.java new file mode 100644 index 0000000000000..ad8f68959cb62 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebError.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class provides various constants for scalar web errors + * + * https://developer.sony.com/develop/audio-control-api/api-references/error-codes + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebError { + // Common result error codes + public static final int UNKNOWN = -1; + public static final int HTTPERROR = -2; + public static final int NONE = 0; + + public static final int ANY = 1; + public static final int TIMEOUT = 2; + public static final int ILLEGALARGUMENT = 3; + public static final int ILLEGALREQUEST = 5; + public static final int ILLEGALSTATE = 7; // such as pip status when not in pip + public static final int NOTIMPLEMENTED = 12; + public static final int UNSUPPORTEDVERSION = 14; + public static final int UNSUPPORTEDOPERATION = 15; + public static final int FORBIDDEN = 403; + public static final int FAILEDTOLAUNCH = 41401; + + public static final int REQUESTRETRY = 40000; + public static final int CLIENTOVERMAXIMUM = 40001; + public static final int ENCRYPTIONFAILED = 40002; + public static final int REQUESTDUPLICATED = 40003; + public static final int MULTIPLESETTINGSFAILED = 40004; + public static final int DISPLAYISOFF = 40005; + + // System service specific + public static final int PASSWORDEXPIRED = 40200; + public static final int ACPOWERREQUIRED = 40201; + + // Audio service specific + public static final int TARGETNOTSUPPORTED = 40800; + public static final int VOLUMEOUTOFRANGE = 40801; + + // AV Content service specific + public static final int CONTENTISPROTECTED = 41000; + public static final int CONTENTDOESNTEXIST = 41001; + public static final int STORAGEHASNOTCONTENT = 41002; + public static final int SOMECONTENTCOULDNTBEDELETED = 41003; + public static final int CHANNELFIXEDBYUSBRECORDING = 41011; + public static final int CHANNELFIXEDBYSCARTRECORDING = 41012; + public static final int CHAPTERDOESNTEXIST = 41013; + public static final int CHANNELCANTBEUNIQUELYDETERMINED = 41014; + public static final int EMPTYCHANNELLIST = 41015; + public static final int STORAGEDOESNTEXIST = 41020; + public static final int STORAGEISFULL = 41021; + public static final int CONTENTATTRIBUTESETTINGFAILED = 41022; + public static final int UNKNOWNGROUPID = 41023; + public static final int CONTENTISNOTSUPPORTED = 41024; +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebEvent.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebEvent.java new file mode 100644 index 0000000000000..8f49729cf25f2 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebEvent.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.google.gson.JsonArray; + +/** + * This class represents a web scalar event result (sent to us from the device). This result will be created by the + * {@link org.openhab.binding.sony.internal.scalarweb.gson.ScalarWebEventDeserializer} when deserializing the event. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebEvent extends AbstractScalarResponse { + // audio notifications + public static final String NOTIFYVOLUMEINFORMATION = "notifyVolumeInformation"; + public static final String NOTIFYWIRELESSSURROUNDINFO = "notifyWirelessSurroundInfo"; + + // AV Notifications + public static final String NOTIFYPLAYINGCONTENTINFO = "notifyPlayingContentInfo"; + public static final String NOTIFYEXTERNALTERMINALSTATUS = "notifyExternalTerminalStatus"; + public static final String NOTIFYAVAILABLEPLAYBACKFUNCTION = "notifyAvailablePlaybackFunction"; + + // system notifications + public static final String NOTIFYPOWERSTATUS = "notifyPowerStatus"; + public static final String NOTIFYSTORAGESTATUS = "notifyStorageStatus"; + public static final String NOTIFYSWUPDATEINFO = "notifySWUpdateInfo"; + public static final String NOTIFYSETTINGSUPDATE = "notifySettingsUpdate"; + + /** The method name for the event */ + private @Nullable String method; + + /** The parameters for the event */ + private @Nullable JsonArray params; + + /** The event version */ + private @Nullable String version; + + /** + * Empty constructor used for deserialization + */ + public ScalarWebEvent() { + } + + /** + * Instantiates a new scalar web event + * + * @param method the non-null, non-empty method name + * @param params the non-null, possibly empty parameters + * @param version the non-null, non-empty version + */ + public ScalarWebEvent(final String method, final JsonArray params, final String version) { + SonyUtil.validateNotEmpty(method, "method cannot be empty"); + Objects.requireNonNull(params, "params cannot be null"); + SonyUtil.validateNotEmpty(version, "version cannot be empty"); + + this.method = method; + this.params = params; + this.version = version; + } + + /** + * Gets the method name + * + * @return the method name + */ + public @Nullable String getMethod() { + return method; + } + + /** + * Gets the version + * + * @return the version + */ + public @Nullable String getVersion() { + return version; + } + + @Override + protected @Nullable JsonArray getPayload() { + return params; + } + + @Override + public String toString() { + return "ScalarWebEvent [method=" + method + ", params=" + params + ", version=" + version + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebMethod.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebMethod.java new file mode 100644 index 0000000000000..a607602c840c5 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebMethod.java @@ -0,0 +1,311 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents a web scalar method definition that can be called + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebMethod { + + // The following are various method names that can be used + + // Supported by all services + public static final String GETVERSIONS = "getVersions"; + public static final String GETMETHODTYPES = "getMethodTypes"; + + // Only AccessControl service + public static final String ACTREGISTER = "actRegister"; + + // Only AppControl service + public static final String GETAPPLICATIONLIST = "getApplicationList"; + public static final String GETAPPLICATIONSTATUSLIST = "getApplicationStatusList"; + public static final String GETTEXTFORM = "getTextForm"; + public static final String GETWEBAPPSTATUS = "getWebAppStatus"; + public static final String SETACTIVEAPP = "setActiveApp"; + public static final String SETTEXTFORM = "setTextForm"; + public static final String TERMINATEAPPS = "terminateApps"; + + // Only Audio service + public static final String GETCUSTOMEQUALIZERSETTINGS = "getCustomEqualizerSettings"; + public static final String GETSOUNDSETTINGS = "getSoundSettings"; + public static final String GETSPEAKERSETTINGS = "getSpeakerSettings"; + public static final String GETVOLUMEINFORMATION = "getVolumeInformation"; + public static final String SETAUDIOMUTE = "setAudioMute"; + public static final String SETAUDIOVOLUME = "setAudioVolume"; + public static final String SETCUSTOMEQUALIZERSETTINGS = "setCustomEqualizerSettings"; + public static final String SETSOUNDSETTINGS = "setSoundSettings"; + public static final String SETSPEAKERSETTINGS = "setSpeakerSettings"; + + // Only AvContent service + public static final String DELETECOUNT = "deleteContent"; + public static final String GETBLUETOOTHSETTINGS = "getBluetoothSettings"; + public static final String GETCONTENTCOUNT = "getContentCount"; + public static final String GETCONTENTLIST = "getContentList"; + public static final String GETCURRENTEXTERNALINPUTSSTATUS = "getCurrentExternalInputsStatus"; + public static final String GETCURRENTEXTERNALTERMINALSSTATUS = "getCurrentExternalTerminalsStatus"; + public static final String GETFAVORITELIST = "getFavoriteList"; // TODO - figure out + public static final String GETPARENTALRATINGSETTINGS = "getParentalRatingSettings"; + public static final String GETPLAYBACKMODESETTINGS = "getPlaybackModeSettings"; + public static final String GETPLAYINGCONTENTINFO = "getPlayingContentInfo"; + public static final String GETSCHEMELIST = "getSchemeList"; + public static final String GETSOURCELIST = "getSourceList"; + public static final String PAUSEPLAYINGCONTENT = "pausePlayingContent"; + public static final String PRESETBROADCASTSTATION = "presetBroadcastStation"; + public static final String SCANPLAYINGCONTENT = "scanPlayingContent"; + public static final String SEEKBROADCASTSTATION = "seekBroadcastStation"; + public static final String SETACTIVETERMINAL = "setActiveTerminal"; + public static final String SETBLUETOOTHSETTINGS = "setBluetoothSettings"; + public static final String SETDELETEPROTECTION = "setDeleteProtection"; + public static final String SETFAVORITECONTENTLIST = "setFavoriteContentList";// TODO - figure out + public static final String SETPLAYBACKMODESETTINGS = "setPlaybackModeSettings"; + public static final String SETPLAYCONTENT = "setPlayContent"; + public static final String SETPLAYNEXTCONTENT = "setPlayNextContent"; + public static final String SETPLAYPREVIOUSCONTENT = "setPlayPreviousContent"; + public static final String SETTVCONTENTVISIBILITY = "setTvContentVisibility"; + public static final String STOPPLAYINGCONTENT = "stopPlayingContent"; + + // Only CEC service + public static final String SETCECCONTROLMODE = "setCecControlMode"; + public static final String SETMHLAUTOINPUTCHANGEMODE = "setMhlAutoInputChangeMode"; + public static final String SETMHLPOWERFEEDMODE = "setMhlPowerFeedMode"; + public static final String SETPOWERSYNCMODE = "setPowerSyncMode"; + + // Only Encryption service + public static final String GETPUBLICKEY = "getPublicKey"; + + // Only Browser service + public static final String ACTIVATEBROWSERCONTROL = "actBrowserControl"; + public static final String GETBROWSERBOOKMARKLIST = "getBrowserBookmarkList"; + public static final String GETTEXTURL = "getTextUrl"; + public static final String SETTEXTURL = "setTextUrl"; + + // Only Illumination service + public static final String GETILLUMNATIONSETTING = "getIlluminationSettings"; + public static final String SETILLUMNATIONSETTING = "setIlluminationSettings"; + + // Only Guide service + public static final String GETSERVICEPROTOCOLS = "getServiceProtocols"; + public static final String GETSUPPORTEDAPIINFO = "getSupportedApiInfo"; + + // Only System service + public static final String GETCURRENTTIME = "getCurrentTime"; + public static final String GETDEVICEMISCSETTINGS = "getDeviceMiscSettings"; + public static final String GETDEVICEMODE = "getDeviceMode"; + public static final String GETINTERFACEINFORMATION = "getInterfaceInformation"; + public static final String GETLEDINDICATORSTATUS = "getLEDIndicatorStatus"; + public static final String GETNETWORKSETTINGS = "getNetworkSettings"; + public static final String GETPOSTALCODE = "getPostalCode"; + public static final String GETPOWERSAVINGMODE = "getPowerSavingMode"; + public static final String GETPOWERSETTINGS = "getPowerSettings"; + public static final String GETPOWERSTATUS = "getPowerStatus"; + public static final String GETREMOTECONTROLLERINFO = "getRemoteControllerInfo"; + public static final String GETREMOTEDEVICESETTINGS = "getRemoteDeviceSettings"; + public static final String GETSLEEPTIMERSETTINGS = "getSleepTimerSettings"; + public static final String GETSTORAGELIST = "getStorageList"; // TODO- figure out + public static final String GETSYSTEMINFORMATION = "getSystemInformation"; + public static final String GETSYSTEMSUPPORTEDFUNCTION = "getSystemSupportedFunction"; // TODO- figure out + public static final String GETWOLMODE = "getWolMode"; + public static final String GETWUTANGINFO = "getWuTangInfo"; + public static final String MOUNTSTORAGE = "mountStorage"; // TODO- figure out + public static final String REQUESTREBOOT = "requestReboot"; + public static final String SETDEVICEMISSETTINGS = "setDeviceMiscSettings"; + public static final String SETDEVICEMODE = "setDeviceMode"; + public static final String SETLANGUAGE = "setLanguage"; + public static final String SETLEDINDICATORSTATUS = "setLEDIndicatorStatus"; + public static final String SETPOSTALCODE = "setPostalCode"; + public static final String SETPOWERSAVINGMODE = "setPowerSavingMode"; + public static final String SETPOWERSETTINGS = "setPowerSettings"; + public static final String SETPOWERSTATUS = "setPowerStatus"; + public static final String SETSLEEPTIMERSETTINGS = "setSleepTimerSettings"; + public static final String SETWOLMODE = "setWolMode"; + public static final String SETWUTANGINFO = "setWuTangInfo"; + public static final String SWITCHNOTIFICATIONS = "switchNotifications"; + + // Only Video Screen service + public static final String GETAUDIOSOURCESCREEN = "getAudioSourceScreen"; + public static final String GETBANNERMODE = "getBannerMode"; + public static final String GETMULTISCREENMODE = "getMultiScreenMode"; + public static final String GETPICTUREQUALITYSETTINGS = "getPictureQualitySettings"; + public static final String GETPIPSUBSCREENPOSITION = "getPipSubScreenPosition"; + public static final String GETSCENESETTING = "getSceneSetting"; + public static final String SETAUDIOSOURCESCREEN = "setAudioSourceScreen"; + public static final String SETBANNERMODE = "setBannerMode"; + public static final String SETMULTISCREENMODE = "setMultiScreenMode"; + public static final String SETPICTUREQUALITYSETTINGS = "setPictureQualitySettings"; + public static final String SETPIPSUBSCREENPOSITION = "setPipSubScreenPosition"; + public static final String SETSCENESETTING = "setSceneSetting"; + + // Various versions + public static final String V1_0 = "1.0"; + public static final String V1_1 = "1.1"; + public static final String V1_2 = "1.2"; + public static final String V1_3 = "1.3"; + public static final String V1_4 = "1.4"; + public static final String V1_5 = "1.5"; + + public static final int UNKNOWN_VARIATION = -1; + + /** The method name */ + private final String methodName; + + /** The parameters for the method (unmodifiable) */ + private final List parms; + + /** The return values for the method (unmodifiable) */ + private final List retVals; + + /** The method version */ + private final String version; + + /** The method version */ + private final int variation; + + // The comparator to compare scalar web methods + public static final Comparator COMPARATOR = Comparator + .comparing((final ScalarWebMethod e) -> e.getMethodName()).thenComparing(e -> e.getVersion()); + + /** + * Instantiates a new scalar web method base on the parameters + * + * @param methodName the non-null, non-empty method name + * @param parms the non-null, possibly empty parameter names + * @param retVals the non-null, possibly empty return values + * @param version the non-null, non-empty method version + */ + public ScalarWebMethod(final String methodName, final List parms, final List retVals, + final String version) { + this(methodName, parms, retVals, version, 0); + } + + /** + * Instantiates a new scalar web method base on the parameters + * + * @param methodName the non-null, non-empty method name + * @param parms the non-null, possibly empty parameter names + * @param retVals the non-null, possibly empty return values + * @param version the non-null, non-empty method version + * @param variation the variation of the method + */ + public ScalarWebMethod(final String methodName, final List parms, final List retVals, + final String version, final int variation) { + SonyUtil.validateNotEmpty(methodName, "methodName cannot be empty"); + Objects.requireNonNull(parms, "parms cannot be null"); + Objects.requireNonNull(retVals, "retVals cannot be null"); + SonyUtil.validateNotEmpty(version, "getVolumeInformationersion cannot be empty"); + + this.methodName = methodName; + this.parms = Collections.unmodifiableList(parms.stream().map(s -> s.trim()).collect(Collectors.toList())); + this.retVals = Collections.unmodifiableList(retVals.stream().map(s -> s.trim()).collect(Collectors.toList())); + this.version = version; + this.variation = variation; + } + + /** + * Gets the method name + * + * @return the method name + */ + public String getMethodName() { + return methodName; + } + + /** + * Gets the method parameters + * + * @return the non-null, possibly empty (unmodifiable) list of parameters + */ + public List getParms() { + return parms; + } + + /** + * Gets the return values + * + * @return the non-null, possibly empty (unmodifiable) list of return values + */ + public List getRetVals() { + return retVals; + } + + /** + * Gets the method version + * + * @return the method version + */ + public String getVersion() { + return version; + } + + /** + * Gets the method version variation + * + * @return the method version variation + */ + public int getVariation() { + return variation; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(100); + + sb.append(getMethodName()); + sb.append("["); + sb.append(getVersion()); + if (variation > 0) { + sb.append("-"); + sb.append(getVariation()); + } + sb.append("]("); + sb.append(String.join(",", parms)); + sb.append("): "); + sb.append(String.join(",", retVals)); + + return sb.toString(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + + if (obj == null) { + return false; + } + + if (!(obj instanceof ScalarWebMethod)) { + return false; + } + + final ScalarWebMethod other = (ScalarWebMethod) obj; + return methodName.equalsIgnoreCase(other.methodName) && version.equalsIgnoreCase(other.version) + && SonyUtil.equalsIgnoreCase(new HashSet<>(parms), new HashSet<>(other.parms)) + && SonyUtil.equalsIgnoreCase(new HashSet<>(retVals), new HashSet<>(other.retVals)) + && variation == ((ScalarWebMethod) obj).variation; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebRequest.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebRequest.java new file mode 100644 index 0000000000000..2263721d39db4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebRequest.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents a web scalar method request + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebRequest { + /** The unique identifier of the request */ + private final int id; + + /** The method request number */ + private final String method; + + /** The method version */ + private final String version; + + /** The parameters for the request */ + private final Object[] params; + + /** + * An incrementing integer for the identifier of this request + * Note: start at 100 to allow utility method ids (like SonyServlet) + */ + private static final AtomicInteger REQUESTID = new AtomicInteger(100); + + /** + * Instantiates a new scalar web request with no parameters + * + * @param method the non-null, non-empty method name + * @param version the non-null, non-empty method version + */ + public ScalarWebRequest(final String method, final String version) { + this(method, version, new Object[0]); + } + + /** + * Instantiates a new scalar web request with parameters + * + * @param method the non-null, non-empty method name + * @param version the non-null, non-empty method version + * @param params the non-null, possibly empty list of parameters + */ + public ScalarWebRequest(final String method, final String version, final Object... params) { + SonyUtil.validateNotEmpty(method, "method cannot be empty"); + SonyUtil.validateNotEmpty(version, "version cannot be empty"); + Objects.requireNonNull(params, "params cannot be null"); + + this.id = REQUESTID.incrementAndGet(); + this.method = method; + this.version = version; + this.params = params; + } + + /** + * Gets the unique request identifier + * + * @return the unique request identifier + */ + public int getId() { + return id; + } + + /** + * Gets the method name + * + * @return the method name + */ + public String getMethod() { + return method; + } + + /** + * Gets the method version + * + * @return the method version + */ + public String getVersion() { + return version; + } + + /** + * Gets the parameters + * + * @return the non-null, possibly empty array of parameters + */ + public Object[] getParams() { + return params; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebResult.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebResult.java new file mode 100644 index 0000000000000..94f8156ff85be --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebResult.java @@ -0,0 +1,268 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.net.HttpResponse; +import org.openhab.binding.sony.internal.scalarweb.gson.ScalarWebResultDeserializer; + +import com.google.gson.JsonArray; +import com.google.gson.JsonPrimitive; +import com.google.gson.annotations.SerializedName; + +/** + * This class represents a web scalar method result (to a request). This result will be created either by the + * {@link ScalarWebResultDeserializer} when deserializing results or directly with an HttpResponse if an error occurred. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebResult extends AbstractScalarResponse { + /** The unique request identifier */ + private @Nullable Integer id; + + /** The related httpResponse */ + private @Nullable HttpResponse httpResponse; + + /** The results of the request */ + @SerializedName(value = "results", alternate = { "result" }) + private @Nullable JsonArray results; + + /** The any errors that occurred */ + @SerializedName(value = "errors", alternate = { "error" }) + private @Nullable JsonArray errors; + + /** + * Empty constructor for deserialization + */ + public ScalarWebResult() { + } + + /** + * Instantiates a new scalar web result from the specified response + * + * @param response a non-null response + */ + public ScalarWebResult(final HttpResponse response) { + Objects.requireNonNull(response, "response cannot be null"); + this.id = -1; + this.httpResponse = response; + this.results = new JsonArray(); + this.errors = new JsonArray(); + + if (response.getHttpCode() != HttpStatus.OK_200) { + this.errors.add(new JsonPrimitive(ScalarWebError.HTTPERROR)); + this.errors.add(new JsonPrimitive(response.getHttpReason())); + } + } + + /** + * Instantiates a new scalar web result from the specified codes + * + * @param httpCode the http code + * @param reason the possibly null, possibly empty reason + * @return the scalar web result + */ + public ScalarWebResult(final int httpCode, final String reason) { + this(new HttpResponse(httpCode, reason)); + } + + /** + * Instantiates a new scalar web result. + * + * @param id the unique request id + * @param results the results (might be null if errors) + * @param errors the errors (might be null if no errors - probably empty however) + */ + public ScalarWebResult(final int id, final JsonArray results, final JsonArray errors) { + Objects.requireNonNull(results, "results cannot be null"); + Objects.requireNonNull(errors, "errors cannot be null"); + + this.id = id; + this.results = results; + this.errors = errors; + + if (errors.size() == 0) { + this.httpResponse = new HttpResponse(HttpStatus.OK_200, "OK"); + } else { + this.httpResponse = new HttpResponse(HttpStatus.INTERNAL_SERVER_ERROR_500, getDeviceErrorDesc()); + } + } + + /** + * Helper method to create a successful empty result + * + * @return a non-null result + */ + public static ScalarWebResult createEmptySuccess() { + return new ScalarWebResult(-1, new JsonArray(), new JsonArray()); + } + + /** + * Helper method to create a NOTIMPLEMENTED result + * + * @param methodName the non-null, non-empty method name not implemented + * @return a non-null result + */ + public static ScalarWebResult createNotImplemented(final String methodName) { + SonyUtil.validateNotEmpty(methodName, "methodName cannot be empty"); + final JsonArray ja = new JsonArray(); + ja.add(ScalarWebError.NOTIMPLEMENTED); + ja.add(methodName + " is not implemented"); + return new ScalarWebResult(-1, new JsonArray(), ja); + } + + /** + * Gets the unique request identifier + * + * @return the unique request identifier + */ + public @Nullable Integer getId() { + return id; + } + + /** + * Gets the results + * + * @return the results (possibly empty) + */ + public @Nullable JsonArray getResults() { + return results; + } + + /** + * Checks if there are results + * + * @return true if results, false otherwise + */ + public boolean hasResults() { + return !isBlank(results); + } + + /** + * Checks if there are any errors + * + * @return true if there are errors, false otherwise + */ + public boolean isError() { + return !isBlank(errors); + } + + /** + * Gets the HTTP response for this request. + * + * @return the non-null http response + */ + public HttpResponse getHttpResponse() { + final HttpResponse localHttpResponse = httpResponse; + + if (localHttpResponse == null) { + if (isBlank(errors)) { + return new HttpResponse(HttpStatus.OK_200, "OK"); + } else { + return new HttpResponse(HttpStatus.INTERNAL_SERVER_ERROR_500, getDeviceErrorDesc()); + } + } else { + return localHttpResponse; + } + } + + /** + * Returns the device error code related to this request (will be NONE if no errors) + * + * @return the device error code + */ + public int getDeviceErrorCode() { + final JsonArray localErrors = errors; + if (isBlank(localErrors)) { + return ScalarWebError.NONE; + } else { + final String rcStr = localErrors.get(0).getAsString(); + try { + return Integer.parseInt(rcStr); + } catch (final NumberFormatException e) { + return ScalarWebError.UNKNOWN; + } + } + } + + /** + * Returns the device error description + * + * @return a non-null, possibly empty (if no errors) error description + */ + public String getDeviceErrorDesc() { + final JsonArray localErrors = errors; + final StringBuilder sb = new StringBuilder(); + if (localErrors != null) { + for (int i = 0; i < localErrors.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(localErrors.get(i).getAsString()); + } + } + return sb.toString(); + } + + @Override + public T as(final Class clazz) throws IOException { + if (isError()) { + throw getHttpResponse().createException(); + } + + return super.as(clazz); + } + + @Override + public List asArray(final Class clazz) throws IOException { + if (isError()) { + throw getHttpResponse().createException(); + } + + return super.asArray(clazz); + } + + @Override + protected @Nullable JsonArray getPayload() { + return results; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + + sb.append("id: "); + sb.append(id); + + final JsonArray localResults = results; + final JsonArray localErrors = errors; + + if (localErrors.size() > 0) { + sb.append(", Error: "); + sb.append(localErrors == null ? "(null)" : localErrors.toString()); + } else { + sb.append(", Results: "); + sb.append(localResults == null ? "(null)" : localResults.toString()); + } + + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebService.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebService.java new file mode 100644 index 0000000000000..b16fadd58acaf --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/ScalarWebService.java @@ -0,0 +1,400 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.models.api.MethodTypes; +import org.openhab.binding.sony.internal.scalarweb.models.api.ServiceProtocol; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApi; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApiInfo; +import org.openhab.binding.sony.internal.scalarweb.models.api.SupportedApiVersionInfo; +import org.openhab.binding.sony.internal.transports.SonyTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.openhab.binding.sony.internal.transports.TransportOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class represents the different web services available + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScalarWebService implements AutoCloseable { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebService.class); + + // The various well know service names (must be unique as they are channel groups) + public static final String ACCESSCONTROL = "accessControl"; + public static final String APPCONTROL = "appControl"; + public static final String AUDIO = "audio"; + public static final String AVCONTENT = "avContent"; + public static final String BROWSER = "browser"; + public static final String CEC = "cec"; + public static final String CONTENTSHARE = "contentshare"; + public static final String ENCRYPTION = "encryption"; + public static final String GUIDE = "guide"; + public static final String ILLUMINATION = "illumination"; + public static final String SYSTEM = "system"; + public static final String VIDEO = "video"; + public static final String VIDEOSCREEN = "videoScreen"; + + // These are (undocumented) services that I haven't figured out yet + public static final String NOTIFICATION = "notification"; + public static final String RECORDING = "recording"; + + // The various know services to their labels + private static final Map SERVICELABELS = Collections.unmodifiableMap(new HashMap() { + private static final long serialVersionUID = 5934100497468165317L; + { + put(ACCESSCONTROL, labelFor(ACCESSCONTROL)); + put(APPCONTROL, labelFor(APPCONTROL)); + put(AUDIO, labelFor(AUDIO)); + put(AVCONTENT, labelFor(AVCONTENT)); + put(BROWSER, labelFor(BROWSER)); + put(CEC, labelFor(CEC)); + put(CONTENTSHARE, labelFor(CONTENTSHARE)); + put(ENCRYPTION, labelFor(ENCRYPTION)); + put(GUIDE, labelFor(GUIDE)); + put(ILLUMINATION, labelFor(ILLUMINATION)); + put(SYSTEM, labelFor(SYSTEM)); + put(VIDEO, labelFor(VIDEO)); + put(VIDEOSCREEN, labelFor(VIDEOSCREEN)); + } + }); + + /** The service name */ + private final String serviceName; + + /** The service version */ + private final String version; + + /** Transport Factory */ + private final SonyTransportFactory transportFactory; + + /** Transport used for communication */ + private final SonyTransport transport; + + /** The API supported by this service */ + private final SupportedApi supportedApi; + + /** + * Instantiates a new scalar web service. + * + * @param transportFactory the non-null transport factory to use + * @param serviceProtocol the non-null service protocol to use + * @param version the non-null, non-empty service version + * @param supportedApi the non-null supported api + */ + public ScalarWebService(final SonyTransportFactory transportFactory, final ServiceProtocol serviceProtocol, + final String version, final SupportedApi supportedApi) { + Objects.requireNonNull(transportFactory, "transportFactory cannot be null"); + Objects.requireNonNull(serviceProtocol, "serviceProtocol cannot be null"); + SonyUtil.validateNotEmpty(version, "version cannot be empty"); + Objects.requireNonNull(supportedApi, "supportedApi cannot be null"); + + this.transportFactory = transportFactory; + this.serviceName = serviceProtocol.getServiceName(); + this.version = version; + this.supportedApi = supportedApi; + + final SonyTransport transport = transportFactory.getSonyTransport(serviceProtocol); + if (transport == null) { + throw new IllegalArgumentException("No transport found for " + serviceProtocol); + } + this.transport = transport; + } + + /** + * Retrieves the methods for this service + * + * @return a non-null, possibly empty list of {@link ScalarWebMethod} + */ + public List getMethods() { + final Set versions = new HashSet<>(); + versions.add(ScalarWebMethod.V1_0); + + final List methods = new ArrayList<>(); + try { + // Retrieve the api versions for the service + versions.addAll(execute(new ScalarWebRequest(ScalarWebMethod.GETVERSIONS, version)).asArray(String.class)); + } catch (final IOException e) { + if (e.getMessage() != null && e.getMessage().contains(String.valueOf(HttpStatus.NOT_FOUND_404))) { + logger.debug("Could not retrieve method versions - missing method {}: {}", ScalarWebMethod.GETVERSIONS, + e.getMessage()); + } else { + logger.debug("Could not retrieve methods versions: {}", e.getMessage(), e); + } + } + + // For each version, retrieve the methods for the service + for (final String apiVersion : versions) { + try { + final MethodTypes mtdResults = execute( + new ScalarWebRequest(ScalarWebMethod.GETMETHODTYPES, version, apiVersion)) + .as(MethodTypes.class); + methods.addAll(mtdResults.getMethods()); + } catch (final IOException e) { + logger.debug("Could not retrieve {} vers {}: {}", ScalarWebMethod.GETMETHODTYPES, apiVersion, + e.getMessage()); + } + } + + // Merge in any methods reported that weren't returned by getmethodtypes/version + supportedApi.getApis().forEach(api -> { + api.getVersions().forEach(v -> { + if (!methods.stream().anyMatch(m -> m.getMethodName().equalsIgnoreCase(api.getName()) + && v.getVersion().equalsIgnoreCase(m.getVersion()))) { + methods.add(new ScalarWebMethod(api.getName(), new ArrayList<>(), new ArrayList<>(), v.getVersion(), + ScalarWebMethod.UNKNOWN_VARIATION)); + } + }); + }); + return methods; + } + + /** + * Gets the list of notifications for the service + * + * @return a non-null, possibly empty list of {@link ScalarWebMethod} + */ + public List getNotifications() { + final List notifications = new ArrayList<>(); + // add in any supported api that has no match above (shouldn't really be any but we are being complete) + // don't use unknown variant since that will prevent change detection in github implementation + supportedApi.getNotifications().forEach(api -> { + api.getVersions().forEach(v -> { + notifications.add( + new ScalarWebMethod(api.getName(), new ArrayList<>(), new ArrayList<>(), v.getVersion(), 0)); + }); + }); + return notifications; + } + + /** + * Gets the latest version for method name + * + * @param methodName the non-null, non-empty method name + * @return the latest version or null if not found + */ + public @Nullable String getVersion(final String methodName) { + SonyUtil.validateNotEmpty(methodName, "methodName cannto be empty"); + final SupportedApiInfo api = supportedApi.getMethod(methodName); + final SupportedApiVersionInfo vers = api == null ? null : api.getLatestVersion(); + return vers == null ? null : vers.getVersion(); + } + + /** + * Gets all the versions for a given method + * + * @param methodName the non-null, non-empty method name + * @return the non-null, possibly empty list of versions + */ + public List getVersions(final String methodName) { + SonyUtil.validateNotEmpty(methodName, "methodName cannto be empty"); + final SupportedApiInfo api = supportedApi.getMethod(methodName); + return api == null ? new ArrayList<>() + : api.getVersions().stream().map(v -> v.getVersion()).collect(Collectors.toList()); + } + + /** + * Determines if the method name exists in the service + * + * @param methodName the non-null, non-empty method name + * @return true if it exists, false otherwise + */ + public boolean hasMethod(final String methodName) { + SonyUtil.validateNotEmpty(methodName, "methodName cannto be empty"); + return supportedApi.getMethod(methodName) != null; + } + + /** + * Gets the service name + * + * @return the service name + */ + public String getServiceName() { + return serviceName; + } + + /** + * Gets the service version + * + * @return the service version + */ + public String getVersion() { + return version; + } + + /** + * Returns the transport related to this service + * + * @return the non-null sony transport + */ + public SonyTransport getTransport() { + return transport; + } + + /** + * Executes the latest method version using the specified parameters + * + * @param methodName the method name + * @param parms the parameters to use + * @return the scalar web result + */ + public ScalarWebResult execute(final String methodName, final Object... parms) { + SonyUtil.validateNotEmpty(methodName, "methodName cannot be empty"); + return executeSpecific(methodName, null, parms); + } + + /** + * Executes a specific method/version using the specified parameters + * + * @param methodName the method name + * @param version the possibly null, possibly empty version (null/empty to use latest version) + * @param parms the parameters to use + * @return the scalar web result + */ + public ScalarWebResult executeSpecific(final String methodName, final @Nullable String version, + final Object... parms) { + SonyUtil.validateNotEmpty(methodName, "methodName cannot be empty"); + + if (version == null || version.isEmpty()) { + final String mtdVersion = getVersion(methodName); + if (mtdVersion == null) { + logger.debug("Method {} doesn't exist in the service {}", methodName, serviceName); + return ScalarWebResult.createNotImplemented(methodName); + } + return execute(new ScalarWebRequest(methodName, mtdVersion, parms)); + } else { + return execute(new ScalarWebRequest(methodName, version, parms)); + } + } + + /** + * Execute the specified request with the specified options + * + * @param request the non-null request to execute + * @param options the possibly not specified options to use the execution with + * @return the scalar web result + */ + public ScalarWebResult execute(final ScalarWebRequest request, final TransportOption... options) { + Objects.requireNonNull(request, "request cannot be null"); + + final Set protocols = supportedApi.getProtocols(request.getMethod(), request.getVersion()); + if (protocols.contains(transport.getProtocolType())) { + return transport.execute(request, options); + } else { + final ServiceProtocol serviceProtocol = new ServiceProtocol(serviceName, protocols); + try (final SonyTransport mthdTransport = transportFactory.getSonyTransport(serviceProtocol)) { + if (mthdTransport == null) { + logger.debug("No transport for {} with protocols: {}", request, protocols); + return new ScalarWebResult(HttpStatus.INTERNAL_SERVER_ERROR_500, + "No transport for " + request + " with protocols: " + protocols); + } else { + logger.debug("Execution of {} is using a different protocol {} than the service {}", request, + mthdTransport.getProtocolType(), transport.getProtocolType()); + return mthdTransport.execute(request, options); + } + } + } + } + + /** + * Returns the label for a given service name + * + * @param serviceName a non-null, non-empty service name + * @return the label for the service + */ + private static String labelFor(final String serviceName) { + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + switch (serviceName) { + case ACCESSCONTROL: + return "Access Control"; + case APPCONTROL: + return "Application Control"; + case AUDIO: + return "Audio"; + case AVCONTENT: + return "A/V Content"; + case BROWSER: + return "Browser"; + case CEC: + return "CEC"; + case CONTENTSHARE: + return "Content Share"; + case ENCRYPTION: + return "Encryption"; + case GUIDE: + return "Guide"; + case ILLUMINATION: + return "Illumination"; + case SYSTEM: + return "System"; + case VIDEO: + return "Video"; + case VIDEOSCREEN: + return "Video Screen"; + default: + return serviceName; + } + } + + /** + * Returns a map of service names to their label + * + * @return a non-null, non-empty map of service names to service labels + */ + public static Map getServiceLabels() { + return SERVICELABELS; + } + + @Override + public void close() { + transport.close(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(100); + final String newLine = java.lang.System.lineSeparator(); + + sb.append("Service: "); + sb.append(serviceName); + sb.append(newLine); + + for (final ScalarWebMethod mthd : getMethods().stream() + .sorted(Comparator.comparing(ScalarWebMethod::getMethodName)).collect(Collectors.toList())) { + sb.append(" "); + sb.append(mthd); + sb.append(newLine); + } + + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActRegisterId.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActRegisterId.java new file mode 100644 index 0000000000000..d54eb2051c5f8 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActRegisterId.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.net.NetUtil; + +/** + * The class represents the active registration and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ActRegisterId { + + /** The clientid for registration */ + private final String clientid = NetUtil.getDeviceId(); + + /** The nickname for registration */ + private final String nickname = NetUtil.getDeviceName(); + + /** + * Gets the clientid for registration + * + * @return the clientid for registration + */ + public String getClientid() { + return clientid; + } + + /** + * Gets the nickname for registration + * + * @return the nickname for registration + */ + public String getNickname() { + return nickname; + } + + @Override + public String toString() { + return "ActRegisterId [clientid=" + clientid + ", nickname=" + nickname + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActRegisterOptions.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActRegisterOptions.java new file mode 100644 index 0000000000000..060357793f5a9 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActRegisterOptions.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the registration options and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ActRegisterOptions { + + /** The registration value */ + private final String value = "yes"; + + /** The registration function */ + private final String function = "WOL"; + + /** + * Gets the value of the registration option + * + * @return the value of the registration option + */ + public String getValue() { + return value; + } + + /** + * Gets the function of the registration option + * + * @return the function of the registration option + */ + public String getFunction() { + return function; + } + + @Override + public String toString() { + return "ActRegisterOptions [value=" + value + ", function=" + function + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActivateSoftwareUpdate.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActivateSoftwareUpdate.java new file mode 100644 index 0000000000000..4a80e723e2355 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActivateSoftwareUpdate.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a request to activate a software update + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ActivateSoftwareUpdate { + /** + * Constructor used for serialization/deserialization + */ + public ActivateSoftwareUpdate() { + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActiveApp.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActiveApp.java new file mode 100644 index 0000000000000..c72ce26b9a7dd --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActiveApp.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the activate application and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ActiveApp { + + /** The uri of the active application */ + private final @Nullable String uri; + + /** The extra data of the active application */ + private final @Nullable String data; + + /** + * Instantiates a new active application + * + * @param uri the non-null, non-empty uri + * @param data the possibly null, possibly empty data + */ + public ActiveApp(final String uri, final @Nullable String data) { + SonyUtil.validateNotEmpty(uri, "uri cannot be empty"); + this.uri = uri; + this.data = data; + } + + /** + * Gets the uri of the active application + * + * @return the uri of the active application + */ + public @Nullable String getUri() { + return uri; + } + + /** + * Gets the optional data for the active application + * + * @return the optional data for the active application + */ + public @Nullable String getData() { + return data; + } + + @Override + public String toString() { + return "ActiveApp [uri=" + uri + ", data=" + data + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActiveTerminal.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActiveTerminal.java new file mode 100644 index 0000000000000..fc6d7bc525540 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ActiveTerminal.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * Sets the active terminal (the power status of a zone) and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ActiveTerminal { + /** The active status value */ + public static final String ACTIVE = "active"; + + /** The inactive status value */ + public static final String INACTIVE = "inactive"; + + /** The URI describing the terminal */ + private final String uri; + + /** The status of the terminal */ + private final String active; + + /** + * Constructs the active terminal + * + * @param uri the non-null, non-empty terminal URI + * @param active the non-null, non-empty active sttus + */ + public ActiveTerminal(final String uri, final String active) { + SonyUtil.validateNotEmpty(uri, "uri cannot be empty"); + SonyUtil.validateNotEmpty(active, "active cannot be empty"); + this.uri = uri; + this.active = active; + } + + /** + * Get's the terminal URI + * + * @return a non-null, non-empty URI + */ + public String getUri() { + return uri; + } + + /** + * Get's the terminal active status + * + * @return a non-null, non-empty active status + */ + public String getActive() { + return active; + } + + @Override + public String toString() { + return "ActiveTerminal [uri=" + uri + ", active=" + active + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ApplicationList.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ApplicationList.java new file mode 100644 index 0000000000000..f397c42841c8c --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ApplicationList.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents an application and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ApplicationList { + /** The application title */ + private @Nullable String title; + + /** The application uri */ + private @Nullable String uri; + + /** The application icon */ + private @Nullable String icon; + + /** The application data */ + private @Nullable String data; + + /** + * Constructor used for deserialization only + */ + public ApplicationList() { + } + + /** + * Gets the application title + * + * @return the application title + */ + public @Nullable String getTitle() { + return title; + } + + /** + * Gets the application uri + * + * @return the application uri + */ + public @Nullable String getUri() { + return uri; + } + + /** + * Gets the application icon + * + * @return the application icon + */ + public @Nullable String getIcon() { + return icon; + } + + /** + * Gets the application data + * + * @return the application data + */ + public @Nullable String getData() { + return data; + } + + @Override + public String toString() { + return "ApplicationListItem [title=" + title + ", uri=" + uri + ", icon=" + icon + ", data=" + data + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ApplicationStatusList.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ApplicationStatusList.java new file mode 100644 index 0000000000000..22fd7faaadcb7 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ApplicationStatusList.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the application status and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ApplicationStatusList { + + /** The text input status */ + public static final String TEXTINPUT = "textInput"; + + /** The cursor display status */ + public static final String CURSORDISPLAY = "cursorDisplay"; + + /** The web browser status */ + public static final String WEBBROWSE = "webBrowse"; + + /** The 'on' status */ + public static final String ON = "on"; + + /** The 'off status */ + public static final String OFF = "off"; + + /** The application name */ + private @Nullable String name; + + /** The application status */ + private @Nullable String status; + + /** + * Constructor used for deserialization only + */ + public ApplicationStatusList() { + } + + /** + * Gets the application name + * + * @return the application name + */ + public @Nullable String getName() { + return name; + } + + /** + * Gets the application status + * + * @return the applicatin status + */ + public @Nullable String getStatus() { + return status; + } + + /** + * Checks if the status is ON + * + * @return true, if is on - false otherwise + */ + public boolean isOn() { + return ON.equalsIgnoreCase(status); + } + + @Override + public String toString() { + return "ApplicationStatusList [name=" + name + ", status=" + status + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioInfo.java new file mode 100644 index 0000000000000..574d83621cbdf --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioInfo.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The audio information class used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class AudioInfo { + /** The audio channel */ + private @Nullable String channel; + + /** The audio codec */ + private @Nullable String codec; + + /** The audio frequency */ + private @Nullable String frequency; + + /** + * Constructor used for deserialization only + */ + public AudioInfo() { + } + + /** + * Returns the audio channel + * + * @return the audio channel + */ + public @Nullable String getChannel() { + return channel; + } + + /** + * Returns the audio codec + * + * @return the audio codec + */ + public @Nullable String getCodec() { + return codec; + } + + /** + * Returns the audio frequency + * + * @return the audio frequency + */ + public @Nullable String getFrequency() { + return frequency; + } + + @Override + public String toString() { + return "AudioInfo [channel=" + channel + ", codec=" + codec + ", frequency=" + frequency + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioMute_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioMute_1_0.java new file mode 100644 index 0000000000000..bdf4a277fb1c1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioMute_1_0.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents whether the audio is muted or not and is used for serialzation + * + * Versions: + *
    + *
  1. 1.0: {"status":"bool"}
  2. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class AudioMute_1_0 { + + /** The target of the mute */ + private final String target; + + /** Whether muted or not */ + private final boolean status; + + /** + * Instantiates a new audio mute. + * + * @param target the non-null, possibly empty target + * @param status the status + */ + public AudioMute_1_0(final String target, final boolean status) { + Objects.requireNonNull(target, "target cannot be empty"); + + this.target = target; + this.status = status; + } + + /** + * Checks if is muted + * + * @return true, if muted - false otherwise + */ + public boolean isOn() { + return status; + } + + /** + * Gets the target of the mute + * + * @return the target of the mute + */ + public String getTarget() { + return target; + } + + @Override + public String toString() { + return "AudioMute_1_0 [Target=" + getTarget() + ", Status=" + status + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioMute_1_1.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioMute_1_1.java new file mode 100644 index 0000000000000..c3d378829642a --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioMute_1_1.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents whether the audio is muted or not and is used for serialzation + * + * Versions: + *
    + *
  1. 1.1: {"output":"string", "mute":"string"}
  2. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class AudioMute_1_1 { + + public static final String MUTEON = "on"; + public static final String MUTEOFF = "off"; + + /** The target of the mute */ + private final String output; + + /** Whether muted or not */ + private final String mute; + + /** + * Instantiates a new audio mute. + * + * @param output the non-null, non-empty output + * @param muted the status + */ + public AudioMute_1_1(final String output, final boolean muted) { + this(output, muted ? MUTEON : MUTEOFF); + } + + /** + * Instantiates a new audio mute. + * + * @param output the non-null, possibly empty output + * @param mute the mute status + */ + public AudioMute_1_1(final String output, final String mute) { + Objects.requireNonNull(output, "output cannot be empty"); + SonyUtil.validateNotEmpty(mute, "mute cannot be empty"); + + this.output = output; + this.mute = mute; + } + + /** + * Gets the output of the mute + * + * @return the output of the mute + */ + public String getOutput() { + return output; + } + + /** + * Get's the status of the mute + * + * @return the status of the mute + */ + public String getMute() { + return mute; + } + + @Override + public String toString() { + return "AudioMute_1_1 [output=" + output + ", mute=" + mute + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioSourceScreen.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioSourceScreen.java new file mode 100644 index 0000000000000..3507d4bd59bfa --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioSourceScreen.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the audio source screen and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class AudioSourceScreen { + + /** The screen for the audio source. */ + private @Nullable String screen; + + /** + * Constructor used for deserialization only + */ + public AudioSourceScreen() { + } + + /** + * Gets the screen + * + * @return the screen + */ + public @Nullable String getScreen() { + return screen; + } + + @Override + public String toString() { + return "AudioSourceScreen [screen=" + screen + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioVolume_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioVolume_1_0.java new file mode 100644 index 0000000000000..7a5b4baa69fa0 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioVolume_1_0.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the audio volume and is used for serialization + * + * Versions: + *
    + *
  1. 1.0: {"target":"string", "volume":"string"}
  2. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class AudioVolume_1_0 { + + /** The target of the volume */ + private final @Nullable String target; + + /** The volume level */ + private final @Nullable String volume; + + /** + * Instantiates a new audio volume. + * + * @param target the non-null, non-empty target + * @param volume the volume + */ + public AudioVolume_1_0(final String target, final int volume) { + this(target, String.valueOf(volume)); + } + + /** + * Instantiates a new audio volume. + * + * @param target the non-null, possibly empty target + * @param volume the non-null, non-empty volume + */ + public AudioVolume_1_0(final String target, final String volume) { + Objects.requireNonNull(target, "target cannot be empty"); + SonyUtil.validateNotEmpty(volume, "volume cannot be empty"); + + this.target = target; + this.volume = volume; + } + + /** + * Gets the volume. + * + * @return the volume + */ + public @Nullable String getVolume() { + return volume; + } + + /** + * Gets the target. + * + * @return the target + */ + public @Nullable String getTarget() { + return target; + } + + @Override + public String toString() { + return "AudioVolume_1_0 [target=" + target + ", volume=" + volume + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioVolume_1_1.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioVolume_1_1.java new file mode 100644 index 0000000000000..571d380a1a0c5 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioVolume_1_1.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the audio volume and is used for serialization + * + * Versions: + *
    + *
  1. 1.1: {"output":"string", "volume":"string"}
  2. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class AudioVolume_1_1 { + + /** The target of the volume */ + private final @Nullable String output; + + /** The volume level */ + private final @Nullable String volume; + + /** + * Instantiates a new audio volume. + * + * @param output the non-null, non-empty output + * @param volume the volume + */ + public AudioVolume_1_1(final String output, final int volume) { + this(output, String.valueOf(volume)); + } + + /** + * Instantiates a new audio volume. + * + * @param output the non-null, possibly empty output + * @param volume the non-null, non-empty volume + */ + public AudioVolume_1_1(final String output, final String volume) { + Objects.requireNonNull(output, "output cannot be empty"); + SonyUtil.validateNotEmpty(volume, "volume cannot be empty"); + + this.output = output; + this.volume = volume; + } + + /** + * Gets the volume. + * + * @return the volume + */ + public @Nullable String getVolume() { + return volume; + } + + /** + * Gets the output + * + * @return the output + */ + public @Nullable String getOutput() { + return output; + } + + @Override + public String toString() { + return "AudioVolume_1_1 [output=" + output + ", volume=" + volume + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioVolume_1_2.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioVolume_1_2.java new file mode 100644 index 0000000000000..9fde319d141cd --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/AudioVolume_1_2.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the audio volume and is used for serialization + * + * Versions: + *
    + *
  1. 1.2: {"target":"string", "volume":"string", "ui":"string"}
  2. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class AudioVolume_1_2 extends AudioVolume_1_0 { + /** + * Instantiates a new audio volume. + * + * @param target the non-null, possibly empty target + * @param volume the volume + */ + public AudioVolume_1_2(final String target, final int volume) { + super(target, volume); + } + + public AudioVolume_1_2(final String target, final String volume) { + super(target, volume); + } + + @Override + public String toString() { + return "AudioVolume_1_2 [target=" + getTarget() + ", volume=" + getVolume() + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BannerMode.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BannerMode.java new file mode 100644 index 0000000000000..17476ce2a6a48 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BannerMode.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the banner mode and is for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class BannerMode { + + /** The current value of the banner mode */ + private @Nullable String currentValue; + + /** + * Constructor used for deserialization only + */ + public BannerMode() { + } + + /** + * Gets the current value of the banner mode + * + * @return the current value of the banner mode + */ + public @Nullable String getCurrentValue() { + return currentValue; + } + + @Override + public String toString() { + return "BannerMode [currentValue=" + currentValue + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BivlInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BivlInfo.java new file mode 100644 index 0000000000000..f17108d84d511 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BivlInfo.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the bravia internet video link information + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class BivlInfo { + /** The Bravia Internet Video Link (BIVL) service id */ + private @Nullable String serviceId; + + /** The BIVL asset ID */ + private @Nullable String assetId; + + /** The BIVL provider */ + private @Nullable String provider; + + /** + * Constructor used for deserialization only + */ + public BivlInfo() { + } + + /** + * Returns the service ID + * + * @return possibly null, possibly empty service id + */ + public @Nullable String getServiceId() { + return serviceId; + } + + /** + * Returns the asset ID + * + * @return possibly null, possibly empty asset id + */ + public @Nullable String getAssetId() { + return assetId; + } + + /** + * Returns the provider ID + * + * @return possibly null, possibly empty provider id + */ + public @Nullable String getProvider() { + return provider; + } + + @Override + public String toString() { + return "BivlInfo [serviceId=" + serviceId + ", assetId=" + assetId + ", provider=" + provider + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BroadcastFreq.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BroadcastFreq.java new file mode 100644 index 0000000000000..b1897919b3512 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BroadcastFreq.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the broadcase frequenecy + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class BroadcastFreq { + /** The broadcast frequency */ + private @Nullable Integer frequency; + + /** THe broadcast band */ + private @Nullable String band; + + /** + * Constructor used for deserialization only + */ + public BroadcastFreq() { + } + + /** + * Returns the broadcast frequency + * + * @return possibly null broadcast frequency + */ + public @Nullable Integer getFrequency() { + return frequency; + } + + /** + * Returns the broadcast band + * + * @return possibly null, possibly empty broadcast band + */ + public @Nullable String getBand() { + return band; + } + + @Override + public String toString() { + return "BroadcastFreq [frequency=" + frequency + ", band=" + band + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BroadcastGenreInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BroadcastGenreInfo.java new file mode 100644 index 0000000000000..5aafb7fcb5375 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BroadcastGenreInfo.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a broadcast genre information + * + * TODO: still looking for an example of this.. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class BroadcastGenreInfo { + /** + * Constructor used for deserialization only + */ + public BroadcastGenreInfo() { + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BrowserBookmark.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BrowserBookmark.java new file mode 100644 index 0000000000000..6575d0028dada --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BrowserBookmark.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents a browser bookmark and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class BrowserBookmark { + + /** The url being bookmarked */ + private @Nullable String url; + + /** The title of the bookmark */ + private @Nullable String title; + + /** + * Constructor used for deserialization only + */ + public BrowserBookmark() { + } + + /** + * Gets the bookmark url + * + * @return the bookmark url + */ + public @Nullable String getUrl() { + return url; + } + + /** + * Gets the title + * + * @return the title + */ + public @Nullable String getTitle() { + return title; + } + + @Override + public String toString() { + return "BrowserBookmark [url=" + url + ", title=" + title + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BrowserControl.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BrowserControl.java new file mode 100644 index 0000000000000..174f347e110cd --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/BrowserControl.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the browser control and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class BrowserControl { + + /** The browser control */ + private final @Nullable String control; + + /** + * Instantiates a new browser control. + * + * @param control the non-null, non-empty control + */ + public BrowserControl(final String control) { + SonyUtil.validateNotEmpty(control, "control cannot be empty"); + this.control = control; + } + + /** + * Gets the control + * + * @return the control + */ + public @Nullable String getControl() { + return control; + } + + @Override + public String toString() { + return "BrowserControl [control=" + control + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentCount_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentCount_1_0.java new file mode 100644 index 0000000000000..6f78a30f35907 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentCount_1_0.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents a content count request and is used for serialization only + * + * Versions: + *
    + *
  1. 1.0: {"source":"string", "type":"string"}
  2. + *
  3. 1.1: {"source":"string", "type":"string", "target":"string"}
  4. + *
  5. 1.2: unknown (may have switched to uri like in 1.3 - waiting for example)
  6. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ContentCount_1_0 { + + /** The source */ + private final String source; + + /** + * Instantiates a new content count for the source + * + * @param source the non-null, non-empty source + */ + public ContentCount_1_0(final String source) { + SonyUtil.validateNotEmpty(source, "source cannot be empty"); + this.source = source; + } + + /** + * Gets the source + * + * @return the source + */ + public String getSource() { + return source; + } + + @Override + public String toString() { + return "ContentCount_1_0 [source=" + source + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentCount_1_3.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentCount_1_3.java new file mode 100644 index 0000000000000..23c69605285d8 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentCount_1_3.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents a content count request and is used for serialization only + * + * Versions: + *
    + *
  1. 1.3: {"uri":"string", "type":"string*", "target":"string", "view":"string", "path":"string"}
  2. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ContentCount_1_3 { + + /** The uri */ + private final String uri; + + /** + * Instantiates a new content count for the uri + * + * @param uri the non-null, non-empty uri + */ + public ContentCount_1_3(final String uri) { + SonyUtil.validateNotEmpty(uri, "uri cannot be empty"); + this.uri = uri; + } + + /** + * Gets the uri + * + * @return the uri + */ + public String getUri() { + return uri; + } + + @Override + public String toString() { + return "ContentCount_1_2 [uri=" + uri + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentInfo.java new file mode 100644 index 0000000000000..908351dc2202a --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentInfo.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents content information + * + * TODO: still looking for an example of this.. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ContentInfo { + /** + * Constructor used for deserialization only + */ + public ContentInfo() { + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListRequest_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListRequest_1_0.java new file mode 100644 index 0000000000000..fc424c6ccf7b0 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListRequest_1_0.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents a content list request and is used for serialization only + * + * Versions: + *
    + *
  1. 1.0: {"source":"string", "stIdx":"int", "cnt":"int", "type":"string"}
  2. + *
  3. 1.1: unknown
  4. + *
  5. 1.2: {"source":"string", "stIdx":"int", "cnt":"int", "type":"string", "target":"string"}
  6. + *
  7. 1.3: unknown
  8. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ContentListRequest_1_0 { + + /** The source for the request */ + private final String source; + + /** The source starting index. */ + private final int stIdx; + + /** The count of items */ + private final int cnt; + + /** + * Instantiates a new content list request. + * + * @param source the non-null, non-empty source + * @param stIdx the starting index (>= 0) + * @param cnt the total count (>= 0) + */ + public ContentListRequest_1_0(final String source, final int stIdx, final int cnt) { + SonyUtil.validateNotEmpty(source, "source cannot be empty"); + if (stIdx < -1) { + throw new IllegalArgumentException("stIdx cannot be < 0: " + stIdx); + } + if (cnt < 0) { + throw new IllegalArgumentException("cnt cannot be < 0: " + cnt); + } + + this.source = source; + this.stIdx = stIdx; + this.cnt = cnt; + } + + /** + * Gets the source of the request + * + * @return the source of the request + */ + public @Nullable String getSource() { + return source; + } + + /** + * Gets the starting index + * + * @return the starting index + */ + public int getStIdx() { + return stIdx; + } + + /** + * Gets the total count + * + * @return the total count + */ + public int getCnt() { + return cnt; + } + + @Override + public String toString() { + return "ContentListRequest_1_0 [source=" + source + ", stIdx=" + stIdx + ", cnt=" + cnt + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListRequest_1_4.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListRequest_1_4.java new file mode 100644 index 0000000000000..10eccbdafe5ea --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListRequest_1_4.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents a content list request and is used for serialization only + * + * Versions: + *
    + *
  1. 1.4: {"uri":"string", "stIdx":"int", "cnt":"int", "type":"string*", "target":"string", "view":"string", + * "sort":"string", "path":"string"}
  2. + *
  3. 1.5: {"uri":"string", "stIdx":"int", "cnt":"int", "type":"string*", "target":"string", "view":"string", + * "sort":"SortInfo", "search":"Search", "filter":"string*"}
  4. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ContentListRequest_1_4 { + + /** The uri for the request */ + private final String uri; + + /** The source starting index. */ + private final int stIdx; + + /** The count of items */ + private final int cnt; + + /** + * Instantiates a new content list request. + * + * @param uri the non-null, non-empty uri + * @param stIdx the starting index (>= 0) + * @param cnt the total count (>= 0) + */ + public ContentListRequest_1_4(final String uri, final int stIdx, final int cnt) { + SonyUtil.validateNotEmpty(uri, "uri cannot be empty"); + if (stIdx < -1) { + throw new IllegalArgumentException("stIdx cannot be < 0: " + stIdx); + } + if (cnt < 0) { + throw new IllegalArgumentException("cnt cannot be < 0: " + cnt); + } + + this.uri = uri; + this.stIdx = stIdx; + this.cnt = cnt; + } + + /** + * Gets the source of the request + * + * @return the source of the request + */ + public @Nullable String getSource() { + return uri; + } + + /** + * Gets the starting index + * + * @return the starting index + */ + public int getStIdx() { + return stIdx; + } + + /** + * Gets the total count + * + * @return the total count + */ + public int getCnt() { + return cnt; + } + + @Override + public String toString() { + return "ContentListRequest_1_4 [uri=" + uri + ", stIdx=" + stIdx + ", cnt=" + cnt + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_0.java new file mode 100644 index 0000000000000..96e56a415c05b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_0.java @@ -0,0 +1,221 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents a content list result and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ContentListResult_1_0 { + + /** The channel name */ + private @Nullable String channelName; + + /** The direct remote number */ + private @Nullable Integer directRemoteNum; + + /** The content display number */ + private @Nullable String dispNum; + + /** The duration (in seconds) */ + private @Nullable Integer durationSec; + + /** The file size (bytes) */ + private @Nullable Integer fileSizeByte; + + /** The index. */ + private @Nullable Integer index; + + /** Whether the content has already been played */ + private @Nullable Boolean isAlreadyPlayed; + + /** Whether the content is protected */ + private @Nullable Boolean isProtected; + + /** The original display number. */ + private @Nullable String originalDispNum; + + /** The program media type */ + private @Nullable String programMediaType; + + /** The program number */ + private @Nullable Integer programNum; + + /** The start date time */ + private @Nullable String startDateTime; + + /** The content title */ + private @Nullable String title; + + /** The triplet channel number */ + private @Nullable String tripletStr; + + /** The content uri */ + private @Nullable String uri; + + /** + * Constructor used for deserialization only + */ + public ContentListResult_1_0() { + } + + /** + * Gets the channel name + * + * @return the channel name + */ + public @Nullable String getChannelName() { + return channelName; + } + + /** + * Gets the direct remote number + * + * @return the direct remote number + */ + public @Nullable Integer getDirectRemoteNum() { + return directRemoteNum; + } + + /** + * Gets the display number + * + * @return the display number + */ + public @Nullable String getDispNum() { + return dispNum; + } + + /** + * Gets the duration in seconds + * + * @return the duration in seconds + */ + public @Nullable Integer getDurationSec() { + return durationSec; + } + + /** + * Gets the file size (in bytes) + * + * @return the file size + */ + public @Nullable Integer getFileSizeByte() { + return fileSizeByte; + } + + /** + * Gets the index position + * + * @return the index position + */ + public @Nullable Integer getIndex() { + return index; + } + + /** + * Gets whether the content has already been played + * + * @return whether the content has already been played + */ + public @Nullable Boolean isAlreadyPlayed() { + return isAlreadyPlayed; + } + + /** + * Gets whether the content is protected + * + * @return whether the content is protected + */ + public @Nullable Boolean isProtected() { + return isProtected; + } + + /** + * Gets the original display number + * + * @return the original display number + */ + public @Nullable String getOriginalDispNum() { + return originalDispNum; + } + + /** + * Gets the program media type + * + * @return the program media type + */ + public @Nullable String getProgramMediaType() { + return programMediaType; + } + + /** + * Gets the program number + * + * @return the program number + */ + public @Nullable Integer getProgramNum() { + return programNum; + } + + /** + * Gets the start date/time + * + * @return the start date/time + */ + public @Nullable String getStartDateTime() { + return startDateTime; + } + + /** + * Gets the content title + * + * @return the content tile + */ + public @Nullable String getTitle() { + return title; + } + + /** + * Gets the content triplet channel string + * + * @return the triplet channel string + */ + public @Nullable String getTripletStr() { + return tripletStr; + } + + /** + * Gets the content URI + * + * @return the content URI + */ + public @Nullable String getUri() { + return uri; + } + + @Override + public String toString() { + return "ContentListResult_1_0 [channelName=" + channelName + ", directRemoteNum=" + directRemoteNum + + ", dispNum=" + dispNum + ", durationSec=" + durationSec + ", fileSizeByte=" + fileSizeByte + + ", index=" + index + ", isAlreadyPlayed=" + isAlreadyPlayed + ", isProtected=" + isProtected + + ", originalDispNum=" + originalDispNum + ", programMediaType=" + programMediaType + ", programNum=" + + programNum + ", startDateTime=" + startDateTime + ", title=" + title + ", tripletStr=" + tripletStr + + ", uri=" + uri + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_2.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_2.java new file mode 100644 index 0000000000000..1248a47a297fb --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_2.java @@ -0,0 +1,460 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents a content list result and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ContentListResult_1_2 { + + /** The audio channels */ + private @Nullable String @Nullable [] audioChannel; + + /** The audio codecs */ + private @Nullable String @Nullable [] audioCodec; + + /** The audio frequencies */ + private @Nullable String @Nullable [] audioFrequency; + + /** The channel name */ + private @Nullable String channelName; + + /** The channel surfing visibility */ + private @Nullable String channelSurfingVisibility; + + /** The total chapter count */ + private @Nullable Integer chapterCount; + + /** The content type */ + private @Nullable String contentType; + + /** The created time */ + private @Nullable String createdTime; + + /** The direct remote number */ + private @Nullable Integer directRemoteNum; + + /** The content display number */ + private @Nullable String dispNum; + + /** The duration (in seconds) */ + private @Nullable Double durationSec; + + /** The epg visibility */ + private @Nullable String epgVisibility; + + /** The file size (bytes) */ + private @Nullable Integer fileSizeByte; + + /** The index. */ + private @Nullable Integer index; + + /** Whether the content has already been played */ + private @Nullable Boolean isAlreadyPlayed; + + /** Whether the content is protected */ + private @Nullable Boolean isProtected; + + /** The original display number. */ + private @Nullable String originalDispNum; + + /** The parental countries */ + private @Nullable String @Nullable [] parentalCountry; + + /** The parental ratings */ + private @Nullable String @Nullable [] parentalRating; + + /** The parental systems */ + private @Nullable String @Nullable [] parentalSystem; + + /** The product identifier */ + private @Nullable String productID; + + /** The program media type */ + private @Nullable String programMediaType; + + /** The program number */ + private @Nullable Integer programNum; + + /** The size (in MB) */ + private @Nullable Integer sizeMB; + + /** The start date time */ + private @Nullable String startDateTime; + + /** The storage uri (for usb, etc) */ + private @Nullable String storageUri; + + /** The subtitle languages */ + private @Nullable String @Nullable [] subtitleLanguage; + + /** The subtitle titles */ + private @Nullable String @Nullable [] subtitleTitle; + + /** The content title */ + private @Nullable String title; + + /** The triplet channel number */ + private @Nullable String tripletStr; + + /** The content uri */ + private @Nullable String uri; + + /** The user content flag */ + private @Nullable Boolean userContentFlag; + + /** The video codec */ + private @Nullable String videoCodec; + + /** The visibility of the content */ + private @Nullable String visibility; + + /** + * Constructor used for deserialization only + */ + public ContentListResult_1_2() { + } + + /** + * Gets the audio channel + * + * @return the audio channel + */ + public @Nullable String @Nullable [] getAudioChannel() { + return audioChannel; + } + + /** + * Gets the audio codec + * + * @return the audio codec + */ + public @Nullable String @Nullable [] getAudioCodec() { + return audioCodec; + } + + /** + * Gets the audio frequency + * + * @return the audio frequency + */ + public @Nullable String @Nullable [] getAudioFrequency() { + return audioFrequency; + } + + /** + * Gets the channel name + * + * @return the channel name + */ + public @Nullable String getChannelName() { + return channelName; + } + + /** + * Gets the channel surfing visibility + * + * @return the channel surfing visibility + */ + public @Nullable String getChannelSurfingVisibility() { + return channelSurfingVisibility; + } + + /** + * Gets the chapter count + * + * @return the chapter count + */ + public @Nullable Integer getChapterCount() { + return chapterCount; + } + + /** + * Gets the content type + * + * @return the content type + */ + public @Nullable String getContentType() { + return contentType; + } + + /** + * Gets the created time + * + * @return the created time + */ + public @Nullable String getCreatedTime() { + return createdTime; + } + + /** + * Gets the direct remote number + * + * @return the direct remote number + */ + public @Nullable Integer getDirectRemoteNum() { + return directRemoteNum; + } + + /** + * Gets the display number + * + * @return the display number + */ + public @Nullable String getDispNum() { + return dispNum; + } + + /** + * Gets the duration (in seconds) + * + * @return the duration (in seconds) + */ + public @Nullable Double getDurationSec() { + return durationSec; + } + + /** + * Gets the epg visibility + * + * @return the epg visibility + */ + public @Nullable String getEpgVisibility() { + return epgVisibility; + } + + /** + * Gets the file size byte + * + * @return the file size byte + */ + public @Nullable Integer getFileSizeByte() { + return fileSizeByte; + } + + /** + * Gets the index + * + * @return the index + */ + public @Nullable Integer getIndex() { + return index; + } + + /** + * Gets the original display number + * + * @return the original display number + */ + public @Nullable String getOriginalDispNum() { + return originalDispNum; + } + + /** + * Gets the parental country + * + * @return the parental country + */ + public @Nullable String @Nullable [] getParentalCountry() { + return parentalCountry; + } + + /** + * Gets the parental rating + * + * @return the parental rating + */ + public @Nullable String @Nullable [] getParentalRating() { + return parentalRating; + } + + /** + * Gets the parental system + * + * @return the parental system + */ + public @Nullable String @Nullable [] getParentalSystem() { + return parentalSystem; + } + + /** + * Gets the product ID + * + * @return the product ID + */ + public @Nullable String getProductID() { + return productID; + } + + /** + * Gets the program media type + * + * @return the program media type + */ + public @Nullable String getProgramMediaType() { + return programMediaType; + } + + /** + * Gets the program number + * + * @return the program number + */ + public @Nullable Integer getProgramNum() { + return programNum; + } + + /** + * Gets the size (in MB) + * + * @return the size (in MB) + */ + public @Nullable Integer getSizeMB() { + return sizeMB; + } + + /** + * Gets the start date time + * + * @return the start date time + */ + public @Nullable String getStartDateTime() { + return startDateTime; + } + + /** + * Gets the storage uri + * + * @return the storage uri + */ + public @Nullable String getStorageUri() { + return storageUri; + } + + /** + * Gets the subtitle language + * + * @return the subtitle language + */ + public @Nullable String @Nullable [] getSubtitleLanguage() { + return subtitleLanguage; + } + + /** + * Gets the subtitle title + * + * @return the subtitle title + */ + public @Nullable String @Nullable [] getSubtitleTitle() { + return subtitleTitle; + } + + /** + * Gets the title + * + * @return the title + */ + public @Nullable String getTitle() { + return title; + } + + /** + * Gets the triplet string + * + * @return the triplet string + */ + public @Nullable String getTripletStr() { + return tripletStr; + } + + /** + * Gets the uri of the content (ie the identifier) + * + * @return the uri of the content + */ + public @Nullable String getUri() { + return uri; + } + + /** + * Gets the video codec + * + * @return the video codec + */ + public @Nullable String getVideoCodec() { + return videoCodec; + } + + /** + * Gets the overall visibility + * + * @return the overall visibility + */ + public @Nullable String getVisibility() { + return visibility; + } + + /** + * Checks if the media was already played + * + * @return true if played, false otherwise + */ + public @Nullable Boolean isAlreadyPlayed() { + return isAlreadyPlayed; + } + + /** + * Checks if media is protected + * + * @return true if protected - false otherwise + */ + public @Nullable Boolean isProtected() { + return isProtected; + } + + /** + * Checks the user content flag + * + * @return true if user content, false otherwise + */ + public @Nullable Boolean isUserContentFlag() { + return userContentFlag; + } + + @Override + public String toString() { + return "ContentListResult_1_2 [audioChannel=" + Arrays.toString(audioChannel) + ", audioCodec=" + + Arrays.toString(audioCodec) + ", audioFrequency=" + Arrays.toString(audioFrequency) + ", channelName=" + + channelName + ", channelSurfingVisibility=" + channelSurfingVisibility + ", chapterCount=" + + chapterCount + ", contentType=" + contentType + ", createdTime=" + createdTime + ", directRemoteNum=" + + directRemoteNum + ", dispNum=" + dispNum + ", durationSec=" + durationSec + ", epgVisibility=" + + epgVisibility + ", fileSizeByte=" + fileSizeByte + ", index=" + index + ", isAlreadyPlayed=" + + isAlreadyPlayed + ", isProtected=" + isProtected + ", originalDispNum=" + originalDispNum + + ", parentalCountry=" + Arrays.toString(parentalCountry) + ", parentalRating=" + + Arrays.toString(parentalRating) + ", parentalSystem=" + Arrays.toString(parentalSystem) + + ", productID=" + productID + ", programMediaType=" + programMediaType + ", programNum=" + programNum + + ", sizeMB=" + sizeMB + ", startDateTime=" + startDateTime + ", storageUri=" + storageUri + + ", subtitleLanguage=" + Arrays.toString(subtitleLanguage) + ", subtitleTitle=" + + Arrays.toString(subtitleTitle) + ", title=" + title + ", tripletStr=" + tripletStr + ", uri=" + uri + + ", userContentFlag=" + userContentFlag + ", videoCodec=" + videoCodec + ", visibility=" + visibility + + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_4.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_4.java new file mode 100644 index 0000000000000..f8a591ecca470 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_4.java @@ -0,0 +1,617 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents a content list result and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ContentListResult_1_4 { + + /** The album name */ + private @Nullable String albumName; + + /** The artist */ + private @Nullable String artist; + + /** The audio codecs */ + private @Nullable AudioInfo @Nullable [] audioInfo; + + /** The broadcast frequency */ + private @Nullable Integer broadcastFreq; + + /** The broadcast frequency band */ + private @Nullable String broadcastFreqBand; + + /** The channel name */ + private @Nullable String channelName; + + /** The channel surfing visibility */ + private @Nullable String channelSurfingVisibility; + + /** The total chapter count */ + private @Nullable Integer chapterCount; + + /** The content information */ + private @Nullable ContentInfo content; + + /** THe content kind */ + private @Nullable String contentKind; + + /** The content type */ + private @Nullable String contentType; + + /** The created time */ + private @Nullable String createdTime; + + /** The direct remote number */ + private @Nullable Integer directRemoteNum; + + /** The content display number */ + private @Nullable String dispNum; + + /** The duration (in seconds) */ + private @Nullable Double durationMSec; + + /** The epg visibility */ + private @Nullable String epgVisibility; + + private @Nullable String fileNo; + + /** The file size (bytes) */ + private @Nullable Integer fileSizeByte; + + /** The folder number */ + private @Nullable String folderNo; + + /** The genre */ + private @Nullable String genre; + + /** The index */ + private @Nullable Integer index; + + /** The 3D setting */ + private @Nullable String is3D; + + /** Whether the content has already been played */ + private @Nullable String isAlreadyPlayed; + + /** Whether the content is browseable */ + private @Nullable String isBrowsable; + + /** Whether the content is playable */ + private @Nullable String isPlayable; + + /** Whether the content is protected */ + private @Nullable String isProtected; + + /** The original display number */ + private @Nullable String originalDispNum; + + /** The audio channels */ + private @Nullable ParentalInfo @Nullable [] parentalInfo; + + /** The parent index */ + private @Nullable Integer parentIndex; + + /** The parent URI */ + private @Nullable String parentUri; + + /** The current path */ + private @Nullable String path; + + /** The play list name */ + private @Nullable String playlistName; + + /** The podcast name */ + private @Nullable String podcastName; + + /** The product identifier */ + private @Nullable String productID; + + /** The program media type */ + private @Nullable String programMediaType; + + /** The program number */ + private @Nullable Integer programNum; + + /** The remote play type */ + private @Nullable String remotePlayType; + + /** The size (in MB) */ + private @Nullable Integer sizeMB; + + /** The start date time */ + private @Nullable String startDateTime; + + /** The storage uri (for usb, etc) */ + private @Nullable String storageUri; + + /** The audio frequencies */ + private @Nullable SubtitleInfo @Nullable [] subtitleInfo; + + /** The content title */ + private @Nullable String title; + + /** The triplet channel number */ + private @Nullable String tripletStr; + + /** The content uri */ + private @Nullable String uri; + + /** The user content flag */ + private @Nullable Boolean userContentFlag; + + /** The video information (codecs) */ + private @Nullable VideoInfo videoInfo; + + /** The visibility of the content */ + private @Nullable String visibility; + + /** + * Constructor used for deserialization only + */ + public ContentListResult_1_4() { + } + + /** + * Gets the album name + * + * @return the album name + */ + public @Nullable String getAlbumName() { + return albumName; + } + + /** + * Gets the artist + * + * @return the artist + */ + public @Nullable String getArtist() { + return artist; + } + + /** + * Gets the audio information + * + * @return the audio information + */ + public @Nullable AudioInfo @Nullable [] getAudioInfo() { + return audioInfo; + } + + /** + * Gets the broadcast frequency + * + * @return the broadcast frequency + */ + public @Nullable Integer getBroadcastFreq() { + return broadcastFreq; + } + + /** + * Gets the broadcast frequency band + * + * @return the broadcast frequency band + */ + public @Nullable String getBroadcastFreqBand() { + return broadcastFreqBand; + } + + /** + * Gets the channel name + * + * @return the channel name + */ + public @Nullable String getChannelName() { + return channelName; + } + + /** + * Gets the channel surfing visibility + * + * @return the channel surfing visibility + */ + public @Nullable String getChannelSurfingVisibility() { + return channelSurfingVisibility; + } + + /** + * Gets the chapter count + * + * @return the chapter count + */ + public @Nullable Integer getChapterCount() { + return chapterCount; + } + + /** + * Gets the content information + * + * @return the content information + */ + public @Nullable ContentInfo getContent() { + return content; + } + + /** + * Gets the content kind + * + * @return the content kind + */ + public @Nullable String getContentKind() { + return contentKind; + } + + /** + * Gets the content type + * + * @return the content type + */ + public @Nullable String getContentType() { + return contentType; + } + + /** + * Gets the create time + * + * @return the create time + */ + public @Nullable String getCreatedTime() { + return createdTime; + } + + /** + * Gets the direct remote number + * + * @return the direct remote number + */ + public @Nullable Integer getDirectRemoteNum() { + return directRemoteNum; + } + + /** + * Gets the display number + * + * @return the display number + */ + public @Nullable String getDispNum() { + return dispNum; + } + + /** + * Gets the duration in milliseconds + * + * @return the duration in milliseconds + */ + public @Nullable Double getDurationMSec() { + return durationMSec; + } + + /** + * Gets the EPG visibility + * + * @return the EPG visibility + */ + public @Nullable String getEpgVisibility() { + return epgVisibility; + } + + /** + * Gets the file number + * + * @return the file number + */ + public @Nullable String getFileNo() { + return fileNo; + } + + /** + * Gets the file size in bytes + * + * @return the file size in bytes + */ + public @Nullable Integer getFileSizeByte() { + return fileSizeByte; + } + + /** + * Gets the folder number + * + * @return the folder number + */ + public @Nullable String getFolderNo() { + return folderNo; + } + + /** + * Gets the genre + * + * @return the genre + */ + public @Nullable String getGenre() { + return genre; + } + + /** + * Gets the index + * + * @return the index + */ + public @Nullable Integer getIndex() { + return index; + } + + /** + * Gets the 3D setting + * + * @return the 3D setting + */ + public @Nullable String is3D() { + return is3D; + } + + /** + * Whether the content was already played + * + * @return whether the content was already played + */ + public @Nullable String isAlreadyPlayed() { + return isAlreadyPlayed; + } + + /** + * Whether the content is browseable + * + * @return whether the content is browseable + */ + public @Nullable String isBrowsable() { + return isBrowsable; + } + + /** + * Whether the content is playable + * + * @return whether the content is playable + */ + public @Nullable String isPlayable() { + return isPlayable; + } + + /** + * Whether the content is protected + * + * @return whether the content is protected + */ + public @Nullable String isProtected() { + return isProtected; + } + + /** + * Gets the original display number + * + * @return the original display number + */ + public @Nullable String getOriginalDispNum() { + return originalDispNum; + } + + /** + * Gets the parental information + * + * @return the parental information + */ + public @Nullable ParentalInfo @Nullable [] getParentalInfo() { + return parentalInfo; + } + + /** + * Gets the parent index + * + * @return the parent index + */ + public @Nullable Integer getParentIndex() { + return parentIndex; + } + + /** + * Gets the parent URI + * + * @return the parent URI + */ + public @Nullable String getParentUri() { + return parentUri; + } + + /** + * Gets the content path + * + * @return the content path + */ + public @Nullable String getPath() { + return path; + } + + /** + * Gets the play list name + * + * @return the play list name + */ + public @Nullable String getPlaylistName() { + return playlistName; + } + + /** + * Gets the podcast name + * + * @return the podcast name + */ + public @Nullable String getPodcastName() { + return podcastName; + } + + /** + * Gets the product ID + * + * @return the product ID + */ + public @Nullable String getProductID() { + return productID; + } + + /** + * Gets the program media type + * + * @return the program media type + */ + public @Nullable String getProgramMediaType() { + return programMediaType; + } + + /** + * Gets the program number + * + * @return the program number + */ + public @Nullable Integer getProgramNum() { + return programNum; + } + + /** + * Gets the remote play type + * + * @return the remote play type + */ + public @Nullable String getRemotePlayType() { + return remotePlayType; + } + + /** + * Gets the size in MB + * + * @return the size in MB + */ + public @Nullable Integer getSizeMB() { + return sizeMB; + } + + /** + * Gets the start date/time + * + * @return the start date/time + */ + public @Nullable String getStartDateTime() { + return startDateTime; + } + + /** + * Gets the storage URI + * + * @return the storage URI + */ + public @Nullable String getStorageUri() { + return storageUri; + } + + /** + * Gets the subtitle information + * + * @return the subtitle information + */ + public @Nullable SubtitleInfo @Nullable [] getSubtitleInfo() { + return subtitleInfo; + } + + /** + * Gets the content title + * + * @return the content title + */ + public @Nullable String getTitle() { + return title; + } + + /** + * Gets the channel triplet string + * + * @return the channel triplet string + */ + public @Nullable String getTripletStr() { + return tripletStr; + } + + /** + * Gets the content URI + * + * @return the contentURI + */ + public @Nullable String getUri() { + return uri; + } + + /** + * Gets the content user flag + * + * @return the content user flag + */ + public @Nullable Boolean getUserContentFlag() { + return userContentFlag; + } + + /** + * Gets the video information + * + * @return the video information + */ + public @Nullable VideoInfo getVideoInfo() { + return videoInfo; + } + + /** + * Gets the content visibility + * + * @return the content visibility + */ + public @Nullable String getVisibility() { + return visibility; + } + + @Override + public String toString() { + return "ContentListResult_1_4 [albumName=" + albumName + ", artist=" + artist + ", audioInfo=" + + Arrays.toString(audioInfo) + ", broadcastFreq=" + broadcastFreq + ", broadcastFreqBand=" + + broadcastFreqBand + ", channelName=" + channelName + ", channelSurfingVisibility=" + + channelSurfingVisibility + ", chapterCount=" + chapterCount + ", content=" + content + + ", contentKind=" + contentKind + ", contentType=" + contentType + ", createdTime=" + createdTime + + ", directRemoteNum=" + directRemoteNum + ", dispNum=" + dispNum + ", durationMSec=" + durationMSec + + ", epgVisibility=" + epgVisibility + ", fileNo=" + fileNo + ", fileSizeByte=" + fileSizeByte + + ", folderNo=" + folderNo + ", genre=" + genre + ", index=" + index + ", is3D=" + is3D + + ", isAlreadyPlayed=" + isAlreadyPlayed + ", isBrowsable=" + isBrowsable + ", isPlayable=" + isPlayable + + ", isProtected=" + isProtected + ", originalDispNum=" + originalDispNum + ", parentalInfo=" + + Arrays.toString(parentalInfo) + ", parentIndex=" + parentIndex + ", parentUri=" + parentUri + + ", path=" + path + ", playlistName=" + playlistName + ", podcastName=" + podcastName + ", productID=" + + productID + ", programMediaType=" + programMediaType + ", programNum=" + programNum + + ", remotePlayType=" + remotePlayType + ", sizeMB=" + sizeMB + ", startDateTime=" + startDateTime + + ", storageUri=" + storageUri + ", subtitleInfo=" + Arrays.toString(subtitleInfo) + ", title=" + title + + ", tripletStr=" + tripletStr + ", uri=" + uri + ", userContentFlag=" + userContentFlag + + ", videoInfo=" + videoInfo + ", visibility=" + visibility + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_5.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_5.java new file mode 100644 index 0000000000000..3d427b304d3d9 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ContentListResult_1_5.java @@ -0,0 +1,975 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents a content list result and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ContentListResult_1_5 { + + /** The album name */ + private @Nullable String albumName; + + /** The application name */ + private @Nullable String applicationName; + + /** The artist */ + private @Nullable String artist; + + /** The audio informaiton */ + private @Nullable AudioInfo @Nullable [] audioInfo; + + /** The bravia internet video link information */ + private @Nullable BivlInfo bivlInfo; + + /** The broadcast frequency information */ + private @Nullable BroadcastFreq broadcastFreq; + + /** The broadcase genre information */ + private @Nullable BroadcastGenreInfo @Nullable [] broadcastGenreInfo; + + /** The channel name */ + private @Nullable String channelName; + + /** The chapter count */ + private @Nullable Integer chapterCount; + + /** The chapter index */ + private @Nullable Integer chapterIndex; + + /** The clip count */ + private @Nullable Integer clipCount; + + /** The content information */ + private @Nullable ContentInfo content; + + /** The content kind */ + private @Nullable String contentKind; + + /** The content type */ + private @Nullable String contentType; + + /** The created time */ + private @Nullable String createdTime; + + /** The digital audio broadcast info */ + private @Nullable DabInfo dabInfo; + + /** The data broadcast info */ + private @Nullable DataBroadcastInfo dataInfo; + + /** The description */ + private @Nullable Description description; + + /** The direct remote number */ + private @Nullable Integer directRemoteNum; + + /** The content display number */ + private @Nullable String dispNum; + + /** The dubbing information */ + private @Nullable DubbingInfo dubbingInfo; + + /** The duration */ + private @Nullable Duration duration; + + /** The event id */ + private @Nullable String eventId; + + /** The file number */ + private @Nullable String fileNo; + + /** The file size (bytes) */ + private @Nullable Integer fileSizeByte; + + /** The folder number */ + private @Nullable String folderNo; + + /** The genre */ + private @Nullable String genre; + + /** The global playback count */ + private @Nullable Integer globalPlaybackCount; + + /** The group information */ + private @Nullable GroupInfo @Nullable [] groupInfo; + + /** Whether the content has resumed */ + private @Nullable String hasResume; + + /** The index */ + private @Nullable Integer index; + + /** The 3D setting */ + private @Nullable String is3D; + + /** The 4K setting */ + private @Nullable String is4K; + + /** Whether the content has already been played */ + private @Nullable String isAlreadyPlayed; + + /** Whether the content is set to auto delete */ + private @Nullable String isAutoDelete; + + /** Whether the content is browseable */ + private @Nullable String isBrowsable; + + /** Whether the content is new */ + private @Nullable String isNew; + + /** Whether the content is playable */ + private @Nullable String isPlayable; + + /** Whether the content is a play list */ + private @Nullable String isPlaylist; + + /** Whether the content is protected */ + private @Nullable String isProtected; + + /** Whether the content is a sound photo */ + private @Nullable String isSoundPhoto; + + /** The content media type */ + private @Nullable String mediaType; + + /** The original display number */ + private @Nullable String originalDispNum; + + /** The output */ + private @Nullable String output; + + /** The parental information */ + private @Nullable ParentalInfo @Nullable [] parentalInfo; + + /** The parent index */ + private @Nullable Integer parentIndex; + + /** The parent URL */ + private @Nullable String parentUri; + + /** The play list information */ + private @Nullable PlaylistInfo @Nullable [] playlistInfo; + + /** The play list name */ + private @Nullable String playlistName; + + /** The play speed */ + private @Nullable Speed playSpeed; + + /** The podcast name */ + private @Nullable String podcastName; + + /** Really doubt this is PIP position */ + private @Nullable Position position; + + /** The product identifier */ + private @Nullable String productID; + + /** The program media type */ + private @Nullable String programMediaType; + + /** The program number */ + private @Nullable Integer programNum; + + /** The program service type */ + private @Nullable String programServiceType; + + /** The program title */ + private @Nullable String programTitle; + + /** The recording information */ + private @Nullable RecordingInfo recordingInfo; + + /** The remote play type */ + private @Nullable String remotePlayType; + + /** The repeat type */ + private @Nullable String repeatType; + + /** The service */ + private @Nullable String service; + + /** The size (in MB) */ + private @Nullable Integer sizeMB; + + /** The content source */ + private @Nullable String source; + + /** The content source label */ + private @Nullable String sourceLabel; + + /** The start date time */ + private @Nullable String startDateTime; + + /** The state information */ + private @Nullable StateInfo stateInfo; + + /** The storage uri (for usb, etc) */ + private @Nullable String storageUri; + + /** The subtitle information */ + private @Nullable SubtitleInfo @Nullable [] subtitleInfo; + + /** The content sync priority */ + private @Nullable String syncContentPriority; + + /** The content title */ + private @Nullable String title; + + /** The content total count */ + private @Nullable Integer totalCount; + + /** The triplet channel number */ + private @Nullable String tripletStr; + + /** The content uri */ + private @Nullable String uri; + + /** The user content flag */ + private @Nullable Boolean userContentFlag; + + /** The content video information */ + private @Nullable VideoInfo @Nullable [] videoInfo; + + /** The visibility of the content */ + private @Nullable Visibility visibility; + + /** + * Constructor used for deserialization only + */ + public ContentListResult_1_5() { + } + + /** + * Gets the album name + * + * @return the album name + */ + public @Nullable String getAlbumName() { + return albumName; + } + + /** + * Gets the application name + * + * @return the application name + */ + public @Nullable String getApplicationName() { + return applicationName; + } + + /** + * Gets the artist + * + * @return the artist + */ + public @Nullable String getArtist() { + return artist; + } + + /** + * Gets the audio information + * + * @return the audio information + */ + public @Nullable AudioInfo @Nullable [] getAudioInfo() { + return audioInfo; + } + + /** + * Gets the BIVL info + * + * @return the BIVL info + */ + public @Nullable BivlInfo getBivlInfo() { + return bivlInfo; + } + + /** + * Gets the broadcast frequency info + * + * @return the broadcast frequency info + */ + public @Nullable BroadcastFreq getBroadcastFreq() { + return broadcastFreq; + } + + /** + * Gets the broadcast genre info + * + * @return the broadcast genre info + */ + public @Nullable BroadcastGenreInfo @Nullable [] getBroadcastGenreInfo() { + return broadcastGenreInfo; + } + + /** + * Gets the channel name + * + * @return the channel name + */ + public @Nullable String getChannelName() { + return channelName; + } + + /** + * Gets the chapter count + * + * @return the chapter count + */ + public @Nullable Integer getChapterCount() { + return chapterCount; + } + + /** + * Gets the chapter index + * + * @return the chapter index + */ + public @Nullable Integer getChapterIndex() { + return chapterIndex; + } + + /** + * Gets the clip count + * + * @return the clip count + */ + public @Nullable Integer getClipCount() { + return clipCount; + } + + /** + * Gets the content info + * + * @return the content info + */ + public @Nullable ContentInfo getContent() { + return content; + } + + /** + * Gets the content kind + * + * @return the content kind + */ + public @Nullable String getContentKind() { + return contentKind; + } + + /** + * Gets the content type + * + * @return the content type + */ + public @Nullable String getContentType() { + return contentType; + } + + /** + * Gets the created time + * + * @return the created time + */ + public @Nullable String getCreatedTime() { + return createdTime; + } + + /** + * Gets the DAB info + * + * @return the DAB info + */ + public @Nullable DabInfo getDabInfo() { + return dabInfo; + } + + /** + * Gets the broadcast data information + * + * @return the broadcast data information + */ + public @Nullable DataBroadcastInfo getDataInfo() { + return dataInfo; + } + + /** + * Gets the description + * + * @return the description + */ + public @Nullable Description getDescription() { + return description; + } + + /** + * Gets the direct remote number + * + * @return the direct remote number + */ + public @Nullable Integer getDirectRemoteNum() { + return directRemoteNum; + } + + /** + * Gets the display number + * + * @return the display number + */ + public @Nullable String getDispNum() { + return dispNum; + } + + /** + * Gets the dubbing information + * + * @return the dubbing information + */ + public @Nullable DubbingInfo getDubbingInfo() { + return dubbingInfo; + } + + /** + * Gets the duration + * + * @return the duration + */ + public @Nullable Duration getDuration() { + return duration; + } + + /** + * Gets the event ID + * + * @return the event ID + */ + public @Nullable String getEventId() { + return eventId; + } + + /** + * Gets the file number + * + * @return the file number + */ + public @Nullable String getFileNo() { + return fileNo; + } + + /** + * Gets the file size (in bytes) + * + * @return the file size (in bytes) + */ + public @Nullable Integer getFileSizeByte() { + return fileSizeByte; + } + + /** + * Gets the folder number + * + * @return the folder number + */ + public @Nullable String getFolderNo() { + return folderNo; + } + + /** + * Gets the genre + * + * @return the genre + */ + public @Nullable String getGenre() { + return genre; + } + + /** + * Gets the global playback count + * + * @return the global playback count + */ + public @Nullable Integer getGlobalPlaybackCount() { + return globalPlaybackCount; + } + + /** + * Gets the group info + * + * @return the group info + */ + public @Nullable GroupInfo @Nullable [] getGroupInfo() { + return groupInfo; + } + + /** + * Gets the has resume + * + * @return the has resume + */ + public @Nullable String getHasResume() { + return hasResume; + } + + /** + * Gets the index + * + * @return the index + */ + public @Nullable Integer getIndex() { + return index; + } + + /** + * Gets the 3D setting + * + * @return the 3D setting + */ + public @Nullable String is3D() { + return is3D; + } + + /** + * Gets the 4K setting + * + * @return the 4K setting + */ + public @Nullable String is4K() { + return is4K; + } + + /** + * Whether the content has already played + * + * @return whether the content has already played + */ + public @Nullable String isAlreadyPlayed() { + return isAlreadyPlayed; + } + + /** + * Whether the content auto deletes + * + * @return whether the content auto deletes + */ + public @Nullable String isAutoDelete() { + return isAutoDelete; + } + + /** + * Whether the content is browesable + * + * @return whether the content is browesable + */ + public @Nullable String isBrowsable() { + return isBrowsable; + } + + /** + * Whether the content is new + * + * @return whether the content is new + */ + public @Nullable String isNew() { + return isNew; + } + + /** + * Whether the content is playable + * + * @return whether the content is playable + */ + public @Nullable String isPlayable() { + return isPlayable; + } + + /** + * Whether the content is a play list + * + * @return whether the content is a play list + */ + public @Nullable String isPlaylist() { + return isPlaylist; + } + + /** + * Whether the content is protected + * + * @return Whether the content is protected + */ + public @Nullable String isProtected() { + return isProtected; + } + + /** + * Whether the content is a sound photo + * + * @return whether the content is a sound photo + */ + public @Nullable String isSoundPhoto() { + return isSoundPhoto; + } + + /** + * Gets the media type + * + * @return the media type + */ + public @Nullable String getMediaType() { + return mediaType; + } + + /** + * Gets the original display number + * + * @return the original display number + */ + public @Nullable String getOriginalDispNum() { + return originalDispNum; + } + + /** + * Gets the output + * + * @return the output + */ + public @Nullable String getOutput() { + return output; + } + + /** + * Gets the parental information + * + * @return the parental information + */ + public @Nullable ParentalInfo @Nullable [] getParentalInfo() { + return parentalInfo; + } + + /** + * Gets the parent index + * + * @return the parent index + */ + public @Nullable Integer getParentIndex() { + return parentIndex; + } + + /** + * Gets the parent URI + * + * @return the parent URI + */ + public @Nullable String getParentUri() { + return parentUri; + } + + /** + * Gets the play list info + * + * @return the play list info + */ + public @Nullable PlaylistInfo @Nullable [] getPlaylistInfo() { + return playlistInfo; + } + + /** + * Gets the play list name + * + * @return the play list name + */ + public @Nullable String getPlaylistName() { + return playlistName; + } + + /** + * Gets the play speed + * + * @return the play speed + */ + public @Nullable Speed getPlaySpeed() { + return playSpeed; + } + + /** + * Gets the podcast name + * + * @return the podcast name + */ + public @Nullable String getPodcastName() { + return podcastName; + } + + /** + * Gets the position + * + * @return the position + */ + public @Nullable Position getPosition() { + return position; + } + + /** + * Gets the product ID + * + * @return the product ID + */ + public @Nullable String getProductID() { + return productID; + } + + /** + * Gets the program media type + * + * @return the program media type + */ + public @Nullable String getProgramMediaType() { + return programMediaType; + } + + /** + * Gets the program number + * + * @return the program number + */ + public @Nullable Integer getProgramNum() { + return programNum; + } + + /** + * Gets the program service type + * + * @return the program service type + */ + public @Nullable String getProgramServiceType() { + return programServiceType; + } + + /** + * Gets the program title + * + * @return the program title + */ + public @Nullable String getProgramTitle() { + return programTitle; + } + + /** + * Gets the recording information + * + * @return the recording information + */ + public @Nullable RecordingInfo getRecordingInfo() { + return recordingInfo; + } + + /** + * Gets the remote play type + * + * @return the remote play type + */ + public @Nullable String getRemotePlayType() { + return remotePlayType; + } + + /** + * Gets the repeat type + * + * @return the repeat type + */ + public @Nullable String getRepeatType() { + return repeatType; + } + + /** + * Gets the service + * + * @return the service + */ + public @Nullable String getService() { + return service; + } + + /** + * Gets the size (in MB) + * + * @return the size (in MB) + */ + public @Nullable Integer getSizeMB() { + return sizeMB; + } + + /** + * Gets the source + * + * @return the source + */ + public @Nullable String getSource() { + return source; + } + + /** + * Gets the source label + * + * @return the source label + */ + public @Nullable String getSourceLabel() { + return sourceLabel; + } + + /** + * Gets the start date time + * + * @return the start date time + */ + public @Nullable String getStartDateTime() { + return startDateTime; + } + + /** + * Gets the state information + * + * @return the state information + */ + public @Nullable StateInfo getStateInfo() { + return stateInfo; + } + + /** + * Gets the storage uri + * + * @return the storage uri + */ + public @Nullable String getStorageUri() { + return storageUri; + } + + /** + * Gets the subtitle info + * + * @return the subtitle info + */ + public @Nullable SubtitleInfo @Nullable [] getSubtitleInfo() { + return subtitleInfo; + } + + /** + * Gets the content sync priority + * + * @return the content sync priority + */ + public @Nullable String getSyncContentPriority() { + return syncContentPriority; + } + + /** + * Gets the content title + * + * @return the content title + */ + public @Nullable String getTitle() { + return title; + } + + /** + * Gets the total count + * + * @return the total count + */ + public @Nullable Integer getTotalCount() { + return totalCount; + } + + /** + * Gets the channel triplet string + * + * @return the channel triplet string + */ + public @Nullable String getTripletStr() { + return tripletStr; + } + + /** + * Gets the content URI + * + * @return the content URI + */ + public @Nullable String getUri() { + return uri; + } + + /** + * Gets the content user flag + * + * @return the content user flag + */ + public @Nullable Boolean getUserContentFlag() { + return userContentFlag; + } + + /** + * Gets the video info + * + * @return the video info + */ + public @Nullable VideoInfo @Nullable [] getVideoInfo() { + return videoInfo; + } + + /** + * Gets the content visibility + * + * @return the content visibility + */ + public @Nullable Visibility getVisibility() { + return visibility; + } + + @Override + public String toString() { + return "ContentListResult_1_5 [albumName=" + albumName + ", applicationName=" + applicationName + ", artist=" + + artist + ", audioInfo=" + Arrays.toString(audioInfo) + ", bivlInfo=" + bivlInfo + ", broadcastFreq=" + + broadcastFreq + ", broadcastGenreInfo=" + Arrays.toString(broadcastGenreInfo) + ", channelName=" + + channelName + ", chapterCount=" + chapterCount + ", chapterIndex=" + chapterIndex + ", clipCount=" + + clipCount + ", content=" + content + ", contentKind=" + contentKind + ", contentType=" + contentType + + ", createdTime=" + createdTime + ", dabInfo=" + dabInfo + ", dataInfo=" + dataInfo + ", description=" + + description + ", directRemoteNum=" + directRemoteNum + ", dispNum=" + dispNum + ", dubbingInfo=" + + dubbingInfo + ", duration=" + duration + ", eventId=" + eventId + ", fileNo=" + fileNo + + ", fileSizeByte=" + fileSizeByte + ", folderNo=" + folderNo + ", genre=" + genre + + ", globalPlaybackCount=" + globalPlaybackCount + ", groupInfo=" + Arrays.toString(groupInfo) + + ", hasResume=" + hasResume + ", index=" + index + ", is3D=" + is3D + ", is4K=" + is4K + + ", isAlreadyPlayed=" + isAlreadyPlayed + ", isAutoDelete=" + isAutoDelete + ", isBrowsable=" + + isBrowsable + ", isNew=" + isNew + ", isPlayable=" + isPlayable + ", isPlaylist=" + isPlaylist + + ", isProtected=" + isProtected + ", isSoundPhoto=" + isSoundPhoto + ", mediaType=" + mediaType + + ", originalDispNum=" + originalDispNum + ", output=" + output + ", parentalInfo=" + + Arrays.toString(parentalInfo) + ", parentIndex=" + parentIndex + ", parentUri=" + parentUri + + ", playlistInfo=" + Arrays.toString(playlistInfo) + ", playlistName=" + playlistName + ", playSpeed=" + + playSpeed + ", podcastName=" + podcastName + ", position=" + position + ", productID=" + productID + + ", programMediaType=" + programMediaType + ", programNum=" + programNum + ", programServiceType=" + + programServiceType + ", programTitle=" + programTitle + ", recordingInfo=" + recordingInfo + + ", remotePlayType=" + remotePlayType + ", repeatType=" + repeatType + ", service=" + service + + ", sizeMB=" + sizeMB + ", source=" + source + ", sourceLabel=" + sourceLabel + ", startDateTime=" + + startDateTime + ", stateInfo=" + stateInfo + ", storageUri=" + storageUri + ", subtitleInfo=" + + Arrays.toString(subtitleInfo) + ", syncContentPriority=" + syncContentPriority + ", title=" + title + + ", totalCount=" + totalCount + ", tripletStr=" + tripletStr + ", uri=" + uri + ", userContentFlag=" + + userContentFlag + ", videoInfo=" + Arrays.toString(videoInfo) + ", visibility=" + visibility + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Count.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Count.java new file mode 100644 index 0000000000000..f098228e21776 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Count.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a count and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Count { + + /** The count */ + private final int count; + + /** + * Instantiates a new count from the specified count + * + * @param count the count + */ + public Count(final int count) { + this.count = count; + } + + /** + * Gets the count + * + * @return the count + */ + public int getCount() { + return count; + } + + @Override + public String toString() { + return "Count [count=" + count + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentExternalInputsStatus_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentExternalInputsStatus_1_0.java new file mode 100644 index 0000000000000..c24405a99986b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentExternalInputsStatus_1_0.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents a current external input status + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class CurrentExternalInputsStatus_1_0 { + + /** The uri identifying the input */ + private @Nullable String uri; + + /** The title (name) of the input */ + private @Nullable String title; + + /** The connection status */ + private @Nullable Boolean connection; + + /** The label of the input */ + private @Nullable String label; + + /** The icon for the input */ + private @Nullable String icon; + + /** + * Constructor used for deserialization only + */ + public CurrentExternalInputsStatus_1_0() { + } + + /** + * Gets the uri of the input + * + * @return the uri of the input + */ + public @Nullable String getUri() { + return uri; + } + + /** + * Gets the title of the input + * + * @return the title of the input + */ + public @Nullable String getTitle() { + return title; + } + + /** + * Get's the title (or label) for the input or the default value if none + * + * @param defaultValue the non-null, non-empty default value + * @return a non-null, non-empty title + */ + public String getTitle(final String defaultValue) { + SonyUtil.validateNotEmpty(defaultValue, "defaultValue cannot be empty"); + + final String titleOrLabel = SonyUtil.defaultIfEmpty(title, label == null ? "" : label); + return SonyUtil.defaultIfEmpty(titleOrLabel, defaultValue); + } + + /** + * Returns the connection status + * + * @return true, if it is a connection + */ + public @Nullable Boolean isConnection() { + return connection; + } + + /** + * Gets the label of the input + * + * @return the label of the input + */ + public @Nullable String getLabel() { + return label; + } + + /** + * Gets the icon of the input + * + * @return the icon of the input + */ + public @Nullable String getIcon() { + return icon; + } + + @Override + public String toString() { + return "CurrentExternalInputsStatus_1_0 [uri=" + uri + ", title=" + title + ", connection=" + connection + + ", label=" + label + ", icon=" + icon + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentExternalInputsStatus_1_1.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentExternalInputsStatus_1_1.java new file mode 100644 index 0000000000000..e91b6f9c0a963 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentExternalInputsStatus_1_1.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents a current external input status + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class CurrentExternalInputsStatus_1_1 extends CurrentExternalInputsStatus_1_0 { + + /** The uri identifying the input */ + private @Nullable String status; + + /** + * Constructor used for deserialization only + */ + public CurrentExternalInputsStatus_1_1() { + super(); + } + + /** + * Gets the status of the input + * + * @return the status of the input + */ + public @Nullable String getStatus() { + return status; + } + + @Override + public String toString() { + return "CurrentExternalInputsStatus_1_1 [uri=" + getUri() + ", title=" + getTitle() + ", connection=" + + isConnection() + ", label=" + getLabel() + ", icon=" + getIcon() + ", status=" + status + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentExternalTerminalsStatus_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentExternalTerminalsStatus_1_0.java new file mode 100644 index 0000000000000..5559ac9078a61 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentExternalTerminalsStatus_1_0.java @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents a current external terminal status + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class CurrentExternalTerminalsStatus_1_0 { + + public static final String META_ZONEOUTPUT = "meta:zone:output"; + + /** The status of the terminal */ + private @Nullable String active; + + /** The connection status */ + private @Nullable String connection; + + /** The icon for the terminal */ + private @Nullable String iconUrl; + + /** The label of the terminal */ + private @Nullable String label; + + /** The meta identifying information on the terminal */ + private @Nullable String meta; + + /** The outputs associated with this terminal (usually an input) */ + private @Nullable String @Nullable [] outputs; + + /** The title (name) of the terminal */ + private @Nullable String title; + + /** The uri identifying the terminal */ + private @Nullable String uri; + + /** + * Constructor used for deserialization only + */ + public CurrentExternalTerminalsStatus_1_0() { + } + + /** + * Constructs the external status from the uri/title + * + * @param uri a non-null, non-empty URI + * @param title a non-null, non-empty title + */ + public CurrentExternalTerminalsStatus_1_0(final String uri, final String title) { + SonyUtil.validateNotEmpty(uri, "uri cannot be empty"); + SonyUtil.validateNotEmpty(title, "title cannot be empty"); + + this.uri = uri; + this.title = title; + this.meta = META_ZONEOUTPUT; + } + + /** + * Gets the status of the terminal + * + * @return the status of the terminal + */ + public @Nullable String getActive() { + return active; + } + + /** + * Gets the connection of the terminal + * + * @return the connection of the terminal + */ + public @Nullable String getConnection() { + return connection; + } + + /** + * Gets the icon of the terminal + * + * @return the icon of the terminal + */ + public @Nullable String getIconUrl() { + return iconUrl; + } + + /** + * Gets the label of the terminal + * + * @return the label of the terminal + */ + public @Nullable String getLabel() { + return label; + } + + public @Nullable String getMeta() { + return meta; + } + + public @Nullable String @Nullable [] getOutputs() { + return outputs; + } + + /** + * Gets the title of the terminal + * + * @return the title of the terminal + */ + public @Nullable String getTitle() { + return title; + } + + /** + * Get's the title (or label) for the input or the default value if none + * + * @param defaultValue the non-null, non-empty default value + * @return a non-null, non-empty title + */ + public String getTitle(final String defaultValue) { + SonyUtil.validateNotEmpty(defaultValue, "defaultValue cannot be empty"); + final String titleOrLabel = SonyUtil.defaultIfEmpty(title, label); + return SonyUtil.defaultIfEmpty(titleOrLabel, defaultValue); + } + + /** + * Gets the uri of the terminal + * + * @return the uri of the terminal + */ + public @Nullable String getUri() { + return uri; + } + + public boolean isOutput() { + return META_ZONEOUTPUT.equalsIgnoreCase(meta) || (uri != null && uri.startsWith(Scheme.EXT_OUTPUT)); + } + + @Override + public String toString() { + return "CurrentExternalTerminalsStatus_1_0 [uri=" + uri + ", title=" + title + ", connection=" + connection + + ", label=" + label + ", iconUrl=" + iconUrl + ", active=" + active + ", outputs=" + + Arrays.toString(outputs) + ", meta=" + meta + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentTime.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentTime.java new file mode 100644 index 0000000000000..d53c702600fab --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentTime.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * This class represents a count and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class CurrentTime { + + /** The date time */ + private final ZonedDateTime dateTime; + + /** + * Instantiates a new current time + * + * @param results the results + */ + public CurrentTime(final ScalarWebResult results) { + Objects.requireNonNull(results, "results cannot be null"); + + final JsonArray resultArray = results.getResults(); + + if (resultArray == null || resultArray.size() != 1) { + throw new JsonParseException("Result should only have a single element: " + resultArray); + } + + final JsonElement elm = resultArray.get(0); + + if (elm.isJsonPrimitive()) { + // 2017-02-01T15:07:11-0500 + final String dateString = elm.getAsString(); + final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); + dateTime = ZonedDateTime.parse(dateString, dtf); + } else if (elm.isJsonObject()) { + final JsonObject obj = elm.getAsJsonObject(); + + String myDateTime = null; + Integer myTimeZoneOffsetMinute = null; + Integer myDstOffsetMinute = null; + + final JsonElement dateTimeElm = obj.get("dateTime"); + if (dateTimeElm != null) { + myDateTime = dateTimeElm.getAsString(); + } + + final JsonElement timeZoneElm = obj.get("timeZoneOffsetMinute"); + if (timeZoneElm != null && timeZoneElm.isJsonPrimitive() && timeZoneElm.getAsJsonPrimitive().isNumber()) { + myTimeZoneOffsetMinute = timeZoneElm.getAsInt(); + } + + final JsonElement dstElm = obj.get("dstOffsetMinute"); + if (dstElm != null && dstElm.isJsonPrimitive() && dstElm.getAsJsonPrimitive().isNumber()) { + myDstOffsetMinute = dstElm.getAsInt(); + } + + if (myDateTime == null || myDateTime.isEmpty()) { + throw new JsonParseException("'dateTime' property was not found: " + obj); + } + + if (myTimeZoneOffsetMinute == null) { + throw new JsonParseException("'timeZoneOffsetMinute' property was not found: " + obj); + } + + if (myDstOffsetMinute == null) { + throw new JsonParseException("'dstOffsetMinute' property was not found: " + obj); + } + + final int offsetMintues = myTimeZoneOffsetMinute + myDstOffsetMinute; + + if (myDateTime != null) { + dateTime = LocalDateTime.parse(myDateTime) + .atZone(ZoneOffset.ofHoursMinutes(offsetMintues / 60, offsetMintues % 60)); + } else { + // should not happen + dateTime = ZonedDateTime.now(); + } + } else { + throw new JsonParseException("Unknown result element: " + elm); + } + } + + /** + * Gets the date time + * + * @return the date time + */ + public ZonedDateTime getDateTime() { + return dateTime; + } + + @Override + public String toString() { + return "CurrentTime [dateTime=" + dateTime + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentValue.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentValue.java new file mode 100644 index 0000000000000..36df13ad2c282 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/CurrentValue.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents a current value and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class CurrentValue { + /** The current value */ + private @Nullable String currentValue; + + /** + * Constructor used for deserialization only + */ + public CurrentValue() { + } + + /** + * Gets the current value + * + * @return the current value + */ + public @Nullable String getCurrentValue() { + return currentValue; + } + + @Override + public String toString() { + return "CurrentValue [currentValue=" + currentValue + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DabInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DabInfo.java new file mode 100644 index 0000000000000..11f8f44753186 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DabInfo.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The digital audio broadcasting (DAB) information class used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class DabInfo { + /** The DAB component label */ + private @Nullable String componentLabel; + + /** The DAB dynamic label */ + private @Nullable String dynamicLabel; + + /** The DAB ensemble label */ + private @Nullable String ensembleLabel; + + /** The DAB service label */ + private @Nullable String serviceLabel; + + /** + * Constructor used for deserialization only + */ + public DabInfo() { + } + + /** + * Returns the DAB component label + * + * @return the component label + */ + public @Nullable String getComponentLabel() { + return componentLabel; + } + + /** + * Returns the DAB dynamic label + * + * @return the dynamic label + */ + public @Nullable String getDynamicLabel() { + return dynamicLabel; + } + + /** + * Returns the DAB ensemble label + * + * @return the ensemble label + */ + public @Nullable String getEnsembleLabel() { + return ensembleLabel; + } + + /** + * Returns the DAB service label + * + * @return the service label + */ + public @Nullable String getServiceLabel() { + return serviceLabel; + } + + @Override + public String toString() { + return "DabInfo [componentLabel=" + componentLabel + ", dynamicLabel=" + dynamicLabel + ", ensembleLabel=" + + ensembleLabel + ", serviceLabel=" + serviceLabel + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DataBroadcastInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DataBroadcastInfo.java new file mode 100644 index 0000000000000..6e7b3caf890e8 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DataBroadcastInfo.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a broadcast data information + * + * TODO: still looking for an example of this.. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class DataBroadcastInfo { + /** + * Constructor used for deserialization only + */ + public DataBroadcastInfo() { + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DeleteContent.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DeleteContent.java new file mode 100644 index 0000000000000..49aa6f52610e3 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DeleteContent.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the request to delete content and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class DeleteContent { + + /** The uri that identifies the content to delete */ + private final String uri; + + /** + * Instantiates a new delete content + * + * @param uri the non-null, non-empty uri + */ + public DeleteContent(final String uri) { + SonyUtil.validateNotEmpty(uri, "Uri cannot be empty"); + this.uri = uri; + } + + @Override + public String toString() { + return "DeleteContent [uri=" + uri + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DeleteProtection.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DeleteProtection.java new file mode 100644 index 0000000000000..cb2ea75971c69 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DeleteProtection.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the request to delete protection and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class DeleteProtection { + + /** The uri that identifies the resource to protect */ + private final String uri; + + /** True if should be protected */ + private final boolean isProtected; + + /** + * Instantiates a new delete protection request + * + * @param uri the non-null, non-empty uri identifying the result + * @param isProtected whether it should be protected + */ + public DeleteProtection(final String uri, final boolean isProtected) { + SonyUtil.validateNotEmpty(uri, "uri cannot be empty"); + this.uri = uri; + this.isProtected = isProtected; + } + + @Override + public String toString() { + return "DeleteProtection [uri=" + uri + ", isProtected=" + isProtected + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Description.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Description.java new file mode 100644 index 0000000000000..0073f6c754fb0 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Description.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a content description + * + * TODO: still looking for an example of this.. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Description { + /** + * Constructor used for deserialization only + */ + public Description() { + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DeviceMode.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DeviceMode.java new file mode 100644 index 0000000000000..90670a77c2e25 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DeviceMode.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the current device mode and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class DeviceMode { + + /** Whether the device is on or not */ + private boolean isOn; + + /** + * Constructor used for deserialization only + */ + public DeviceMode() { + } + + /** + * Checks if the device is on or not + * + * @return true, if on - false otherwise + */ + public boolean isOn() { + return isOn; + } + + @Override + public String toString() { + return "DeviceMode [isOn=" + isOn + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DubbingInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DubbingInfo.java new file mode 100644 index 0000000000000..1d3a212fa334e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/DubbingInfo.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a dubbing information + * + * TODO: still looking for an example of this.. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class DubbingInfo { + /** + * Constructor used for deserialization only + */ + public DubbingInfo() { + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Duration.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Duration.java new file mode 100644 index 0000000000000..77f676a6c9594 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Duration.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the content duration + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Duration { + /** Duration in seconds */ + private @Nullable Integer seconds; + + /** Duration in milliseconds */ + private @Nullable Integer millseconds; + + /** + * Constructor used for deserialization only + */ + public Duration() { + } + + /** + * Gets the duration in seconds + * + * @return duration in seconds + */ + public @Nullable Integer getSeconds() { + return seconds; + } + + /** + * Gets duration in milliseconds + * + * @return duration in milliseconds + */ + public @Nullable Integer getMillseconds() { + return millseconds; + } + + @Override + public String toString() { + return "Duration [seconds=" + seconds + ", millseconds=" + millseconds + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Enabled.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Enabled.java new file mode 100644 index 0000000000000..4ab5f21e5aeb0 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Enabled.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents an enable request and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Enabled { + /** Whether enabling is enabled */ + private final boolean enabled; + + /** + * Creates the enabling from the passed parameter + * + * @param enabled true if enabled, false otherwise + */ + public Enabled(final boolean enabled) { + super(); + this.enabled = enabled; + } + + @Override + public String toString() { + return "Enabled [enabled=" + enabled + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSetting.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSetting.java new file mode 100644 index 0000000000000..03a2791fe09ca --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSetting.java @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents information about a specific sony general setting. Sony has created this as a simplified way of + * representing and setting various settings (sound, video, etc) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class GeneralSetting { + /** A boolean setting target */ + public static final String BOOLEANTARGET = "booleanTarget"; + + /** A double setting target */ + public static final String DOUBLETARGET = "doubleNumberTarget"; + + /** An enum setting target */ + public static final String ENUMTARGET = "enumTarget"; + + /** An integer setting target */ + public static final String INTEGERTARGET = "integerTarget"; + + /** A string setting target */ + public static final String STRINGTARGET = "stringTarget"; + + /** The DEFAULT value for boolean ON */ + public static final String DEFAULTON = "on"; + + /** The DEFAULT value for boolean OFF */ + public static final String DEFAULTOFF = "off"; + + /** The slider deviceUI type for a setting */ + public static final String SLIDER = "slider"; + + /** A picker deviceUI type for a setting */ + public static final String PICKER = "picker"; + + /** The various constants for sound settings */ + public static final String SOUNDSETTING_SPEAKER = "speaker"; + public static final String SOUNDSETTING_SPEAKERHDMI = "speaker_hdmi"; + public static final String SOUNDSETTING_HDMI = "hdmi"; + public static final String SOUNDSETTING_AUDIOSYSTEM = "audioSystem"; + + /** Whether the setting is currently available */ + private @Nullable Boolean isAvailable; + + /** The current value of the setting */ + private @Nullable String currentValue; + + /** The target of the setting */ + private @Nullable String target; + + /** The title of the setting */ + private @Nullable String title; + + /** The title text ID of the setting */ + private @Nullable String titleTextID; + + /** The type of setting (boolean, etc) */ + private @Nullable String type; + + /** The device UI info (picker/slider) */ + private @Nullable String deviceUIInfo; + + /** The URI the setting may apply to */ + private @Nullable String uri; + + /** The candidates for the setting (think enums or min/max/step values) */ + private @Nullable List<@Nullable GeneralSettingsCandidate> candidate; + + /** + * Constructor used for deserialization only + */ + public GeneralSetting() { + } + + /** + * Constructs a setting form the parameters + * + * @param target a non-null, non-empty target + * @param uri a possibly null, possibly empty uri + * @param type a possibly null, possibly empty type + * @param currentValue a possibly null, possibly empty current value + */ + public GeneralSetting(final String target, @Nullable final String uri, @Nullable final String type, + @Nullable final String currentValue) { + SonyUtil.validateNotEmpty(target, "target cannot be empty"); + this.target = target; + this.uri = uri; + this.type = type; + this.currentValue = currentValue; + } + + /** + * Whether the setting is currently available + * + * @return true if available, false otherwise + */ + public boolean isAvailable() { + return isAvailable == null || Boolean.TRUE.equals(isAvailable); + } + + /** + * Gets the current setting value + * + * @return the curent setting value + */ + public @Nullable String getCurrentValue() { + return currentValue; + } + + /** + * The setting's target + * + * @return the setting's target + */ + public @Nullable String getTarget() { + return target; + } + + /** + * The setting's title + * + * @return the setting's title + */ + public @Nullable String getTitle() { + return title; + } + + /** + * The title text identifier + * + * @return the title text identifier + */ + public @Nullable String getTitleTextID() { + return titleTextID; + } + + /** + * The setting type + * + * @return the setting type + */ + public @Nullable String getType() { + return type; + } + + /** + * The setting's UI information + * + * @return the setting UI information + */ + public @Nullable String getDeviceUIInfo() { + return deviceUIInfo; + } + + /** + * The uri to apply the setting to + * + * @return the uri to apply the setting to + */ + public @Nullable String getUri() { + return uri; + } + + /** + * Whether the setting's UI is a slider + * + * @return true if a slider, false otherwise + */ + public boolean isUiSlider() { + return deviceUIInfo != null && deviceUIInfo.contains(SLIDER); + } + + /** + * Whether the setting's UI is a picker + * + * @return true if a picker, false otherwise + */ + public boolean isUiPicker() { + return deviceUIInfo != null && deviceUIInfo.contains(PICKER); + } + + /** + * Get's the candidates for the setting + * + * @return the setting's candidates + */ + public @Nullable List<@Nullable GeneralSettingsCandidate> getCandidate() { + return candidate; + } + + @Override + public String toString() { + return "SoundSetting [isAvailable=" + isAvailable + ", currentValue=" + currentValue + ", target=" + target + + ", title=" + title + ", titleTextID=" + titleTextID + ", type=" + type + ", deviceUIInfo=" + + deviceUIInfo + ", candidate=" + candidate + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSettingsCandidate.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSettingsCandidate.java new file mode 100644 index 0000000000000..03facfccc1d28 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSettingsCandidate.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class describes the candidate parameters for a general setting. Please note that only specific fields are used + * based on the overall setting (example: only a double slider will have min/max/step values whereas an enum will have + * values) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class GeneralSettingsCandidate { + /** Whether the candidate is currently available */ + private @Nullable Boolean isAvailable; + + /** The minimum value */ + private @Nullable Double min; + + /** The maximum value */ + private @Nullable Double max; + + /** The step of the value */ + private @Nullable Double step; + + /** The candidate title */ + private @Nullable String title; + + /** The candidate title text id */ + private @Nullable String titleTextID; + + /** The value of the candidate */ + private @Nullable String value; + + /** + * Constructor used for deserialization only + */ + public GeneralSettingsCandidate() { + } + + /** + * Whether the candidate is available + * + * @return true if available, false otherwise + */ + public boolean isAvailable() { + return isAvailable == null || Boolean.TRUE.equals(isAvailable); + } + + /** + * Gets the minimum value + * + * @return the minimum value + */ + public @Nullable Double getMin() { + return min; + } + + /** + * Gets the maximum value + * + * @return the maximum value + */ + public @Nullable Double getMax() { + return max; + } + + /** + * Gets the step value + * + * @return the step value + */ + public @Nullable Double getStep() { + return step; + } + + /** + * Gets the title + * + * @return the title + */ + public @Nullable String getTitle() { + return title; + } + + /** + * Gets the title text identifier + * + * @return the title text identifier + */ + public @Nullable String getTitleTextID() { + return titleTextID; + } + + /** + * Gets the value + * + * @return the value + */ + public @Nullable String getValue() { + return value; + } + + @Override + public String toString() { + return "GeneralSettingsCandidate [isAvailable=" + isAvailable + ", max=" + max + ", min=" + min + ", step=" + + step + ", title=" + title + ", titleTextID=" + titleTextID + ", value=" + value + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSettingsRequest.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSettingsRequest.java new file mode 100644 index 0000000000000..74e978d89ecfe --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSettingsRequest.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents a general setting request. Please note the API supports setting multiple values at a time but + * we limit this to one setting at a time + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class GeneralSettingsRequest { + /** The list of settings to set */ + private final List settings; + + /** + * Constructs the setting request from the target/value/uri + * + * @param target a non-null, non-empty target + * @param value a non-null, possibly empty value + * @param uri a possibly null, possibly empty URI target + */ + public GeneralSettingsRequest(final String target, final String value, final @Nullable String uri) { + SonyUtil.validateNotEmpty(target, "target cannot be empty"); + Objects.requireNonNull(value, "value cannot be null"); + settings = Collections.singletonList(new Setting(target, value, uri)); + } + + @Override + public String toString() { + return "GeneralSettingsRequest [settings=" + settings + "]"; + } + + /** + * Represents the structure of an individual setting + */ + @NonNullByDefault + private class Setting { + /** The setting target */ + private final String target; + + /** the setting value */ + private final String value; + + /** the setting uri */ + private final @Nullable String uri; + + /** + * Constructs the setting from the target/value + * + * @param target a non-null, non-empty target + * @param value a non-null, possibly empty value + * @param uri a possibly null uri + */ + private Setting(final String target, final String value, final @Nullable String uri) { + SonyUtil.validateNotEmpty(target, "target cannot be empty"); + Objects.requireNonNull(value, "value cannot be null"); + this.target = target; + this.value = value; + this.uri = uri; + } + + @Override + public String toString() { + return "Setting [target=" + target + ", uri=" + uri + ", value=" + value + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSettings_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSettings_1_0.java new file mode 100644 index 0000000000000..34ba6aa757ca3 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GeneralSettings_1_0.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.gson.GsonUtilities; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +/** + * This class represents all the various setting values. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class GeneralSettings_1_0 { + /** The general settings */ + private final List generalSettings = new ArrayList<>(); + + /** + * Constructor for a custom deserialization of the results + * + * @param results a non-null results + */ + public GeneralSettings_1_0(final ScalarWebResult results) { + Objects.requireNonNull(results, "results cannot be null"); + + final JsonArray rsts = results.getResults(); + if (rsts == null) { + throw new JsonParseException("No results to deserialize"); + } + + final Gson gson = GsonUtilities.getApiGson(); + + for (final JsonElement elm : rsts) { + if (elm.isJsonArray()) { + for (final JsonElement arr : elm.getAsJsonArray()) { + if (arr.isJsonObject()) { + generalSettings.add(Objects.requireNonNull(gson.fromJson(arr, GeneralSetting.class))); + } else { + throw new JsonParseException("General Settings entry not an object: " + arr); + } + } + } + } + } + + /** + * Gets the settings + * + * @return a non-null, possibly empty list of settings + */ + public List getSettings() { + return generalSettings; + } + + /** + * Returns the settings for a given target + * + * @param target a non-null, non-empty target + * @return a non-null, possibly empty stream of general settings + */ + public Stream getSettings(final String target) { + SonyUtil.validateNotEmpty(target, "target cannot be empty"); + return generalSettings.stream().filter(s -> s.getTarget() != null && s.getTarget().equalsIgnoreCase(target)); + } + + @Override + public String toString() { + return "SpeakerSettings_1_1 [settings=" + generalSettings + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GroupInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GroupInfo.java new file mode 100644 index 0000000000000..b1f8df2d95546 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/GroupInfo.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the group information + * + * TODO: still looking for an example of this.. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class GroupInfo { + /** + * Constructor used for deserialization only + */ + public GroupInfo() { + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/InterfaceInformation.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/InterfaceInformation.java new file mode 100644 index 0000000000000..c78ba7d04ae7c --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/InterfaceInformation.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the request to get interface information and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class InterfaceInformation { + + /** The product category */ + private @Nullable String productCategory; + + /** The product name */ + private @Nullable String productName; + + /** The model name */ + private @Nullable String modelName; + + /** The server name */ + private @Nullable String serverName; + + /** The interface version */ + private @Nullable String interfaceVersion; + + /** + * Constructor used for deserialization only + */ + public InterfaceInformation() { + } + + /** + * Gets the product category + * + * @return the product category + */ + public @Nullable String getProductCategory() { + return productCategory; + } + + /** + * Gets the product name + * + * @return the product name + */ + public @Nullable String getProductName() { + return productName; + } + + /** + * Gets the model name + * + * @return the model name + */ + public @Nullable String getModelName() { + return modelName; + } + + /** + * Gets the server name + * + * @return the server name + */ + public @Nullable String getServerName() { + return serverName; + } + + /** + * Gets the interface version + * + * @return the interface version + */ + public @Nullable String getInterfaceVersion() { + return interfaceVersion; + } + + @Override + public String toString() { + return "InterfaceInformation [productCategory=" + productCategory + ", productName=" + productName + + ", modelName=" + modelName + ", serverName=" + serverName + ", interfaceVersion=" + interfaceVersion + + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Language.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Language.java new file mode 100644 index 0000000000000..1d47e0e28daef --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Language.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the request to set a language and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Language { + + /** The language */ + private final String language; + + /** + * Instantiates a new language + * + * @param language the non-null non-empty language + */ + public Language(final String language) { + SonyUtil.validateNotEmpty(language, "language cannot be empty"); + this.language = language; + } + + /** + * Gets the language. + * + * @return the language + */ + public String getLanguage() { + return language; + } + + @Override + public String toString() { + return "Language [language=" + language + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/LedIndicatorStatus.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/LedIndicatorStatus.java new file mode 100644 index 0000000000000..f2317ebc2aa2e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/LedIndicatorStatus.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the request to set the LED status and is used for serialization/deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class LedIndicatorStatus { + + /** The mode */ + private @Nullable String mode; + + /** The status */ + private @Nullable String status; + + /** + * Constructor used for deserialization only + */ + public LedIndicatorStatus() { + } + + /** + * Instantiates a new led indicator status. + * + * @param mode the non-null, non-empty mode + * @param status the non-null, non-empty status + */ + public LedIndicatorStatus(final String mode, final String status) { + SonyUtil.validateNotEmpty(mode, "mode cannot be empty"); + SonyUtil.validateNotEmpty(status, "status cannot be empty"); + this.mode = mode; + this.status = status; + } + + /** + * Gets the mode. + * + * @return the mode + */ + public @Nullable String getMode() { + return mode; + } + + /** + * Gets the status. + * + * @return the status + */ + public @Nullable String getStatus() { + return status; + } + + @Override + public String toString() { + return "LedIndicatorStatus [mode=" + mode + ", status=" + status + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/MethodTypes.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/MethodTypes.java new file mode 100644 index 0000000000000..764c6be262be9 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/MethodTypes.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +/** + * This class represents the request to get the method types and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class MethodTypes { + + /** The methods by method name (will be unmodifiable) */ + private final List methods; + + /** + * Instantiates a new method types + * + * @param results the non-null results + */ + public MethodTypes(final ScalarWebResult results) { + Objects.requireNonNull(results, "results cannot be null"); + + final List myMethods = new ArrayList(); + final JsonArray rsts = results.getResults(); + if (rsts == null) { + throw new JsonParseException("No results to deserialize"); + } + + for (final JsonElement elm : rsts) { + if (elm.isJsonArray()) { + final JsonArray elmArray = elm.getAsJsonArray(); + + if (elmArray.size() == 4) { + final String methodName = elmArray.get(0).getAsString(); + + // NOTE: some devices include whitespace in the response like: + // "{\"stuff\": \"blah\"}" vs "{\"stuff\":\"blah\"}" + // we remove the whitespace to make the parms/retvals consistent + // over all devices + final List parms = new ArrayList(); + final JsonElement parmElm = elmArray.get(1); + if (parmElm.isJsonArray()) { + for (final JsonElement parm : parmElm.getAsJsonArray()) { + parms.add(parm.getAsString().replaceAll("\\s", "")); + } + } else { + throw new JsonParseException("Method Parameters wasn't an array: " + elmArray); + } + + final List retVals = new ArrayList(); + final JsonElement valsElm = elmArray.get(2); + if (valsElm.isJsonArray()) { + for (final JsonElement retVal : valsElm.getAsJsonArray()) { + retVals.add(retVal.getAsString().replaceAll("\\s", "")); + } + } else { + throw new JsonParseException("Return Values wasn't an array: " + elmArray); + } + + final String methodVersion = elmArray.get(3).getAsString(); + + myMethods.add(new ScalarWebMethod(methodName, parms, retVals, methodVersion)); + } else { + throw new JsonParseException("MethodTypes array didn't have 4 elements: " + elmArray); + } + + } else { + throw new JsonParseException("MethodTypes had a non-array element: " + elm); + } + } + + methods = Collections.unmodifiableList(myMethods); + } + + /** + * Gets the methods supported + * + * @return the non-null, possibly empty unmodifiable map of methods by method name + */ + public List getMethods() { + return methods; + } + + @Override + public String toString() { + return "MethodTypes [methods=" + methods + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Mode.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Mode.java new file mode 100644 index 0000000000000..3b506e4294586 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Mode.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the request to set/get the current mode and is used for serialization/deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Mode { + + /** The mode */ + private @Nullable String mode; + + /** + * Constructor used for deserialization only + */ + public Mode() { + } + + /** + * Instantiates a new mode. + * + * @param mode the mode + */ + public Mode(final String mode) { + SonyUtil.validateNotEmpty(mode, "mode cannot be empty"); + this.mode = mode; + } + + /** + * Gets the mode. + * + * @return the mode + */ + public @Nullable String getMode() { + return mode; + } + + @Override + public String toString() { + return "Mode [mode=" + mode + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NetIf.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NetIf.java new file mode 100644 index 0000000000000..f05b2cd13a812 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NetIf.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the request to get the network interface information and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class NetIf { + + /** The network interface to get */ + private final String netif; + + /** + * Instantiates a new network interface request + * + * @param netif the non-null, non-empty interface name + */ + public NetIf(final String netif) { + SonyUtil.validateNotEmpty(netif, "netif cannot be empty"); + this.netif = netif; + } + + /** + * Gets the network interface name + * + * @return the non-null, non-empty network interface name + */ + public String getNetif() { + return netif; + } + + @Override + public String toString() { + return "NetIf [netif=" + netif + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NetworkSetting.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NetworkSetting.java new file mode 100644 index 0000000000000..a1a541e71d5d9 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NetworkSetting.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the network interface settings and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class NetworkSetting { + + /** The network interface name */ + private @Nullable String netif; + + /** The mac address. */ + private @Nullable String hwAddr; + + /** The ip v4 address */ + private @Nullable String ipAddrV4; + + /** The ip v6 address */ + private @Nullable String ipAddrV6; + + /** The network mask */ + private @Nullable String netmask; + + /** The gateway */ + private @Nullable String gateway; + + /** The list of DNS names */ + private @Nullable List<@Nullable String> dns; + + /** + * Constructor used for deserialization only + */ + public NetworkSetting() { + } + + /** + * Gets the network interface name + * + * @return the network interface name + */ + public @Nullable String getNetif() { + return netif; + } + + /** + * Gets the mac address + * + * @return the mac address + */ + public @Nullable String getHwAddr() { + return hwAddr; + } + + /** + * Gets the IP Address (v4) + * + * @return the IP Address (v4) + */ + public @Nullable String getIpAddrV4() { + return ipAddrV4; + } + + /** + * Gets the IP Address (v6) + * + * @return the IP Address (V6) + */ + public @Nullable String getIpAddrV6() { + return ipAddrV6; + } + + /** + * Gets the network mask + * + * @return the network mask + */ + public @Nullable String getNetmask() { + return netmask; + } + + /** + * Gets the gateway + * + * @return the gateway + */ + public @Nullable String getGateway() { + return gateway; + } + + /** + * Gets list of DNS names + * + * @return the non-null, possibly empty unmodifiable list of DNS names + */ + public List<@Nullable String> getDns() { + return dns == null ? Collections.emptyList() : Collections.unmodifiableList(dns); + } + + @Override + public String toString() { + return "NetworkSetting [netif=" + netif + ", hwAddr=" + hwAddr + ", ipAddrV4=" + ipAddrV4 + ", ipAddrV6=" + + ipAddrV6 + ", netmask=" + netmask + ", gateway=" + gateway + ", dns=" + dns + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Notification.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Notification.java new file mode 100644 index 0000000000000..46d3d0001e8c7 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Notification.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * The class represents a specific notification and is used for serialization/deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Notification { + /** The name of the notification */ + private @Nullable String name; + + /** The version of the notification */ + private @Nullable String version; + + /** + * Constructor used for deserialization only + */ + public Notification() { + } + + /** + * Constructs the notification from the name/version + * + * @param name a non-null, non-empty name + * @param version a non-null, non-empty version + */ + public Notification(final String name, final String version) { + SonyUtil.validateNotEmpty(name, "name cannot be empty"); + SonyUtil.validateNotEmpty(version, "version cannot be empty"); + + this.name = name; + this.version = version; + } + + /** + * Get's the name for this notification + * + * @return the name + */ + public @Nullable String getName() { + return name; + } + + /** + * Get's the version for this notification + * + * @return the version + */ + public @Nullable String getVersion() { + return version; + } + + @Override + public String toString() { + return "Notification [name=" + name + ", version=" + version + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Notifications.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Notifications.java new file mode 100644 index 0000000000000..aad185c18786d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Notifications.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents all the notifications and whether they are enabled or disabled + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Notifications { + /** The enabled notifications */ + private final @Nullable List enabled; + + /** The disabled notifications */ + private final @Nullable List disabled; + + /** Constructs an empty list of notifications */ + public Notifications() { + this.enabled = new ArrayList<>(); + this.disabled = new ArrayList<>(); + } + + /** + * Constructs the notification with a specific set enable/disabledf + * + * @param enabled a non-null, possibly empty list of enabled notifications + * @param disabled a non-null, possibly empty list of disabled notifications + */ + public Notifications(final List enabled, final List disabled) { + Objects.requireNonNull(enabled, "enabled cannot be null"); + Objects.requireNonNull(enabled, "disabled cannot be null"); + + // note: if empty - set to null. Sony has a bad habit of ignoring + // the request if one or the other array is an empty array (both can be empty however - go figure) + this.enabled = enabled.isEmpty() && !disabled.isEmpty() ? null : new ArrayList<>(enabled); + this.disabled = !enabled.isEmpty() && disabled.isEmpty() ? null : new ArrayList<>(disabled); + } + + /** + * This list of enabled notifications + * + * @return a non-null, unmodifiable list of notifications + */ + public List getEnabled() { + final List localEnabled = enabled; + return localEnabled == null ? Collections.emptyList() : Collections.unmodifiableList(localEnabled); + } + + /** + * This list of enabled notifications + * + * @return a non-null, unmodifiable list of notifications + */ + public List getDisabled() { + final List localDisabled = disabled; + return localDisabled == null ? Collections.emptyList() : Collections.unmodifiableList(localDisabled); + } + + /** + * Determines if the specified name is enabled (true) or not (false) + * + * @param name a non-null, non-empty name + * @return true if enabled, false otherwise + */ + public boolean isEnabled(final String name) { + SonyUtil.validateNotEmpty(name, "name cannot be empty"); + + final List localEnabled = enabled; + return localEnabled != null && localEnabled.stream().anyMatch(e -> name.equalsIgnoreCase(e.getName())); + } + + @Override + public String toString() { + return "Notifications [enabled=" + enabled + ", disabled=" + disabled + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NotifySettingUpdate.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NotifySettingUpdate.java new file mode 100644 index 0000000000000..1680c0ff3ee8b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NotifySettingUpdate.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents a notification of a general setting update (differs slightly from the GeneralSetting) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class NotifySettingUpdate { + /** Whether the setting is currently available */ + private @Nullable Boolean isAvailable; + + /** The title of the setting */ + private @Nullable String title; + + /** The title text ID of the setting */ + private @Nullable String titleTextID; + + /** The type of setting (boolean, etc) */ + private @Nullable String type; + + /** The device UI info (picker/slider) */ + private @Nullable String deviceUIInfo; + + /** The mapping update */ + private @Nullable NotifySettingUpdateApiMapping apiMappingUpdate; + + /** + * Constructor used for deserialization only + */ + public NotifySettingUpdate() { + } + + /** + * Whether the setting is currently available + * + * @return true if available, false otherwise + */ + public boolean isAvailable() { + return isAvailable == null || Boolean.TRUE.equals(isAvailable); + } + + /** + * The setting's title + * + * @return the setting's title + */ + public @Nullable String getTitle() { + return title; + } + + /** + * The title text identifier + * + * @return the title text identifier + */ + public @Nullable String getTitleTextID() { + return titleTextID; + } + + /** + * The setting type + * + * @return the setting type + */ + public @Nullable String getType() { + return type; + } + + /** + * The setting's UI information + * + * @return the setting UI information + */ + public @Nullable String getDeviceUIInfo() { + return deviceUIInfo; + } + + /** + * The api mapping update + * + * @return the apiMappingUpdate mapping update + */ + public @Nullable NotifySettingUpdateApiMapping getApiMappingUpdate() { + return apiMappingUpdate; + } + + @Override + public String toString() { + return "NotificationGeneralSetting [apiMappingUpdate=" + apiMappingUpdate + ", deviceUIInfo=" + deviceUIInfo + + ", isAvailable=" + isAvailable + ", title=" + title + ", titleTextID=" + titleTextID + ", type=" + + type + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NotifySettingUpdateApi.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NotifySettingUpdateApi.java new file mode 100644 index 0000000000000..f36b646fb8e69 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NotifySettingUpdateApi.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the API in a setting update notification + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class NotifySettingUpdateApi { + + /** The current value of the setting */ + private @Nullable String name; + + /** The target of the setting */ + private @Nullable String version; + + /** + * Constructor used for deserialization only + */ + public NotifySettingUpdateApi() { + } + + /** + * Get's the API name + * + * @return the API name + */ + public @Nullable String getName() { + return name; + } + + /** + * Get's the API version + * + * @return the API version + */ + public @Nullable String getVersion() { + return version; + } + + @Override + public String toString() { + return "NotifySettingUpdateApi [name=" + name + ", version=" + version + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NotifySettingUpdateApiMapping.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NotifySettingUpdateApiMapping.java new file mode 100644 index 0000000000000..963bd9ee8fb8e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/NotifySettingUpdateApiMapping.java @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents notification of an API mapping update (and includes some general setting information). + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class NotifySettingUpdateApiMapping { + /** + * Developer note: there are API information that I'm ignoring for now: + * "apiMappingUpdate": { + * "commandApi": { + * "name": "", + * "version": "" + * }, + * "currentValue": "track", + * "getApi": { + * "name": "getPlaybackModeSettings", + * "version": "1.0" + * }, + * "service": "avContent", + * "setApi": { + * "name": "setPlaybackModeSettings", + * "version": "1.0" + * }, + * "target": "repeatType", + * "targetSuppl": "", + * "uri": "storage:usb1" + * }, + */ + + /** The current value of the setting */ + private @Nullable String currentValue; + + /** The target of the setting */ + private @Nullable String target; + + /** The target supplement of the setting */ + private @Nullable String targetSuppl; + + /** The URI the setting may apply to */ + private @Nullable String uri; + + /** The service the setting may apply to */ + private @Nullable String service; + + /** The command API */ + private @Nullable NotifySettingUpdateApi commandApi; + + /** The get API */ + private @Nullable NotifySettingUpdateApi getApi; + + /** The set API */ + private @Nullable NotifySettingUpdateApi setApi; + + /** + * Constructor used for deserialization only + */ + public NotifySettingUpdateApiMapping() { + } + + /** + * Gets the current setting value + * + * @return the curent setting value + */ + public @Nullable String getCurrentValue() { + return currentValue; + } + + /** + * The setting's target + * + * @return the setting's target + */ + public @Nullable String getTarget() { + return target; + } + + /** + * The setting's target supplement + * + * @return the setting's target supplement + */ + public @Nullable String getTargetSuppl() { + return targetSuppl; + } + + /** + * The uri to apply the setting to + * + * @return the uri to apply the setting to + */ + public @Nullable String getUri() { + return uri; + } + + /** + * The uri to apply the setting to + * + * @return the uri to apply the setting to + */ + public @Nullable String getService() { + return service; + } + + /** + * Get's the command API + * + * @return the command API + */ + public @Nullable NotifySettingUpdateApi getCommandApi() { + return commandApi; + } + + /** + * Get's the get API + * + * @return the get API + */ + public @Nullable NotifySettingUpdateApi getGetApi() { + return getApi; + } + + /** + * Get's the set API + * + * @return the set API + */ + public @Nullable NotifySettingUpdateApi getSetApi() { + return setApi; + } + + @Override + public String toString() { + return "NotifySettingUpdateApiMapping [commandApi=" + commandApi + ", currentValue=" + currentValue + + ", getApi=" + getApi + ", service=" + service + ", setApi=" + setApi + ", target=" + target + + ", targetSuppl=" + targetSuppl + ", uri=" + uri + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Output.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Output.java new file mode 100644 index 0000000000000..a691473f58191 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Output.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the output and is used in serialization to specify the output uri in a call + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Output { + /** The output */ + private final String output; + + /** + * Constructs the output using the default output + */ + public Output() { + this.output = ""; + } + + /** + * Constructs the output from the parameter + * + * @param output a non-null, can be empty output (empty meaning default) + */ + public Output(final String output) { + Objects.requireNonNull(output, "output cannot be null"); + this.output = output; + } + + /** + * Gets the output + * + * @return the output + */ + public String getOutput() { + return output; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ParentalInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ParentalInfo.java new file mode 100644 index 0000000000000..2fe8a5a3373e2 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ParentalInfo.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the parental information + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ParentalInfo { + /** The rating of the content */ + private @Nullable String rating; + + /** The rating system used */ + private @Nullable String system; + + /** The rating country */ + private @Nullable String country; + + /** + * Constructor used for deserialization only + */ + public ParentalInfo() { + } + + /** + * Gets the content rating + * + * @return the content rating + */ + public @Nullable String getRating() { + return rating; + } + + /** + * Gets the rating system + * + * @return the rating system + */ + public @Nullable String getSystem() { + return system; + } + + /** + * Gets the rating country + * + * @return the rating country + */ + public @Nullable String getCountry() { + return country; + } + + @Override + public String toString() { + return "ParentalInfo [rating=" + rating + ", system=" + system + ", country=" + country + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ParentalRatingSetting_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ParentalRatingSetting_1_0.java new file mode 100644 index 0000000000000..5967be0ccd135 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ParentalRatingSetting_1_0.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the parent rating settings and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ParentalRatingSetting_1_0 { + + /** The age limit for the rating type */ + private @Nullable Integer ratingTypeAge; + + /** The Sony rating type. */ + private @Nullable String ratingTypeSony; + + /** The country rating */ + private @Nullable String ratingCountry; + + /** The custom rating type */ + private @Nullable String @Nullable [] ratingCustomTypeTV; + + /** The MPAA rating type. */ + private @Nullable String ratingCustomTypeMpaa; + + /** The Canada Rating type (english) */ + private @Nullable String ratingCustomTypeCaEnglish; + + /** The Canada Rating type (french) */ + private @Nullable String ratingCustomTypeCaFrench; + + /** The unrated lock */ + private @Nullable Boolean unratedLock; + + /** + * Constructor used for deserialization only + */ + public ParentalRatingSetting_1_0() { + } + + /** + * Gets the rating age limit + * + * @return the rating age limit + */ + public @Nullable Integer getRatingTypeAge() { + return ratingTypeAge; + } + + /** + * Gets the Sony rating type + * + * @return the Sony rating type + */ + public @Nullable String getRatingTypeSony() { + return ratingTypeSony; + } + + /** + * Gets the rating country. + * + * @return the rating country + */ + public @Nullable String getRatingCountry() { + return ratingCountry; + } + + /** + * Gets the TV rating type + * + * @return the TV Rating type + */ + public @Nullable String @Nullable [] getRatingCustomTypeTV() { + return ratingCustomTypeTV; + } + + /** + * Gets the MPAA rating type + * + * @return the MPAA rating type + */ + public @Nullable String getRatingCustomTypeMpaa() { + return ratingCustomTypeMpaa; + } + + /** + * Gets the Canada (english) rating type + * + * @return the Canada (english) rating type + */ + public @Nullable String getRatingCustomTypeCaEnglish() { + return ratingCustomTypeCaEnglish; + } + + /** + * Gets the Canada (french) rating type + * + * @return the Canada (french) rating type + */ + public @Nullable String getRatingCustomTypeCaFrench() { + return ratingCustomTypeCaFrench; + } + + /** + * Checks if is unrated is locked + * + * @return true, if locked - false otherwise + */ + public @Nullable Boolean isUnratedLock() { + return unratedLock; + } + + @Override + public String toString() { + return "ParentalRatingSetting_1_0 [ratingTypeAge=" + ratingTypeAge + ", ratingTypeSony=" + ratingTypeSony + + ", ratingCountry=" + ratingCountry + ", ratingCustomTypeTV=" + Arrays.toString(ratingCustomTypeTV) + + ", ratingCustomTypeMpaa=" + ratingCustomTypeMpaa + ", ratingCustomTypeCaEnglish=" + + ratingCustomTypeCaEnglish + ", ratingCustomTypeCaFrench=" + ratingCustomTypeCaFrench + + ", unratedLock=" + unratedLock + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PipSubScreenPosition.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PipSubScreenPosition.java new file mode 100644 index 0000000000000..64c746d9e673f --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PipSubScreenPosition.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the picture-in-picture (PIP) screen position and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PipSubScreenPosition { + + /** The PIP screen position */ + private @Nullable String position; + + /** + * Constructor used for deserialization only + */ + public PipSubScreenPosition() { + } + + /** + * Gets the PIP screen position + * + * @return the PIP screen position + */ + public @Nullable String getPosition() { + return position; + } + + @Override + public String toString() { + return "PipSubScreenPosition [position=" + position + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayContent_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayContent_1_0.java new file mode 100644 index 0000000000000..14eadd928c27e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayContent_1_0.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the request to play content and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PlayContent_1_0 { + + /** The uri of the content */ + private final String uri; + + /** + * Instantiates a new play content request + * + * @param uri the non-null, possibly empty uri + */ + public PlayContent_1_0(final String uri) { + Objects.requireNonNull(uri, "uri cannot be null"); + this.uri = uri; + } + + /** + * Gets the uri of the content + * + * @return the uri of the content + */ + public String getUri() { + return uri; + } + + @Override + public String toString() { + return "PlayContent_1_0 [uri=" + uri + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayContent_1_2.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayContent_1_2.java new file mode 100644 index 0000000000000..b1db939d6b71c --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayContent_1_2.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the request to play content and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PlayContent_1_2 extends PlayContent_1_0 { + /** The output of the content */ + private final String output; + + /** + * Instantiates a new play content request + * + * @param uri the non-null, non-empty uri + * @param output the non-null, possibly empty output + */ + public PlayContent_1_2(final String uri, final String output) { + super(uri); + Objects.requireNonNull(output, "output cannot be empty"); + this.output = output; + } + + /** + * Gets the output of the content + * + * @return the output of the content + */ + public String getOutput() { + return output; + } + + @Override + public String toString() { + return "PlayContent_1_2 [uri=" + getUri() + ", output=" + output + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayingContentInfoRequest_1_2.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayingContentInfoRequest_1_2.java new file mode 100644 index 0000000000000..033452a389a70 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayingContentInfoRequest_1_2.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the request to play content and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PlayingContentInfoRequest_1_2 { + + /** The output of the content */ + private final String output; + + /** + * Instantiates a new play content request + * + * @param output the non-null, possibly empty empty output + */ + public PlayingContentInfoRequest_1_2(final String output) { + Objects.requireNonNull(output, "output cannot be empty"); + this.output = output; + } + + /** + * Gets the output of the content + * + * @return the output of the content + */ + public String getOutput() { + return output; + } + + @Override + public String toString() { + return "PlayContent_1_2 [output=" + output + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayingContentInfoResult_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayingContentInfoResult_1_0.java new file mode 100644 index 0000000000000..eee689e3ea0f5 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayingContentInfoResult_1_0.java @@ -0,0 +1,221 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the request to play content information and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PlayingContentInfoResult_1_0 { + + /** The BIVL asset ID */ + private @Nullable String bivlAssetId; + + /** The BIVL provider */ + private @Nullable String bivlProvider; + + /** The Bravia Internet Video Link (BIVL) service id */ + private @Nullable String bivlServiceId; + + /** The display number */ + private @Nullable String dispNum; + + /** The duration (in seconds) */ + private @Nullable Double durationSec; + + /** The media type */ + private @Nullable String mediaType; + + /** The original display number */ + private @Nullable String originalDispNum; + + /** The play speed */ + private @Nullable String playSpeed; + + /** The program number */ + private @Nullable Integer programNum; + + /** The program title */ + private @Nullable String programTitle; + + /** The source of the content */ + private @Nullable String source; + + /** The start date time */ + private @Nullable String startDateTime; + + /** The title of the content */ + private @Nullable String title; + + /** The triplet string identifier */ + private @Nullable String tripletStr; + + /** The uri of the content */ + private @Nullable String uri; + + /** + * Constructor used for deserialization only + */ + public PlayingContentInfoResult_1_0() { + } + + /** + * Gets the BIVL asset id + * + * @return the BIVL asset id + */ + public @Nullable String getBivlAssetId() { + return bivlAssetId; + } + + /** + * Gets the BIVL provider + * + * @return the BIVL provider + */ + public @Nullable String getBivlProvider() { + return bivlProvider; + } + + /** + * Gets the BIVL service id + * + * @return the BIVL service id + */ + public @Nullable String getBivlServiceId() { + return bivlServiceId; + } + + /** + * Gets the display number + * + * @return the display number + */ + public @Nullable String getDispNum() { + return dispNum; + } + + /** + * Gets the duration (in seconds) + * + * @return the duration (in seconds) + */ + public @Nullable Double getDurationSec() { + return durationSec; + } + + /** + * Gets the media type + * + * @return the media type + */ + public @Nullable String getMediaType() { + return mediaType; + } + + /** + * Gets the original display number + * + * @return the original display number + */ + public @Nullable String getOriginalDispNum() { + return originalDispNum; + } + + /** + * Gets the play speed + * + * @return the play speed + */ + public @Nullable String getPlaySpeed() { + return playSpeed; + } + + /** + * Gets the program number + * + * @return the program number + */ + public @Nullable Integer getProgramNum() { + return programNum; + } + + /** + * Gets the program title + * + * @return the program title + */ + public @Nullable String getProgramTitle() { + return programTitle; + } + + /** + * Gets the source of the content + * + * @return the source of the content + */ + public @Nullable String getSource() { + return source; + } + + /** + * Gets the start date time + * + * @return the start date time + */ + public @Nullable String getStartDateTime() { + return startDateTime; + } + + /** + * Gets the title + * + * @return the title + */ + public @Nullable String getTitle() { + return title; + } + + /** + * Gets the triplet string identifier + * + * @return the triplet string identifier + */ + public @Nullable String getTripletStr() { + return tripletStr; + } + + /** + * Gets the uri of the ocntent + * + * @return the uri of the content + */ + public @Nullable String getUri() { + return uri; + } + + @Override + public String toString() { + return "PlayingContentInfoResult_1_0 [bivlAssetId=" + bivlAssetId + ", bivlProvider=" + bivlProvider + + ", bivlServiceId=" + bivlServiceId + ", dispNum=" + dispNum + ", durationSec=" + durationSec + + ", mediaType=" + mediaType + ", originalDispNum=" + originalDispNum + ", playSpeed=" + playSpeed + + ", programNum=" + programNum + ", programTitle=" + programTitle + ", source=" + source + + ", startDateTime=" + startDateTime + ", title=" + title + ", tripletStr=" + tripletStr + ", uri=" + + uri + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayingContentInfoResult_1_2.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayingContentInfoResult_1_2.java new file mode 100644 index 0000000000000..7224237dbd630 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlayingContentInfoResult_1_2.java @@ -0,0 +1,450 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the request to play content information and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PlayingContentInfoResult_1_2 extends PlayingContentInfoResult_1_0 { + + /** The album name */ + private @Nullable String albumName; + + /** The application name to the content */ + private @Nullable String applicationName; + + /** The artist */ + private @Nullable String artist; + + /** The audio information */ + private @Nullable AudioInfo @Nullable [] audioInfo; + + /** The broadcast frequency */ + private @Nullable Integer broadcastFreq; + + /** The broadcast frequency band */ + private @Nullable String broadcastFreqBand; + + /** The channel name */ + private @Nullable String channelName; + + /** The chapter count */ + private @Nullable Integer chapterCount; + + /** The chapter index */ + private @Nullable Integer chapterIndex; + + /** The content kind */ + private @Nullable String contentKind; + + /** The dab info */ + private @Nullable DabInfo dabInfo; + + /** The duration milliseconds of the content */ + private @Nullable Integer durationMsec; + + /** The file number? */ + private @Nullable String fileNo; + + /** The genre */ + private @Nullable String genre; + + /** The index of the content */ + private @Nullable Integer index; + + /** The index of the content */ + private @Nullable String is3D; + + /** The output of the content */ + private @Nullable String output; + + /** The parent index of the content */ + private @Nullable Integer parentIndex; + + /** The URI of the parent */ + private @Nullable String parentUri; + + /** The path to the content */ + private @Nullable String path; + + /** The playlist name */ + private @Nullable String playlistName; + + /** The play step speed of the content */ + private @Nullable Integer playStepSpeed; + + /** The podcast name */ + private @Nullable String podcastName; + + /** The position milliseconds of the content */ + private @Nullable Integer positionMsec; + + /** The position seconds of the content */ + private @Nullable Double positionSec; + + /** The repeat type of the content */ + private @Nullable String repeatType; + + /** The service of the content */ + private @Nullable String service; + + /** The source label of the content */ + private @Nullable String sourceLabel; + + /** The state info of the content */ + private @Nullable StateInfo stateInfo; + + /** The subtitle index */ + private @Nullable Integer subtitleIndex; + + /** The total count */ + private @Nullable Integer totalCount; + + /** The video information */ + private @Nullable VideoInfo videoInfo; + + /** + * Constructor used for deserialization only + */ + public PlayingContentInfoResult_1_2() { + } + + /** + * Gets the album name + * + * @return the album name + */ + public @Nullable String getAlbumName() { + return albumName; + } + + /** + * Gets the application name + * + * @return the application name + */ + public @Nullable String getApplicationName() { + return applicationName; + } + + /** + * Gets the artist + * + * @return the artist + */ + public @Nullable String getArtist() { + return artist; + } + + /** + * Gets the audio info + * + * @return the audio info + */ + public @Nullable AudioInfo @Nullable [] getAudioInfo() { + return audioInfo; + } + + /** + * Gets the broadcast frequency + * + * @return the broadcast frequency + */ + public @Nullable Integer getBroadcastFreq() { + return broadcastFreq; + } + + /** + * Gets the broadcast frequency band + * + * @return the broadcast frequency band + */ + public @Nullable String getBroadcastFreqBand() { + return broadcastFreqBand; + } + + /** + * Gets the channel name + * + * @return the channel name + */ + public @Nullable String getChannelName() { + return channelName; + } + + /** + * Gets the chapter count + * + * @return the chapter count + */ + public @Nullable Integer getChapterCount() { + return chapterCount; + } + + /** + * Gets the chapter index + * + * @return the chapter index + */ + public @Nullable Integer getChapterIndex() { + return chapterIndex; + } + + /** + * Gets the content kind + * + * @return the content kind + */ + public @Nullable String getContentKind() { + return contentKind; + } + + /** + * Gets the DAB info + * + * @return the DAB info + */ + public @Nullable DabInfo getDabInfo() { + return dabInfo; + } + + /** + * Gets the duration (in milliseconds) + * + * @return the duration (in milliseconds) + */ + public @Nullable Integer getDurationMsec() { + return durationMsec; + } + + /** + * Gets the file number + * + * @return the file number + */ + public @Nullable String getFileNo() { + return fileNo; + } + + /** + * Gets the genre + * + * @return the genre + */ + public @Nullable String getGenre() { + return genre; + } + + /** + * Gets the index + * + * @return the index + */ + public @Nullable Integer getIndex() { + return index; + } + + /** + * Gets the 3D setting + * + * @return the 3D setting + */ + public @Nullable String getIs3D() { + return is3D; + } + + /** + * Gets the output + * + * @return the output + */ + public @Nullable String getOutput() { + return output; + } + + /** + * Gets the output or a default value if output is empty + * + * @param defValue a non-null, non-empty default value + * @return a non-null, non-empty output value + */ + public String getOutput(final String defValue) { + SonyUtil.validateNotEmpty(defValue, "defValue cannot be empty"); + return SonyUtil.defaultIfEmpty(output, defValue); + } + + /** + * Gets the parent index + * + * @return the parent index + */ + public @Nullable Integer getParentIndex() { + return parentIndex; + } + + /** + * Gets the parent uri + * + * @return the parent uri + */ + public @Nullable String getParentUri() { + return parentUri; + } + + /** + * Gets the path + * + * @return the path + */ + public @Nullable String getPath() { + return path; + } + + /** + * Gets the play list name + * + * @return the play list name + */ + public @Nullable String getPlaylistName() { + return playlistName; + } + + /** + * Gets the play step speed + * + * @return the play step speed + */ + public @Nullable Integer getPlayStepSpeed() { + return playStepSpeed; + } + + /** + * Gets the podcast name + * + * @return the podcast name + */ + public @Nullable String getPodcastName() { + return podcastName; + } + + /** + * Gets the position(in milliseconds) + * + * @return the position(in milliseconds) + */ + public @Nullable Integer getPositionMsec() { + return positionMsec; + } + + /** + * Gets the position(in seconds) + * + * @return the position(in seconds) + */ + public @Nullable Double getPositionSec() { + return positionSec; + } + + /** + * Gets the repeat type + * + * @return the repeat type + */ + public @Nullable String getRepeatType() { + return repeatType; + } + + /** + * Gets the service + * + * @return the service + */ + public @Nullable String getService() { + return service; + } + + /** + * Gets the source label + * + * @return the source label + */ + public @Nullable String getSourceLabel() { + return sourceLabel; + } + + /** + * Gets the state information + * + * @return the state information + */ + public @Nullable StateInfo getStateInfo() { + return stateInfo; + } + + /** + * Gets the subtitle index + * + * @return the subtitle index + */ + public @Nullable Integer getSubtitleIndex() { + return subtitleIndex; + } + + /** + * Gets the total count + * + * @return the total count + */ + public @Nullable Integer getTotalCount() { + return totalCount; + } + + /** + * Gets the video information + * + * @return the video information + */ + public @Nullable VideoInfo getVideoInfo() { + return videoInfo; + } + + @Override + public String toString() { + return "PlayingContentInfoResult_1_2 [albumName=" + albumName + ", applicationName=" + applicationName + + ", artist=" + artist + ", audioInfo=" + Arrays.toString(audioInfo) + ", broadcastFreq=" + + broadcastFreq + ", broadcastFreqBand=" + broadcastFreqBand + ", channelName=" + channelName + + ", chapterCount=" + chapterCount + ", chapterIndex=" + chapterIndex + ", contentKind=" + contentKind + + ", dabInfo=" + dabInfo + ", durationMsec=" + durationMsec + ", fileNo=" + fileNo + ", genre=" + genre + + ", index=" + index + ", is3D=" + is3D + ", output=" + output + ", parentIndex=" + parentIndex + + ", parentUri=" + parentUri + ", path=" + path + ", playlistName=" + playlistName + ", playStepSpeed=" + + playStepSpeed + ", podcastName=" + podcastName + ", positionMsec=" + positionMsec + ", positionSec=" + + positionSec + ", repeatType=" + repeatType + ", service=" + service + ", sourceLabel=" + sourceLabel + + ", stateInfo=" + stateInfo + ", subtitleIndex=" + subtitleIndex + ", totalCount=" + totalCount + + ", videoInfo=" + videoInfo + ", getBivlAssetId()=" + getBivlAssetId() + ", getBivlProvider()=" + + getBivlProvider() + ", getBivlServiceId()=" + getBivlServiceId() + ", getDispNum()=" + getDispNum() + + ", getDurationSec()=" + getDurationSec() + ", getMediaType()=" + getMediaType() + + ", getOriginalDispNum()=" + getOriginalDispNum() + ", getPlaySpeed()=" + getPlaySpeed() + + ", getProgramNum()=" + getProgramNum() + ", getProgramTitle()=" + getProgramTitle() + ", getSource()=" + + getSource() + ", getStartDateTime()=" + getStartDateTime() + ", getTitle()=" + getTitle() + + ", getTripletStr()=" + getTripletStr() + ", getUri()=" + getUri() + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlaylistInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlaylistInfo.java new file mode 100644 index 0000000000000..e9e11e3227b42 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PlaylistInfo.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the play list information + * + * TODO: still looking for an example of this.. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PlaylistInfo { + /** + * Constructor used for deserialization only + */ + public PlaylistInfo() { + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Position.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Position.java new file mode 100644 index 0000000000000..b958e91e22466 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Position.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the request to set the picture-in-picture (PIP) location and is used for + * deserialization/serialization + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Position { + /** The PIP position */ + private @Nullable String position; + + /** + * Constructor used for deserialization only + */ + public Position() { + } + + /** + * Instantiates a new position + * + * @param position the non-null, non-empty position + */ + public Position(final String position) { + SonyUtil.validateNotEmpty(position, "position cannot be empty"); + this.position = position; + } + + /** + * Gets the position + * + * @return the position + */ + public @Nullable String getPosition() { + return position; + } + + @Override + public String toString() { + return "Position [position=" + position + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PostalCode.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PostalCode.java new file mode 100644 index 0000000000000..e4c194d192632 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PostalCode.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the request to set the postal code and is used for deserialization/serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PostalCode { + /** The postal code */ + private @Nullable String postalCode; + + /** + * Constructor used for deserialization only + */ + public PostalCode() { + } + + /** + * Instantiates a new postal code + * + * @param postalCode the postal code + */ + public PostalCode(final String postalCode) { + SonyUtil.validateNotEmpty(postalCode, "postalCode cannot be empty"); + this.postalCode = postalCode; + } + + /** + * Gets the postal code + * + * @return the postal code + */ + public @Nullable String getPostalCode() { + return postalCode; + } + + @Override + public String toString() { + return "PostalCode [postalCode=" + postalCode + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerSavingMode.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerSavingMode.java new file mode 100644 index 0000000000000..6dc184e57a37f --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerSavingMode.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the request for the power savings mode and is used for deserialization/serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PowerSavingMode { + /** The power savings mode */ + private @Nullable String mode; + + /** + * Constructor used for deserialization only + */ + public PowerSavingMode() { + } + + /** + * Instantiates a new power saving mode. + * + * @param mode the mode + */ + public PowerSavingMode(final String mode) { + SonyUtil.validateNotEmpty(mode, "mode cannot be empty"); + this.mode = mode; + } + + /** + * Gets the power savings mode + * + * @return the power savings mode + */ + public @Nullable String getMode() { + return mode; + } + + @Override + public String toString() { + return "PowerSavingMode [mode=" + mode + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusRequest_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusRequest_1_0.java new file mode 100644 index 0000000000000..7b4415ed2c4f4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusRequest_1_0.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the request for the power status and is used for deserialization/serialization only + * + * Version: 1.0 + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PowerStatusRequest_1_0 { + /** The status */ + private final boolean status; + + /** + * Instantiates a new power status. + * + * @param status the status + */ + public PowerStatusRequest_1_0(final boolean status) { + this.status = status; + } + + /** + * Gets the power status + * + * @return true if on, false otherwise + */ + public boolean getStatus() { + return status; + } + + @Override + public String toString() { + return "PowerStatus_1_0 [status=" + status + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusRequest_1_1.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusRequest_1_1.java new file mode 100644 index 0000000000000..fe1d521fc74c1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusRequest_1_1.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the request for the power status and is used for deserialization/serialization only + * + * Version: 1.0 + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PowerStatusRequest_1_1 { + + // Various constants for the status + public static final String ACTIVE = "active"; + public static final String OFF = "off"; + + /** The status */ + private final String status; + + /** The standby detail */ + private final @Nullable String standbyDetail; + + /** + * Instantiates a new power status. + * + * @param status the status + */ + public PowerStatusRequest_1_1(final boolean status) { + this.status = status ? ACTIVE : OFF; + standbyDetail = null; + } + + @Override + public String toString() { + return "PowerStatus_1_1 [status=" + status + ", standByDetail=" + standbyDetail + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusResult_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusResult_1_0.java new file mode 100644 index 0000000000000..43cab3d53cf8e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusResult_1_0.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the request for the power status and is used for deserialization/serialization only + * + * Version: 1.0 + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PowerStatusResult_1_0 { + // Various constants for the status + public static final String ACTIVE = "active"; + + // Note - the request is "OFF" but the result is "standby" + public static final String STANDBY = "standby"; + + /** The status */ + private @Nullable String status; + + /** + * Constructor used for deserialization only + */ + public PowerStatusResult_1_0() { + } + + /** + * Gets the power status + * + * @return true if on, false otherwise + */ + public @Nullable String getStatus() { + return status; + } + + public boolean isActive() { + return ACTIVE.equalsIgnoreCase(status); + } + + @Override + public String toString() { + return "PowerStatus_1_0 [status=" + status + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusResult_1_1.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusResult_1_1.java new file mode 100644 index 0000000000000..526f06c23ace9 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerStatusResult_1_1.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the request for the power status and is used for deserialization/serialization only + * + * Version: 1.1 + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PowerStatusResult_1_1 extends PowerStatusResult_1_0 { + + /** The standby detail */ + private @Nullable String standByDetail; + + /** + * Constructor used for deserialization only + */ + public PowerStatusResult_1_1() { + } + + /** + * Gets the power status + * + * @return true if on, false otherwise + */ + public @Nullable String getStandByDetail() { + return standByDetail; + } + + @Override + public String toString() { + return "PowerStatus_1_1 [status=" + getStatus() + ", standByDetail=" + standByDetail + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerSyncMode.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerSyncMode.java new file mode 100644 index 0000000000000..b85dc8cc1930b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PowerSyncMode.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the request for the power sync (CEC) mode and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PowerSyncMode { + + /** The sink power off sync */ + private final @Nullable Boolean sinkPowerOffSync; + + /** The source power on sync */ + private final @Nullable Boolean sourcePowerOnSync; + + /** + * Instantiates a new power sync mode + * + * @param sinkPowerOffSync the sink power off sync (null to not specify) + * @param sourcePowerOnSync the source power on sync (null to not specify) + */ + public PowerSyncMode(final @Nullable Boolean sinkPowerOffSync, final @Nullable Boolean sourcePowerOnSync) { + this.sinkPowerOffSync = sinkPowerOffSync; + this.sourcePowerOnSync = sourcePowerOnSync; + } + + /** + * Checks if is sink power off sync + * + * @return true, if is sink power off sync + */ + public @Nullable Boolean isSinkPowerOffSync() { + return sinkPowerOffSync; + } + + /** + * Checks if is source power on sync. + * + * @return true, if is source power on sync + */ + public @Nullable Boolean isSourcePowerOnSync() { + return sourcePowerOnSync; + } + + @Override + public String toString() { + return "PowerSyncMode [sinkPowerOffSync=" + sinkPowerOffSync + ", sourcePowerOnSync=" + sourcePowerOnSync + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PresetBroadcastStation.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PresetBroadcastStation.java new file mode 100644 index 0000000000000..5d99456589ba1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PresetBroadcastStation.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents a preset broadcast station and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PresetBroadcastStation { + /** The URI of the preset */ + private final String uri; + + /** The frequency of the preset (may not be used - part of the URI) */ + private final @Nullable String frequency; + + /** + * Creates the preset from the uri + * + * @param uri a non-null, non-empty URI + */ + public PresetBroadcastStation(final String uri) { + SonyUtil.validateNotEmpty(uri, "uri"); + + this.uri = uri; + this.frequency = null; + } + + /** + * Gets the URI + * + * @return the URI + */ + public String getUri() { + return uri; + } + + /** + * Gets the frequency + * + * @return the frequency + */ + public @Nullable String getFrequency() { + return frequency; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PublicKey.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PublicKey.java new file mode 100644 index 0000000000000..410f22f58357d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/PublicKey.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the public key to use and is used for deserialization/serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class PublicKey { + + /** The public key */ + private final @Nullable String publicKey; + + /** + * Instantiates a new public key + * + * @param publicKey the non-null, non-empty public key + */ + public PublicKey(final String publicKey) { + SonyUtil.validateNotEmpty(publicKey, "publicKey cannot be null"); + this.publicKey = publicKey; + } + + /** + * Gets the public key + * + * @return the public key + */ + public @Nullable String getPublicKey() { + return publicKey; + } + + @Override + public String toString() { + return "PublicKey [publicKey=" + publicKey + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/RecordingInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/RecordingInfo.java new file mode 100644 index 0000000000000..3ebbae429644a --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/RecordingInfo.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the recording information + * + * TODO: still looking for an example of this.. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class RecordingInfo { + /** + * Constructor used for deserialization only + */ + public RecordingInfo() { + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/RemoteControllerInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/RemoteControllerInfo.java new file mode 100644 index 0000000000000..fb0f4d46c9443 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/RemoteControllerInfo.java @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.gson.GsonUtilities; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * This class represents the remote controller information and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class RemoteControllerInfo { + + /** Whether bundled or not */ + private final boolean bundled; + + /** The controller type */ + private final @Nullable String type; + + /** The commands support (unmodifiable and can be empty) */ + private final List commands; + + /** + * Instantiates a new remote controller info. + * + * @param results the results + */ + public RemoteControllerInfo(final ScalarWebResult results) { + Objects.requireNonNull(results, "result cannot be null"); + + final Gson gson = GsonUtilities.getDefaultGson(); + + Boolean myBundled = null; + String myType = null; + final List myCommands = new ArrayList(); + + final JsonArray rsts = results.getResults(); + if (rsts == null) { + throw new JsonParseException("No results to deserialize"); + } + + for (final JsonElement elm : rsts) { + if (elm.isJsonArray()) { + for (final JsonElement elm2 : elm.getAsJsonArray()) { + myCommands.add(Objects.requireNonNull(gson.fromJson(elm2, RemoteCommand.class))); + } + } else if (elm.isJsonObject()) { + final JsonObject obj = elm.getAsJsonObject(); + + final JsonElement bundElm = obj.get("bundled"); + if (bundElm != null && bundElm.isJsonPrimitive() && bundElm.getAsJsonPrimitive().isBoolean()) { + myBundled = bundElm.getAsBoolean(); + } + + final JsonElement typeElm = obj.get("type"); + if (typeElm != null) { + myType = typeElm.getAsString(); + } + } else { + throw new JsonParseException("Unknown element in array: " + elm); + } + } + + if (myBundled == null) { + throw new JsonParseException("'bundled' was not found or not an boolean"); + } + if (SonyUtil.isEmpty(myType)) { + throw new JsonParseException("'type' was not found or not an boolean"); + } + + bundled = myBundled; + type = myType; + commands = Collections.unmodifiableList(myCommands); + } + + /** + * Checks if is bundled + * + * @return true, if bundled - false otherwise + */ + public boolean isBundled() { + return bundled; + } + + /** + * Gets the controller type + * + * @return the controller type + */ + public @Nullable String getType() { + return type; + } + + /** + * Gets the commands + * + * @return the commands + */ + public List getCommands() { + return commands; + } + + @Override + public String toString() { + return "RemoteControllerInfo [bundled=" + bundled + ", type=" + type + ", commands=" + commands + "]"; + } + + /** + * This class represents the remote command information + * + * @author Tim Roberts - Initial contribution + */ + @NonNullByDefault + public class RemoteCommand { + + /** The name of the command */ + private @Nullable String name; + + /** The value of the command */ + private @Nullable String value; + + /** + * Gets the name of the command + * + * @return the name of the command + */ + public @Nullable String getName() { + return name; + } + + /** + * Gets the value of the command + * + * @return the value of the command + */ + public @Nullable String getValue() { + return value; + } + + @Override + public String toString() { + return "RemoteCommand [name=" + name + ", value=" + value + "]"; + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ScanPlayingContent_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ScanPlayingContent_1_0.java new file mode 100644 index 0000000000000..369c6cded8cb1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ScanPlayingContent_1_0.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the scan direction and is used for serialization + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ScanPlayingContent_1_0 { + /** Constant for a forward direction scan */ + public static final String DIR_FWD = "fwd"; + + /** Constant for a backward direction scan */ + public static final String DIR_BWD = "bwd"; + + /** The direction of the scan */ + private final String direction; + + /** The output to use */ + private final String output; + + /** + * Constructs the scan + * + * @param fwd whether to scan forward (true) or not (false) + * @param output the non-null, possibly empty (for default) output to use + */ + public ScanPlayingContent_1_0(final boolean fwd, final String output) { + Objects.requireNonNull(output, "output cannot be null"); + + this.direction = fwd ? DIR_FWD : DIR_BWD; + this.output = output; + } + + /** + * Get's the direction + * + * @return the direction + */ + public String getDirection() { + return direction; + } + + /** + * Get's the output + * + * @return the output + */ + public String getOutput() { + return output; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Scheme.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Scheme.java new file mode 100644 index 0000000000000..26755de2105b1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Scheme.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the scheme and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Scheme { + + /** Various scheme identifiers */ + public static final String EXT_OUTPUT = "extOutput"; + public static final String EXT_INPUT = "extInput"; + public static final String RADIO = "radio"; + public static final String TV = "tv"; + public static final String STORAGE = "storage"; + + /** The scheme identifier */ + private @Nullable String scheme; + + /** + * Constructor used for deserialization only + */ + public Scheme() { + } + + /** + * Gets the scheme identifier + * + * @return the scheme identifier + */ + public @Nullable String getScheme() { + return scheme; + } + + @Override + public int hashCode() { + final String localScheme = scheme; + return ((localScheme == null) ? 0 : localScheme.hashCode()); + } + + @Override + public boolean equals(final @Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return SonyUtil.equals(scheme, ((Scheme) obj).scheme); + } + + @Override + public String toString() { + return "Scheme [scheme=" + scheme + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Screen.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Screen.java new file mode 100644 index 0000000000000..7ea2d069ad00d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Screen.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the screen and is used for serialization/deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Screen { + /** The screen identifier */ + private @Nullable String screen; + + /** + * Constructor used for deserialization only + */ + public Screen() { + } + + /** + * Instantiates a new screen request + * + * @param screen the screen identifier + */ + public Screen(final String screen) { + SonyUtil.validateNotEmpty(screen, "screen cannot be empty"); + this.screen = screen; + } + + /** + * Gets the screen identifier + * + * @return the screen identifier + */ + public @Nullable String getScreen() { + return screen; + } + + @Override + public String toString() { + return "Screen [screen=" + screen + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SeekBroadcastStation_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SeekBroadcastStation_1_0.java new file mode 100644 index 0000000000000..c628608d69c84 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SeekBroadcastStation_1_0.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a broadcast seek and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SeekBroadcastStation_1_0 { + /** Constant for forward direction */ + public static final String DIR_FWD = "fwd"; + + /** Constant for backward direction */ + public static final String DIR_BWD = "bwd"; + + /** Constant for auto tuning */ + public static final String TUN_AUTO = "auto"; + + /** Constant for manual tuning (step tuning) */ + public static final String TUN_MANUAL = "manual"; + + /** The direction of the seek */ + private final String direction; + + /** The type of tuning during the seek */ + private final String tuning; + + /** + * Constructs the seek from the parms + * + * @param direction true for forward seek, false for backward + * @param tuning true for autotune, false for manual + */ + public SeekBroadcastStation_1_0(final boolean direction, final boolean tuning) { + this.direction = direction ? DIR_FWD : DIR_BWD; + this.tuning = tuning ? TUN_AUTO : TUN_MANUAL; + } + + /** + * The direction of the seek + * + * @return the direction of the seek + */ + public String getDirection() { + return direction; + } + + /** + * The type of seek tuning + * + * @return the type of seek tuning + */ + public String getTuning() { + return tuning; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ServiceProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ServiceProtocol.java new file mode 100644 index 0000000000000..c743b5a4dac8c --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ServiceProtocol.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; + +/** + * This class represents the service protocols for a given service. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ServiceProtocol { + /** The service name */ + private final String serviceName; + + /** The service protocols (immutable) */ + private final Set protocols; + + /** + * Creates the service protocol from the name and protocols + * + * @param serviceName a non-null, non-empty service name + * @param protocols a non-null, possibly empty set of protocols + */ + public ServiceProtocol(final String serviceName, final Set protocols) { + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + Objects.requireNonNull(protocols, "protocols cannot be null"); + + this.serviceName = serviceName; + this.protocols = Collections.unmodifiableSet(new HashSet<>(protocols)); + } + + /** + * Returns the service name + * + * @return a non-null, non-empty service name + */ + public String getServiceName() { + return serviceName; + } + + /** + * Returns the protocols for the service + * + * @return a non-null, possibly empty set of protocols + */ + public Set getProtocols() { + return protocols; + } + + /** + * Determines if the web socket protocol ({@link SonyTransport#WEBSOCKET}) is one of the protocols + * + * @return true if the web socket protocol is found, false otherwise + */ + public boolean hasWebsocketProtocol() { + return protocols.contains(SonyTransportFactory.WEBSOCKET); + } + + /** + * Determines if the http protocol ({@link SonyTransport#HTTP}) is one of the protocols + * + * @return true if the http protocol is found, false otherwise + */ + public boolean hasHttpProtocol() { + return protocols.contains(SonyTransportFactory.HTTP); + } + + @Override + public int hashCode() { + return serviceName.hashCode(); + } + + @Override + public boolean equals(final @Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ServiceProtocol other = (ServiceProtocol) obj; + return SonyUtil.equals(serviceName, other.serviceName); + } + + @Override + public String toString() { + return "ServiceProtocol [serviceName=" + serviceName + ", protocols=" + protocols + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ServiceProtocols.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ServiceProtocols.java new file mode 100644 index 0000000000000..0d8bbab93d3ca --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/ServiceProtocols.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +/** + * This class represents the request to get the service protocols and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class ServiceProtocols { + /** The service names */ + private final Set serviceProtocols = new HashSet<>(); + + /** + * Instantiates a new gets the service protocols. + * + * @param results the non-null results + */ + public ServiceProtocols(final ScalarWebResult results) { + Objects.requireNonNull(results, "results cannot be null"); + + final JsonArray rsts = results.getResults(); + if (rsts == null) { + throw new JsonParseException("No results to deserialize"); + } + + for (final JsonElement elm : rsts) { + if (elm.isJsonArray()) { + final JsonArray ja = elm.getAsJsonArray(); + if (ja.size() > 0) { + final String serviceName = ja.get(0).getAsString(); + if (serviceName == null || serviceName.isEmpty()) { + continue; + } + final Set protocols = new HashSet<>(); + if (ja.size() > 1 && ja.get(1).isJsonArray()) { + for (final JsonElement je : ja.get(1).getAsJsonArray()) { + protocols.add(je.getAsString()); + } + } + serviceProtocols.add(new ServiceProtocol(serviceName, protocols)); + } + } + } + } + + /** + * Gets the service names + * + * @return the non-null, possibly empty unmodifiable set of service names + */ + public Set getServiceProtocols() { + return Collections.unmodifiableSet(serviceProtocols); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SoftwareUpdate.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SoftwareUpdate.java new file mode 100644 index 0000000000000..719d4276f1315 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SoftwareUpdate.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the information on a software update + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SoftwareUpdate { + + /** The updateable status (usually "true" or "false") */ + private @Nullable String isUpdatable; + + /** The update information */ + private @Nullable List<@Nullable SoftwareUpdateInfo> swInfo; + + /** + * Constructor used for deserialization only + */ + public SoftwareUpdate() { + } + + /** + * Get's the updateable status + * + * @return the updateable statuc + */ + public @Nullable String getIsUpdatable() { + return isUpdatable; + } + + /** + * Attempt's to determine if there is a software update + * + * @return true if an update, false otherwise + */ + public boolean isUpdatable() { + // return BooleanUtils.toBooleanObject(isUpdatable) == Boolean.TRUE; + if (isUpdatable == null) { + return false; + } + return "true".equalsIgnoreCase(isUpdatable) || "yes".equalsIgnoreCase(isUpdatable); + } + + /** + * Get's the software update information + * + * @return the software update information + */ + public @Nullable List<@Nullable SoftwareUpdateInfo> getSwInfo() { + return swInfo; + } + + @Override + public String toString() { + return "SoftwareUpdate [isUpdatable=" + isUpdatable + ", swInfo=" + swInfo + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SoftwareUpdateInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SoftwareUpdateInfo.java new file mode 100644 index 0000000000000..07ac6a09b6d13 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SoftwareUpdateInfo.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the information for a specific software update + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SoftwareUpdateInfo { + /** Estimated time to update */ + private @Nullable Integer estimatedTimeSec; + + /** Whether a forced update is required */ + private @Nullable String forcedUpdate; + + /** The target of the update */ + private @Nullable String target; + + /** The update version */ + private @Nullable String updatableVersion; + + /** + * Constructor used for deserialization only + */ + public SoftwareUpdateInfo() { + } + + /** + * Get's the estimated time to updated + * + * @return the estimated time to updated + */ + public @Nullable Integer getEstimatedTimeSec() { + return estimatedTimeSec; + } + + /** + * Get's whether a forced update is required + * + * @return whether a forced update is required + */ + public @Nullable String getForcedUpdate() { + return forcedUpdate; + } + + /** + * Get's the target of the update + * + * @return the target of the update + */ + public @Nullable String getTarget() { + return target; + } + + /** + * Get's the update version + * + * @return the update version + */ + public @Nullable String getUpdatableVersion() { + return updatableVersion; + } + + @Override + public String toString() { + return "SoftwareUpdateInfo [estimatedTimeSec=" + estimatedTimeSec + ", forcedUpdate=" + forcedUpdate + + ", target=" + target + ", updatableVersion=" + updatableVersion + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SoftwareUpdateRequest.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SoftwareUpdateRequest.java new file mode 100644 index 0000000000000..cfcc190e15c26 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SoftwareUpdateRequest.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the request for a software update + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SoftwareUpdateRequest { + + /** Constant saying to use the network for software updates */ + public static final String USENETWORK = "true"; + + /** Constant saying to use a USB for software updates */ + public static final String USEUSB = "false"; + + /** Whether to use the network ("true") or a USB ("false)") */ + private final String network; + + /** + * Creates the request using the specified network + * + * @param network a non-null, non-empty network + */ + public SoftwareUpdateRequest(final String network) { + SonyUtil.validateNotEmpty(network, "network cannot be empty"); + + this.network = network; + } + + @Override + public String toString() { + return "SoftwareUpdateRequest [network=" + network + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Source.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Source.java new file mode 100644 index 0000000000000..5d9d08a40e0b7 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Source.java @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Arrays; +import java.util.Objects; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the source identifier and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Source { + + /** Pattern for a radio source URI */ + public static final Pattern RADIOPATTERN = Pattern.compile("radio:[af]m(?:\\?contentId=(\\d+))?"); + + /** The source identifier */ + private @Nullable String source; + + /** The source title */ + private @Nullable String title; + + /** Whether the source is browsable */ + private @Nullable Boolean isBrowsable; + + /** Whether the source is playable */ + private @Nullable Boolean isPlayable; + + /** The source meta information */ + private @Nullable String meta; + + /** The source play action */ + private @Nullable String playAction; + + /** The source outputs */ + private @Nullable String @Nullable [] outputs; + + /** + * Constructor used for deserialization only + */ + public Source() { + } + + /** + * Gets the source meta information + * + * @return the source meta information + */ + public @Nullable String getMeta() { + return meta; + } + + /** + * Gets the outputs for the source + * + * @return the outputs for the source + */ + public @Nullable String @Nullable [] getOutputs() { + return outputs; + } + + /** + * Gets the play action for the source + * + * @return the play action for the source + */ + public @Nullable String getPlayAction() { + return playAction; + } + + /** + * Gets the source identifier + * + * @return the source identifier + */ + public @Nullable String getSource() { + return source; + } + + /** + * Gets just the scheme part of the source uri + * + * @return the scheme part of the source uri + */ + public @Nullable String getSchemePart() { + return getSchemePart(source); + } + + /** + * Gets the source part of the source uri + * + * @return the source part of the source uri + */ + public @Nullable String getSourcePart() { + return getSourcePart(source); + } + + /** + * Gets the title of the source + * + * @return the title of the source + */ + public @Nullable String getTitle() { + return title; + } + + /** + * Whether the source is browsable + * + * @return whether the source is browsable + */ + public @Nullable Boolean isBrowsable() { + return isBrowsable; + } + + /** + * Whether the source is playable + * + * @return whether the source is playable + */ + public @Nullable Boolean isPlayable() { + return isPlayable; + } + + /** + * Whether this source matches the given name (either by title or by source url) + * + * @param name a non-null, possibly empty name + * @return true for a match, false otherwise + */ + public boolean isMatch(final String name) { + Objects.requireNonNull(name, "name cannot be null"); + return name.equalsIgnoreCase(title) || name.equalsIgnoreCase(source); + } + + /** + * Helper method to extract the scheme part from a source uri + * + * @param uri a possibly null, possibly empty source uri + * @return the scheme part or null if not found + */ + public static @Nullable String getSchemePart(final @Nullable String uri) { + if (uri == null || uri.isEmpty()) { + return null; + } + + final int idx = uri.indexOf(":"); + return idx < 0 ? uri : uri.substring(0, idx); + } + + /** + * Helper method to extract the source part from a source uri + * + * @param uri a possibly null, possibly empty source uri + * @return the source part or null if not found + */ + public static @Nullable String getSourcePart(final @Nullable String uri) { + if (uri == null || uri.isEmpty()) { + return null; + } + + final int idx = uri.indexOf(":"); + return idx < 0 ? uri : uri.substring(idx + 1); + } + + @Override + public int hashCode() { + final String localSource = source; + return ((localSource == null) ? 0 : localSource.hashCode()); + } + + @Override + public boolean equals(final @Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return SonyUtil.equals(source, ((Source) obj).source); + } + + @Override + public String toString() { + return "Source [source=" + source + ", isBrowsable=" + isBrowsable + ", isPlayable=" + isPlayable + ", meta=" + + meta + ", playAction=" + playAction + ", outputs=" + Arrays.toString(outputs) + ", title=" + title + + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Speed.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Speed.java new file mode 100644 index 0000000000000..a0403d523d3ec --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Speed.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the speed information + * + * TODO: still looking for an example of this.. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Speed { + /** + * Constructor used for deserialization only + */ + public Speed() { + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StateInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StateInfo.java new file mode 100644 index 0000000000000..d9c7d61d56101 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StateInfo.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The state information class used for deserialization only. Please note that sony broke this in their API and all + * states are stopped + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class StateInfo { + /** The static field for the Stopped state */ + public static final String STOPPED = "stopped"; + + /** The current state */ + private @Nullable String state; + + /** The current state supplemental information */ + private @Nullable String supplement; + + /** + * Constructor used for deserialization only + */ + public StateInfo() { + } + + /** + * Returns the current state + * + * @return the current state + */ + public @Nullable String getState() { + return state; + } + + /** + * Returns the current state supplemental information + * + * @return the current state supplemental information + */ + public @Nullable String getSupplement() { + return supplement; + } + + @Override + public String toString() { + return "StateInfo [state=" + state + ", supplement=" + supplement + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListItem_1_1.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListItem_1_1.java new file mode 100644 index 0000000000000..d61f1c7ad7020 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListItem_1_1.java @@ -0,0 +1,220 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The specific storage list item information - used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class StorageListItem_1_1 { + /** The uri */ + private @Nullable String uri; + + /** The device name */ + private @Nullable String deviceName; + + /** The volume label */ + private @Nullable String volumeLabel; + + /** The permissions required */ + private @Nullable String permission; + + /** The position of the storage (internal, front, back ,etc) */ + private @Nullable String position; + + /** Whether it's formattable */ + private @Nullable String formattable; + + /** Whether the storge is mounted */ + private @Nullable String mounted; + + /** THe whole capacity (MB) */ + private @Nullable Integer wholeCapacityMB; + + /** The free capacity (MB) */ + private @Nullable Integer freeCapacityMB; + + /** The system area capacity (MB) */ + private @Nullable Integer systemAreaCapacityMB; + + /** Status of whether it's being formatted */ + private @Nullable String formatting; + + /** Whether the storage is available */ + private @Nullable String isAvailable; + + /** The logical unit number */ + private @Nullable Integer lun; + + /** The storage type */ + private @Nullable String type; + + /** The format of the storage */ + private @Nullable String format; + + /** Storage errors */ + private @Nullable String error; + + /** + * Constructor used for deserialization only + */ + public StorageListItem_1_1() { + } + + /** + * Gets the storage URI + * + * @return the uri + */ + public @Nullable String getUri() { + return uri; + } + + /** + * Gets the device name + * + * @return the device name + */ + public @Nullable String getDeviceName() { + return deviceName; + } + + /** + * Gets the volume label + * + * @return the volume label + */ + public @Nullable String getVolumeLabel() { + return volumeLabel; + } + + /** + * @return the permission + */ + public @Nullable String getPermission() { + return permission; + } + + /** + * Gets the position of the storage (front, back, internal, etc) + * + * @return the position + */ + public @Nullable String getPosition() { + return position; + } + + /** + * Gets the formattable status + * + * @return the formattable status + */ + public @Nullable String getFormattable() { + return formattable; + } + + /** + * Gets the mounted status + * + * @return the mounted status + */ + public @Nullable String getMounted() { + return mounted; + } + + /** + * Gets the whole capacity (in MB) + * + * @return the whole capacity + */ + public @Nullable Integer getWholeCapacityMB() { + return wholeCapacityMB; + } + + /** + * Gets the free capacity (in MB) + * + * @return the free capacity + */ + public @Nullable Integer getFreeCapacityMB() { + return freeCapacityMB; + } + + /** + * Gets the system area capacity (MB) + * + * @return the system area capacity + */ + public @Nullable Integer getSystemAreaCapacityMB() { + return systemAreaCapacityMB; + } + + /** + * Gets the formatting status + * + * @return the formatting status + */ + public @Nullable String getFormatting() { + return formatting; + } + + /** + * Gets the storage availability status + * + * @return the availability status + */ + public @Nullable String getIsAvailable() { + return isAvailable; + } + + /** + * Gets the logical unit number + * + * @return the logical unit number + */ + public @Nullable Integer getLun() { + return lun; + } + + /** + * Gets the storage type + * + * @return the storage type + */ + public @Nullable String getType() { + return type; + } + + /** + * Gets the storage format + * + * @return the storage format + */ + public @Nullable String getFormat() { + return format; + } + + /** + * Gets the storage error + * + * @return the storage error + */ + public @Nullable String getError() { + return error; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListItem_1_2.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListItem_1_2.java new file mode 100644 index 0000000000000..cbd30e87f9a92 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListItem_1_2.java @@ -0,0 +1,403 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The specific storage list item information - used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class StorageListItem_1_2 { + /** The device name */ + private @Nullable String deviceName; + + /** The storage error */ + private @Nullable String error; + + /** The file system type */ + private @Nullable String fileSystem; + + /** The finalize status */ + private @Nullable String finalizeStatus; + + /** The storage format */ + private @Nullable String format; + + /** The format status */ + private @Nullable String formatStatus; + + /** The formattable status */ + private @Nullable String formattable; + + /** The free capacity (MB) */ + private @Nullable Integer freeCapacityMB; + + /** Whether it has non standard data */ + private @Nullable String hasNonStandardData; + + /** Whether it has unsupported contents */ + private @Nullable String hasUnsupportedContents; + + /** Whether it is available */ + private @Nullable String isAvailable; + + /** Whether it is locked */ + private @Nullable String isLocked; + + /** Whether the management information is full */ + private @Nullable String isManagementInfoFull; + + /** Whether it is protected */ + private @Nullable String isProtected; + + /** Whether it is registered */ + private @Nullable String isRegistered; + + /** Whether it is self recorded */ + private @Nullable String isSelfRecorded; + + /** Whether SQV (standard quality voice) is supported */ + private @Nullable String isSqvSupported; + + /** The logical unit number */ + private @Nullable Integer lun; + + /** The mount status */ + private @Nullable String mounted; + + /** The storage permission */ + private @Nullable String permission; + + /** The storage position (front, back, internal, etc) */ + private @Nullable String position; + + /** The storage protocol */ + private @Nullable String protocol; + + /** The storage registration date */ + private @Nullable String registrationDate; + + /** The system area capacity (MB) */ + private @Nullable Integer systemAreaCapacityMB; + + /** The time (in seconds) to finalize */ + private @Nullable Integer timeSecToFinalize; + + /** The time (in seconds) to get contents */ + private @Nullable Integer timeSecToGetContents; + + /** The storage type */ + private @Nullable String type; + + /** The storage URI */ + private @Nullable String uri; + + /** The USB device type */ + private @Nullable String usbDeviceType; + + /** The volume label */ + private @Nullable String volumeLabel; + + /** The whole capacity (MB) */ + private @Nullable Integer wholeCapacityMB; + + /** + * Constructor used for deserialization only + */ + public StorageListItem_1_2() { + } + + /** + * Gets the storage device name + * + * @return the storage device name + */ + public @Nullable String getDeviceName() { + return deviceName; + } + + /** + * Gets the storage error + * + * @return the storage error + */ + public @Nullable String getError() { + return error; + } + + /** + * Gets the file system + * + * @return the file system + */ + public @Nullable String getFileSystem() { + return fileSystem; + } + + /** + * Gets the finalization status + * + * @return the finalization status + */ + public @Nullable String getFinalizeStatus() { + return finalizeStatus; + } + + /** + * Gets the storage format + * + * @return the storage format + */ + public @Nullable String getFormat() { + return format; + } + + /** + * Gets the format status + * + * @return the format status + */ + public @Nullable String getFormatStatus() { + return formatStatus; + } + + /** + * Gets the formattable status + * + * @return the formattable status + */ + public @Nullable String getFormattable() { + return formattable; + } + + /** + * Gets the free capacity (MB) + * + * @return the free capacity (MB) + */ + public @Nullable Integer getFreeCapacityMB() { + return freeCapacityMB; + } + + /** + * ' + * Gets whether the storage has non-standard data + * + * @return whether the storage has non-standard data + */ + public @Nullable String getHasNonStandardData() { + return hasNonStandardData; + } + + /** + * Gets whether the storage has unsupported contents + * + * @return whether the storage has unsupported contents + */ + public @Nullable String getHasUnsupportedContents() { + return hasUnsupportedContents; + } + + /** + * Whether the storage is available + * + * @return whether the storage is available + */ + public @Nullable String getIsAvailable() { + return isAvailable; + } + + /** + * Whether the storage is locked + * + * @return Whether the storage is locked + */ + public @Nullable String getIsLocked() { + return isLocked; + } + + /** + * Whether the storage management info is full + * + * @return whether the storage management info is full + */ + public @Nullable String getIsManagementInfoFull() { + return isManagementInfoFull; + } + + /** + * Whether the storage is protected + * + * @return whether the storage is protected + */ + public @Nullable String getIsProtected() { + return isProtected; + } + + /** + * Whether the storage is registered + * + * @return whether the storage is registered + */ + public @Nullable String getIsRegistered() { + return isRegistered; + } + + /** + * Whether the storage is self recorded + * + * @return whether the storage is self recorded + */ + public @Nullable String getIsSelfRecorded() { + return isSelfRecorded; + } + + /** + * Whether SQV is supported + * + * @return whether SQV is supported + */ + public @Nullable String getIsSqvSupported() { + return isSqvSupported; + } + + /** + * Gets the logical unit number + * + * @return the logical unit number + */ + public @Nullable Integer getLun() { + return lun; + } + + /** + * Gets the mount status + * + * @return the mount status + */ + public @Nullable String getMounted() { + return mounted; + } + + /** + * Gets the storage permission + * + * @return the storage permission + */ + public @Nullable String getPermission() { + return permission; + } + + /** + * Gets the storage position (front, back, internal, etc) + * + * @return the storage position + */ + public @Nullable String getPosition() { + return position; + } + + /** + * Gets the storage protocol + * + * @return the storage protocol + */ + public @Nullable String getProtocol() { + return protocol; + } + + /** + * Gets the registration date + * + * @return the registration date + */ + public @Nullable String getRegistrationDate() { + return registrationDate; + } + + /** + * Gets the system area capacity (MB) + * + * @return the system area capacity + */ + public @Nullable Integer getSystemAreaCapacityMB() { + return systemAreaCapacityMB; + } + + /** + * Gets the time (in seconds) to finalize + * + * @return the time to finalize + */ + public @Nullable Integer getTimeSecToFinalize() { + return timeSecToFinalize; + } + + /** + * Gets the time (in seconds) to get contents + * + * @return the time to get contents + */ + public @Nullable Integer getTimeSecToGetContents() { + return timeSecToGetContents; + } + + /** + * Gets the storage type + * + * @return the storage type + */ + public @Nullable String getType() { + return type; + } + + /** + * Gets the storage URI + * + * @return the uri + */ + public @Nullable String getUri() { + return uri; + } + + /** + * Gets the USB device type + * + * @return the USB device type + */ + public @Nullable String getUsbDeviceType() { + return usbDeviceType; + } + + /** + * Gets the volume label + * + * @return the volume label + */ + public @Nullable String getVolumeLabel() { + return volumeLabel; + } + + /** + * Gets the whole capacity (MB) + * + * @return the whole capacity + */ + public @Nullable Integer getWholeCapacityMB() { + return wholeCapacityMB; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListRequest_1_1.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListRequest_1_1.java new file mode 100644 index 0000000000000..db25cd328bdb6 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListRequest_1_1.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * The storage list request class used for serialization only. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class StorageListRequest_1_1 { + + /** The storage list items */ + private final String uri; + + /** + * Constructs the request with no URI + */ + public StorageListRequest_1_1() { + this(""); + } + + /** + * Constructs the request with the URI + * + * @param uri a possibly null, possibly empty uri + */ + public StorageListRequest_1_1(final String uri) { + this.uri = SonyUtil.defaultIfEmpty(uri, ""); + } + + /** + * Get's the URI + * + * @return the uri + */ + public String getUri() { + return uri; + } + + @Override + public String toString() { + return "StorageListRequest_1_1 [uri=" + uri + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListRequest_1_2.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListRequest_1_2.java new file mode 100644 index 0000000000000..38111a15ee09b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageListRequest_1_2.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * The storage list request class used for serialization only. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class StorageListRequest_1_2 extends StorageListRequest_1_1 { + /** The is registered parm */ + private final String isRegistered; + + /** + * Constructs the request with no URI + */ + public StorageListRequest_1_2() { + this("", ""); + } + + /** + * Constructs the request with the URI and isRegistered + * + * @param uri a possibly null, possibly empty uri + * @param isRegistered a possibly null, possibly empty is registered parm + */ + public StorageListRequest_1_2(final String uri, final String isRegistered) { + super(uri); + this.isRegistered = SonyUtil.defaultIfEmpty(isRegistered, ""); + } + + @Override + public String toString() { + return "StorageListRequest_1_2 [uri=" + getUri() + ", isRegistered=" + isRegistered + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageList_1_1.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageList_1_1.java new file mode 100644 index 0000000000000..4ad719098a657 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageList_1_1.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The storage list information class used for deserialization only. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class StorageList_1_1 { + + /** The storage list items */ + private @Nullable List<@Nullable StorageListItem_1_1> items; + + /** + * Constructor used for deserialization only + */ + public StorageList_1_1() { + } + + /** + * Gets the list of storage items + * + * @return the list of storage items + */ + public @Nullable List<@Nullable StorageListItem_1_1> getItems() { + return items; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageList_1_2.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageList_1_2.java new file mode 100644 index 0000000000000..0d50748635be1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/StorageList_1_2.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The storage list information class used for deserialization only. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class StorageList_1_2 { + /** The storage list items */ + private @Nullable List<@Nullable StorageListItem_1_2> items; + + /** + * Constructor used for deserialization only + */ + public StorageList_1_2() { + } + + /** + * Gets the list of storage items + * + * @return the list of storage items + */ + public @Nullable List<@Nullable StorageListItem_1_2> getItems() { + return items; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SubtitleInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SubtitleInfo.java new file mode 100644 index 0000000000000..69992144dcd6f --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SubtitleInfo.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the subtitle information + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SubtitleInfo { + /** The subtitle language */ + private @Nullable String langauge; + + /** The name (title) of the subtitle */ + private @Nullable String title; + + /** + * Constructor used for deserialization only + */ + public SubtitleInfo() { + } + + /** + * Gets the subtitle language + * + * @return the subtitle language + */ + public @Nullable String getLangauge() { + return langauge; + } + + /** + * Gets the subtitle title (name) + * + * @return the subtitle title (name) + */ + public @Nullable String getTitle() { + return title; + } + + @Override + public String toString() { + return "SubtitleInfo [langauge=" + langauge + ", title=" + title + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApi.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApi.java new file mode 100644 index 0000000000000..7ab3256f652eb --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApi.java @@ -0,0 +1,278 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +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.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.gson.SupportedApiDeserializer; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebRequest; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.transports.SonyTransport; +import org.slf4j.Logger; + +/** + * This class represents all the supported APIs that a service provides and is used for serialization and + * deserialization via {@link SupportedApiDeserializer}. + * + * Note: if a service name is unknown/illegal (such as + * dial/dd.xml), we need to catch + * IllegalArgumentException (which is thrown in the 'as' methods) to prevent the illegal service from keeping the thing + * from going online. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SupportedApi { + /** The map of APIs to the api name (unmodifiable) */ + private final Map apis; + + /** The map of notifications to notification name (unmodifiable) */ + private final Map notifications; + + /** The set of protocols the service supports (unmodifiable) */ + private final Set protocols; + + /** The service name */ + private final String service; + + /** + * Constructs the supported API from the parameters + * + * @param service a non-null, non-empty service name + * @param apis a non-null, possibly empty list of supported apis + * @param notifications a non-null, possibly empty list of notifications + * @param protocols a non-null, possibly empty set of protocols + */ + public SupportedApi(final String service, final List apis, + final List notifications, final Set protocols) { + SonyUtil.validateNotEmpty(service, "service cannot be empty"); + Objects.requireNonNull(apis, "apis cannot be null"); + Objects.requireNonNull(notifications, "notifications cannot be null"); + Objects.requireNonNull(protocols, "protocols cannot be null"); + + this.service = service; + this.apis = Collections.unmodifiableMap(apis.stream().collect(Collectors.toMap(k -> k.getName(), v -> v))); + this.notifications = Collections + .unmodifiableMap(notifications.stream().collect(Collectors.toMap(k -> k.getName(), v -> v))); + this.protocols = Collections.unmodifiableSet(protocols); + } + + /** + * Returns the service name + * + * @return the non-null, non-empty service name + */ + public String getService() { + return service; + } + + /** + * Returns the set of protocols supported + * + * @return a non-null, possibly empty unmodifiable set of protocols + */ + public Set getProtocols() { + return protocols; + } + + /** + * Returns a collection of APIs supported + * + * @return a non-null, possibly empty unmodifiable collection of apis + */ + public Collection getApis() { + return apis.values(); + } + + /** + * Returns a collection of notifications supported + * + * @return a non-null, possibly empty unmodifiable collection of notifications + */ + public Collection getNotifications() { + return notifications.values(); + } + + /** + * Returns the API for a given method name + * + * @param methodName a non-null, non empty method name + * @return the supported API or null if none found + */ + public @Nullable SupportedApiInfo getMethod(final String methodName) { + SonyUtil.validateNotEmpty(methodName, "methodName cannot be null"); + return apis.get(methodName); + } + + /** + * Returns the set of protocols for a given method/version + * + * @param methodName a non-null, non-empty method name + * @param version a non-null, non-empty version + * @return a non-null, possibly empty set of protocols + */ + public Set getProtocols(final String methodName, final String version) { + SonyUtil.validateNotEmpty(methodName, "methodName cannot be null"); + SonyUtil.validateNotEmpty(version, "version cannot be null"); + + final SupportedApiInfo info = apis.get(methodName); + final SupportedApiVersionInfo vers = info == null ? null : info.getVersions(version); + final Set mthdProtocols = vers == null ? null : vers.getProtocols(); + return mthdProtocols != null && !mthdProtocols.isEmpty() ? mthdProtocols : protocols; + } + + /** + * A helper method to retrieve the supported API for a given service and transport + * + * @param service a non-null service to use + * @param serviceName a non-null, non-empty service name to query (may be different from service) + * @param transport a non-null transport to use in retrieving the API + * @param logger a non-null logger to log messasge + * @return a non-null supported API + */ + public static SupportedApi getSupportedApi(final ScalarWebService service, final String serviceName, + final SonyTransport transport, final Logger logger) { + Objects.requireNonNull(service, "service cannot be null"); + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + Objects.requireNonNull(transport, "transport cannot be null"); + Objects.requireNonNull(logger, "logger cannot be null"); + + final SupportedApi api = getSupportApi(service, serviceName, logger); + return api == null ? getSupportApiAlternate(serviceName, transport, logger) : api; + } + + /** + * Helper method to get the supported API from teh service and service name + * + * @param service a non-null service to use + * @param serviceName a non-null, non-empty servicename to use + * @param logger a non-null logger to log message to. + * @return the supported API or null if not found + */ + private static @Nullable SupportedApi getSupportApi(final ScalarWebService service, final String serviceName, + final Logger logger) { + Objects.requireNonNull(service, "service cannot be null"); + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + Objects.requireNonNull(logger, "logger cannot be null"); + + try { + return service.execute(ScalarWebMethod.GETSUPPORTEDAPIINFO, new SupportedApiServices(serviceName)) + .as(SupportedApi.class); + } catch (final IOException | IllegalArgumentException e) { + logger.trace("Exception getting supported api info: {}", e.getMessage(), e); + return null; + } + } + + /** + * Helper method to get the 'alternative' supported api from a servicename and transport. The 'alternative' + * supported API is created from the getversions/getmethodtypes calls rather than the native getsupportedapi call. + * + * @param serviceName a non-null, non-empty service name + * @param transport a non-null transport + * @param logger a non-null logger to log messages with + * @return the supported api based on getversions/getmethodtypes calls + */ + public static SupportedApi getSupportApiAlternate(final String serviceName, final SonyTransport transport, + final Logger logger) { + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + Objects.requireNonNull(transport, "transport cannot be null"); + Objects.requireNonNull(logger, "logger cannot be null"); + + final List methods = new ArrayList<>(); + try { + // Retrieve the versions for the service + final List versionResult = transport + .execute(new ScalarWebRequest(ScalarWebMethod.GETVERSIONS, ScalarWebMethod.V1_0)) + .asArray(String.class); + + // For each version, retrieve the methods for the service + for (final String apiVersion : versionResult) { + final MethodTypes mtdResults = transport + .execute(new ScalarWebRequest(ScalarWebMethod.GETMETHODTYPES, ScalarWebMethod.V1_0, apiVersion)) + .as(MethodTypes.class); + methods.addAll(mtdResults.getMethods()); + } + } catch (final IOException | IllegalArgumentException e) { + logger.debug("Could not retrieve methods: {}", e.getMessage(), e); + } + + final Map> mthdVersions = new HashMap<>(); + methods.stream().forEach(m -> { + final String mthdName = m.getMethodName(); + Set versions = mthdVersions.get(mthdName); + if (versions == null) { + versions = new HashSet<>(); + mthdVersions.put(mthdName, versions); + } + versions.add(m.getVersion()); + }); + + final List apis = mthdVersions.entrySet().stream() + .map(e -> new SupportedApiInfo(e.getKey(), + e.getValue().stream().map(v -> new SupportedApiVersionInfo(v)).collect(Collectors.toList()))) + .collect(Collectors.toList()); + + final Set protocols = new HashSet<>(); + protocols.add(transport.getProtocolType()); + + final List notifications = new ArrayList<>(); + if (mthdVersions.containsKey(ScalarWebMethod.SWITCHNOTIFICATIONS)) { + try { + final Notifications ntfs = transport + .execute(new ScalarWebRequest(ScalarWebMethod.SWITCHNOTIFICATIONS, "1.0", new Notifications())) + .as(Notifications.class); + Stream.concat(ntfs.getEnabled().stream(), ntfs.getDisabled().stream()).map(n -> { + final String version = n.getVersion(); + final String name = n.getName(); + Optional spi = Optional.empty(); + if (name != null && !name.isEmpty() && version != null && !version.isEmpty()) { + spi = Optional + .of(new SupportedApiInfo(name, Arrays.asList(new SupportedApiVersionInfo(version)))); + } + return spi; + }).filter(Optional::isPresent).map(Optional::get).forEachOrdered(notifications::add); + } catch (final IOException | IllegalArgumentException e) { + logger.debug("Exception getting notifications for service {}: {}", serviceName, e.getMessage()); + } + } else { + logger.debug("Service {} doesn't contain {} - ignoring notifications", serviceName, + ScalarWebMethod.SWITCHNOTIFICATIONS); + } + + return new SupportedApi(serviceName, apis, notifications, protocols); + } + + @Override + public String toString() { + return "SupportedApi [apis=" + apis + ", notifications=" + notifications + ", protocols=" + protocols + + ", service=" + service + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApiInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApiInfo.java new file mode 100644 index 0000000000000..2a08722dbf19f --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApiInfo.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.VersionUtilities; +import org.openhab.binding.sony.internal.scalarweb.gson.SupportedApiInfoDeserializer; + +/** + * This class represents a supported API and all the version information for it. This will be used in deserialization + * and in serialization via {@link SupportedApiInfoDeserializer} + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SupportedApiInfo { + /** the API name */ + private final String name; + + /** The map of version information by version number */ + private final Map versions; + + /** The latest version (if found) */ + private final @Nullable SupportedApiVersionInfo latestVersion; + + /** + * Constructs the supported API via the parameters + * + * @param name a non-null, non-empty name + * @param versions a non-null, possibly empty list of versions + */ + public SupportedApiInfo(final String name, final List versions) { + SonyUtil.validateNotEmpty(name, "name cannot be empty"); + Objects.requireNonNull(versions, "versions cannot be null"); + + this.name = name; + this.versions = Collections + .unmodifiableMap(versions.stream().collect(Collectors.toMap(k -> k.version, v -> v))); + + final Optional latest = versions.stream().max((c1, c2) -> { + final double d1 = VersionUtilities.parse(c1.version); + final double d2 = VersionUtilities.parse(c2.version); + if (d1 < d2) { + return -1; + } + if (d2 > d1) { + return 1; + } + return 0; + }); + + this.latestVersion = latest.isPresent() ? latest.get() : null; + } + + /** + * Returns the API name + * + * @return a non-null, non-empty name + */ + public String getName() { + return name; + } + + /** + * Returns the versions + * + * @return a non-null, possibly emtpy collection of version info + */ + public Collection getVersions() { + return versions.values(); + } + + /** + * Get's the version info for the given version (or null if not found) + * + * @param version a non-null, possibly empty version + * @return the version info if found, null if not + */ + public @Nullable SupportedApiVersionInfo getVersions(final String version) { + Objects.requireNonNull(version, "version cannot be null"); + return versions.get(version); + } + + /** + * Get's the latest version for the API or null if there is none + * + * @return the latest version info or null if none + */ + public @Nullable SupportedApiVersionInfo getLatestVersion() { + return latestVersion; + } + + @Override + public String toString() { + return "SupportedApiInfo [name=" + name + ", versions=" + versions + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApiServices.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApiServices.java new file mode 100644 index 0000000000000..07552f490e802 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApiServices.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents the service supported by the API and is used to request information + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SupportedApiServices { + /** The sevices */ + private final String[] services; + + /** + * Constructs the class from teh specified services + * + * @param services a non-empty list of services + */ + public SupportedApiServices(final String... services) { + if (services.length == 0) { + throw new IllegalArgumentException("services must have atlease one"); + } + this.services = services; + } + + /** + * Returns the services + * + * @return a non-null, non empty array of services + */ + public String[] getServices() { + return services; + } + + @Override + public String toString() { + return "SupportedApiServices [services=" + Arrays.toString(services) + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApiVersionInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApiVersionInfo.java new file mode 100644 index 0000000000000..972f58f2c42b0 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SupportedApiVersionInfo.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.gson.SupportedApiInfoDeserializer; + +/** + * This class represents a supported API version and is used for serialization and deserialization via + * {@link SupportedApiInfoDeserializer} + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SupportedApiVersionInfo { + /** The authorization level of the API */ + private final String authLevel; + + /** The protocols for the API */ + private final Set protocols; + + /** The API version */ + final String version; + + /** + * Constructs the API with only the version (authLevel and protols are blank) + * + * @param version a non-null, non-empty version + */ + public SupportedApiVersionInfo(final String version) { + this("", new HashSet<>(), version); + } + + /** + * Constructs the API with the parameters + * + * @param authLevel a non-null, possibly empty authLevel + * @param protocols a non-null, possibly empty set of protocols + * @param version a non-null, non-empty version + */ + public SupportedApiVersionInfo(final String authLevel, final Set protocols, final String version) { + Objects.requireNonNull(authLevel, "authLevel cannot be null"); + Objects.requireNonNull(protocols, "protocols cannot be null"); + SonyUtil.validateNotEmpty(version, "version cannot be empty"); + + this.authLevel = authLevel; + this.protocols = protocols; + this.version = version; + } + + /** + * Returns the auth level + * + * @return the non-null, possibly empty auth level + */ + public String getAuthLevel() { + return authLevel; + } + + /** + * Returns the supported protocols + * + * @return a non-null, possibly empty set of protocols + */ + public Set getProtocols() { + return protocols; + } + + /** + * Returns the API version + * + * @return a non-null, non-empty version + */ + public String getVersion() { + return version; + } + + @Override + public String toString() { + return "SupportedApiVersionInfo [authLevel=" + authLevel + ", protocols=" + protocols + ", version=" + version + + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SystemInformation.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SystemInformation.java new file mode 100644 index 0000000000000..29deb147b5169 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SystemInformation.java @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the system information and is used for deserialization only. Note that there are many more + * properties to the getSystemInformation call that we do not currently use. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SystemInformation { + /** The product id */ + private @Nullable String product; + + /** The region */ + private @Nullable String region; + + /** The system language */ + private @Nullable String language; + + /** The model */ + private @Nullable String model; + + /** The serial */ + private @Nullable String serial; + + /** The mac address */ + private @Nullable String macAddr; + + /** The name */ + private @Nullable String name; + + /** The generation */ + private @Nullable String generation; + + /** The area */ + private @Nullable String area; + + /** The cid (not sure) */ + private @Nullable String cid; + + /** + * Constructor used for deserialization only + */ + public SystemInformation() { + } + + /** + * Gets the product + * + * @return the product + */ + public @Nullable String getProduct() { + return product; + } + + /** + * Gets the region + * + * @return the region + */ + public @Nullable String getRegion() { + return region; + } + + /** + * Gets the language + * + * @return the language + */ + public @Nullable String getLanguage() { + return language; + } + + /** + * Gets the model + * + * @return the model + */ + public @Nullable String getModel() { + return model; + } + + /** + * Gets the serial + * + * @return the serial + */ + public @Nullable String getSerial() { + return serial; + } + + /** + * Gets the mac address + * + * @return the mac address + */ + public @Nullable String getMacAddr() { + return macAddr; + } + + /** + * Gets the name + * + * @return the name + */ + public @Nullable String getName() { + return name; + } + + /** + * Gets the generation + * + * @return the generation + */ + public @Nullable String getGeneration() { + return generation; + } + + /** + * Gets the area + * + * @return the area + */ + public @Nullable String getArea() { + return area; + } + + /** + * Gets the cid + * + * @return the cid + */ + public @Nullable String getCid() { + return cid; + } + + @Override + public String toString() { + return "SystemInformation [product=" + product + ", region=" + region + ", language=" + language + ", model=" + + model + ", serial=" + serial + ", macAddr=" + macAddr + ", name=" + name + ", generation=" + + generation + ", area=" + area + ", cid=" + cid + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SystemSupportedFunction.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SystemSupportedFunction.java new file mode 100644 index 0000000000000..3499bed68c154 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/SystemSupportedFunction.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the system functions supported and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SystemSupportedFunction { + /** The function */ + private @Nullable String option; + + /** The value */ + private @Nullable String value; + + /** + * Constructor used for deserialization only + */ + public SystemSupportedFunction() { + } + + /** + * Gets the function + * + * @return the function + */ + public @Nullable String getOption() { + return option; + } + + /** + * Gets the value + * + * @return the value + */ + public @Nullable String getValue() { + return value; + } + + @Override + public String toString() { + return "SystemSupportedFunction [option=" + option + ", value=" + value + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Target.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Target.java new file mode 100644 index 0000000000000..2261bbbcd7e06 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Target.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a target and is used to specify a target of some operation + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Target { + /** Well known targets */ + public static final String OUTPUTTERMINAL = "outputTerminal"; + + /** The target */ + private final String target; + + /** Constructs the target using a default target */ + public Target() { + this.target = ""; + } + + /** + * Constructs the target using a specified target + * + * @param target a non-null, possibly empty (for default) target + */ + public Target(final String target) { + this.target = target; + } + + /** + * Gets the target + * + * @return the target + */ + public String getTarget() { + return target; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TextFormRequest_1_1.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TextFormRequest_1_1.java new file mode 100644 index 0000000000000..a53f6f2f585be --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TextFormRequest_1_1.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the text form request and is used for serialization only. Note that this class is currently + * broken as the encryption doesn't seem to work (see + * https://pro-bravia.sony.net/develop/integrate/rest-api/doc/Data-Encryption_401146660/index.html) + * + * Versions: + *
    + *
  1. 1.0: string
  2. + *
  3. 1.1: {"encKey":"string", "text":"string"}
  4. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class TextFormRequest_1_1 { + /** The encryption key */ + private final String encKey; + + /** The text to send */ + private final @Nullable String text; + + /** + * Instantiates a new text form request. + * + * @param encKey the non-null, non-empty encryption key + * @param text the possibly null, possibly empty text + */ + public TextFormRequest_1_1(final String encKey, final @Nullable String text) { + SonyUtil.validateNotEmpty(encKey, "encKey cannot be empty"); + this.encKey = encKey; + this.text = text; + } + + /** + * Gets the encryption key + * + * @return the encryption key + */ + public String getEncKey() { + return encKey; + } + + /** + * Gets the text to send + * + * @return the text to send + */ + public @Nullable String getText() { + return text; + } + + @Override + public String toString() { + return "TextFormRequest_1_1 [encKey=" + encKey + ", text=" + text + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TextFormResult.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TextFormResult.java new file mode 100644 index 0000000000000..3d13a57c866a4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TextFormResult.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the current text form and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class TextFormResult { + /** The current text */ + private @Nullable String text; + + /** + * Constructor used for deserialization only + */ + public TextFormResult() { + } + + /** + * Gets the current text + * + * @return the current text + */ + public @Nullable String getText() { + return text; + } + + @Override + public String toString() { + return "TextForm [text=" + text + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TextUrl.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TextUrl.java new file mode 100644 index 0000000000000..0c5cb917e9d48 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TextUrl.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the URL in the onscreen browser and is used for serialization/deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class TextUrl { + /** The url */ + private @Nullable String url; + + /** The title of the page (result only) */ + private @Nullable String title; + + /** The type of page (result only) */ + private @Nullable String type; + + /** The favorite icon (result only) */ + private @Nullable String favicon; + + /** + * Constructor used for deserialization only + */ + public TextUrl() { + } + + /** + * Instantiates a new URL request + * + * @param url the non-null, non-empty url + */ + public TextUrl(final String url) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + this.url = url; + } + + /** + * Gets the url + * + * @return the url + */ + public @Nullable String getUrl() { + return url; + } + + /** + * Gets the page title + * + * @return the page title + */ + public @Nullable String getTitle() { + return title; + } + + /** + * Gets the page type + * + * @return the page type + */ + public @Nullable String getType() { + return type; + } + + /** + * Gets the favorite icon + * + * @return the favorite icon + */ + public @Nullable String getFavicon() { + return favicon; + } + + @Override + public String toString() { + return "TextUrl [url=" + url + ", title=" + title + ", type=" + type + ", favicon=" + favicon + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TvContentVisibility.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TvContentVisibility.java new file mode 100644 index 0000000000000..b397194ea5894 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/TvContentVisibility.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents the TV content visibility and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class TvContentVisibility { + /** The uri of the content */ + private final String uri; + + /** The epg visibility */ + private final @Nullable String epgVisibility; + + /** The channel surfing visibility */ + private final @Nullable String channelSurfingVisibility; + + /** The overall visibility */ + private final @Nullable String visibility; + + /** + * Instantiates a new tv content visibility. + * + * @param uri the non-null, non-empty uri + * @param epgVisibility the epg visibility (null if not specified) + * @param channelSurfingVisibility the channel surfing visibility (null if not specified) + * @param visibility the overall visibility (null if not specified) + */ + public TvContentVisibility(final String uri, final @Nullable String epgVisibility, + final @Nullable String channelSurfingVisibility, final @Nullable String visibility) { + SonyUtil.validateNotEmpty(uri, "uri cannot be null"); + this.uri = uri; + this.epgVisibility = epgVisibility; + this.channelSurfingVisibility = channelSurfingVisibility; + this.visibility = visibility; + } + + /** + * Gets the uri + * + * @return the uri + */ + public String getUri() { + return uri; + } + + /** + * Gets the epg visibility + * + * @return the epg visibility + */ + public @Nullable String getEpgVisibility() { + return epgVisibility; + } + + /** + * Gets the channel surfing visibility + * + * @return the channel surfing visibility + */ + public @Nullable String getChannelSurfingVisibility() { + return channelSurfingVisibility; + } + + /** + * Gets the overall visibility + * + * @return the overall visibility + */ + public @Nullable String getVisibility() { + return visibility; + } + + @Override + public String toString() { + return "TvContentVisibility [uri=" + uri + ", epgVisibility=" + epgVisibility + ", channelSurfingVisibility=" + + channelSurfingVisibility + ", visibility=" + visibility + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Value.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Value.java new file mode 100644 index 0000000000000..a29c33057bf33 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Value.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents a value and is used for serialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Value { + /** The value */ + private final String value; + + /** + * Instantiates a new value + * + * @param value the non-null, non-emty value + */ + public Value(final String value) { + SonyUtil.validateNotEmpty(value, "value cannot be empty"); + this.value = value; + } + + /** + * Gets the value. + * + * @return the value + */ + public String getValue() { + return value; + } + + @Override + public String toString() { + return "Value [value=" + value + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/VideoInfo.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/VideoInfo.java new file mode 100644 index 0000000000000..8d422662bda5a --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/VideoInfo.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The video information class used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class VideoInfo { + /** The video codec */ + private @Nullable String codec; + + /** + * Constructor used for deserialization only + */ + public VideoInfo() { + } + + /** + * Returns the video codec + * + * @return the video codec + */ + public @Nullable String getCodec() { + return codec; + } + + @Override + public String toString() { + return "VideoInfo [codec=" + codec + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Visibility.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Visibility.java new file mode 100644 index 0000000000000..351a9fccffc69 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/Visibility.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class specifies the visibility of an item + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class Visibility { + /** The epg visibility */ + private @Nullable String epgVisibility; + + /** The channel surfing visibility */ + private @Nullable String channelSurfingVisibility; + + /** The visibility of the content */ + private @Nullable String visibility; + + /** + * Constructor used for deserialization only + */ + public Visibility() { + } + + /** + * Gets the EPG visibility + * + * @return the EPG visibility + */ + public @Nullable String getEpgVisibility() { + return epgVisibility; + } + + /** + * Gets the channel surfing visibility + * + * @return the channel surfing visibility + */ + public @Nullable String getChannelSurfingVisibility() { + return channelSurfingVisibility; + } + + /** + * Gets the general visibility + * + * @return the general visibility + */ + public @Nullable String getVisibility() { + return visibility; + } + + @Override + public String toString() { + return "Visibility [epgVisibility=" + epgVisibility + ", channelSurfingVisibility=" + channelSurfingVisibility + + ", visibility=" + visibility + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/VolumeInformation_1_0.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/VolumeInformation_1_0.java new file mode 100644 index 0000000000000..dcdbe9004ae09 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/VolumeInformation_1_0.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the volume information and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class VolumeInformation_1_0 { + /** The target of the volume */ + private @Nullable String target; + + /** The current volume */ + private @Nullable Integer volume; + + /** Whether muted or not */ + private @Nullable Boolean mute; + + /** The max volume level */ + private @Nullable Integer maxVolume; + + /** The min volume level */ + private @Nullable Integer minVolume; + + /** + * Constructor used for deserialization only + */ + public VolumeInformation_1_0() { + } + + /** + * Gets the target of the volume + * + * @return the target of the volume + */ + public @Nullable String getTarget() { + return target; + } + + /** + * Gets the current volume + * + * @return the current volume + */ + public @Nullable Integer getVolume() { + return volume; + } + + /** + * Whether the target is muted + * + * @return true if muted, false otherwise + */ + public @Nullable Boolean isMute() { + return mute; + } + + /** + * Gets the maximum volume level + * + * @return the maximum volume level + */ + public @Nullable Integer getMaxVolume() { + return maxVolume; + } + + /** + * Gets the minimum volume level + * + * @return the minimum volume level + */ + public @Nullable Integer getMinVolume() { + return minVolume; + } + + @Override + public String toString() { + return "VolumeInformation_1_0 [target=" + target + ", volume=" + volume + ", mute=" + mute + ", maxVolume=" + + maxVolume + ", minVolume=" + minVolume + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/VolumeInformation_1_1.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/VolumeInformation_1_1.java new file mode 100644 index 0000000000000..e208898236238 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/VolumeInformation_1_1.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the volume information and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class VolumeInformation_1_1 { + /** The constant for turning mute ON */ + public static final String MUTEON = "on"; + + /** The constant for turning mute OFF */ + public static final String MUTEOFF = "off"; + + /** The output of the volume */ + private @Nullable String output; + + /** The current volume */ + private @Nullable Integer volume; + + /** Whether muted or not */ + private @Nullable String mute; + + /** The max volume level */ + private @Nullable Integer maxVolume; + + /** The min volume level */ + private @Nullable Integer minVolume; + + /** + * Constructor used for deserialization only + */ + public VolumeInformation_1_1() { + } + + /** + * Gets the output of the volume + * + * @return the output of the volume + */ + public @Nullable String getOutput() { + return output; + } + + /** + * Gets the current volume + * + * @return the current volume + */ + public @Nullable Integer getVolume() { + return volume; + } + + /** + * Get's the mute setting + * + * @return the mute setting + */ + public @Nullable String getMute() { + return mute; + } + + /** + * Determines if the channel is muted or not + * + * @return true if muted, false otherwise + */ + public boolean isMute() { + return MUTEON.equalsIgnoreCase(mute); + } + + /** + * Gets the maximum volume level + * + * @return the maximum volume level + */ + public @Nullable Integer getMaxVolume() { + return maxVolume; + } + + /** + * Gets the minimum volume level + * + * @return the minimum volume level + */ + public @Nullable Integer getMinVolume() { + return minVolume; + } + + @Override + public String toString() { + return "VolumeInformation_1_1 [output=" + output + ", volume=" + volume + ", mute=" + mute + ", maxVolume=" + + maxVolume + ", minVolume=" + minVolume + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/WebAppStatus.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/WebAppStatus.java new file mode 100644 index 0000000000000..e9bd6489d0294 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/WebAppStatus.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the web application status and is used for deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class WebAppStatus { + /** The url of the application */ + private @Nullable String url; + + /** Whether active or not */ + private @Nullable Boolean active; + + /** + * Constructor used for deserialization only + */ + public WebAppStatus() { + } + + /** + * Gets the url of the application + * + * @return the url of the application + */ + public @Nullable String getUrl() { + return url; + } + + /** + * Checks if the application is active + * + * @return true if active, false otherwise + */ + public @Nullable Boolean isActive() { + return active; + } + + @Override + public String toString() { + return "WebAppStatus [active=" + active + ", url=" + url + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/WolMode.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/WolMode.java new file mode 100644 index 0000000000000..d0491c32da10d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/models/api/WolMode.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.models.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class represents the wake on lan (WOL) value and is used for serialization/deserialization only + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class WolMode { + /** Whether enabled or not */ + private @Nullable Boolean enabled; + + /** + * Constructor used for deserialization only + */ + public WolMode() { + } + + /** + * Constructs the WOL mode + * + * @param enabled true for enabled, false otherwise + */ + public WolMode(final boolean enabled) { + this.enabled = enabled; + } + + /** + * Checks if WOL is enabled + * + * @return true if enabled, false otherwise + */ + public @Nullable Boolean isEnabled() { + return enabled; + } + + @Override + public String toString() { + return "WolMode [enabled=" + enabled + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/AbstractScalarWebProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/AbstractScalarWebProtocol.java new file mode 100644 index 0000000000000..cdacea437ad1e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/AbstractScalarWebProtocol.java @@ -0,0 +1,1129 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.providers.SonyDynamicStateProvider; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelTracker; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebError; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebEvent; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.GeneralSetting; +import org.openhab.binding.sony.internal.scalarweb.models.api.GeneralSettingsCandidate; +import org.openhab.binding.sony.internal.scalarweb.models.api.GeneralSettingsRequest; +import org.openhab.binding.sony.internal.scalarweb.models.api.GeneralSettings_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.Notification; +import org.openhab.binding.sony.internal.scalarweb.models.api.Notifications; +import org.openhab.binding.sony.internal.scalarweb.models.api.NotifySettingUpdate; +import org.openhab.binding.sony.internal.scalarweb.models.api.NotifySettingUpdateApi; +import org.openhab.binding.sony.internal.scalarweb.models.api.NotifySettingUpdateApiMapping; +import org.openhab.binding.sony.internal.scalarweb.models.api.Source; +import org.openhab.binding.sony.internal.scalarweb.models.api.Target; +import org.openhab.binding.sony.internal.transports.SonyTransportListener; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.openhab.core.types.StateOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class represents the base of all scalar web protocols + * + * @author Tim Roberts - Initial contribution + * @param the generic type for the callback + */ +@NonNullByDefault +public abstract class AbstractScalarWebProtocol<@NonNull T extends ThingCallback> implements ScalarWebProtocol { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(AbstractScalarWebProtocol.class); + + // the property key to the setting type (boolean, number, etc) + private static final String PROP_SETTINGTYPE = "settingType"; + + // the property key to the device ui setting (slider, etc) + private static final String PROP_DEVICEUI = "deviceUi"; + + // following property only valid on device UI slider items + // we need to save curr value if an increase/decrease type comes in + private static final String PROP_CURRVALUE = "currValue"; + + // The off value (used for the boolean off/false value and the current value of a number channel if mute was pressed + // {saved when OFF, restored on ON}) + private static final String PROP_OFFVALUE = "offValue"; + + // The on value (used for boolean on/true value) + private static final String PROP_ONVALUE = "onValue"; + + /** The context to use */ + private final ScalarWebContext context; + + /** The specific service for the protocol */ + protected final ScalarWebService service; + + /** The callback to use */ + protected final T callback; + + /** The factory used to for protocols */ + private final ScalarWebProtocolFactory factory; + + /** The listener for transport events */ + private final Listener listener = new Listener(); + + /** The API to category lookup for general settings (note: do we support version?) */ + private final Map apiToCtgy = new ConcurrentHashMap<>(); + + /** + * Instantiates a new abstract scalar web protocol. + * + * @param factory the non-null factory to use + * @param context the non-null context to use + * @param service the non-null web service to use + * @param callback the non-null callback to use + */ + protected AbstractScalarWebProtocol(final ScalarWebProtocolFactory factory, final ScalarWebContext context, + final ScalarWebService service, final @NonNull T callback) { + this.factory = factory; + this.context = context; + this.service = service; + this.callback = callback; + } + + /** + * Helper method to enable a list of notifications + * + * @param notificationEvents the list of notifications to enable + * @return the notifications that were enabled/disabled as a result + */ + protected Notifications enableNotifications(final String... notificationEvents) { + if (service.hasMethod(ScalarWebMethod.SWITCHNOTIFICATIONS)) { + try { + final Notifications notifications = execute(ScalarWebMethod.SWITCHNOTIFICATIONS, new Notifications()) + .as(Notifications.class); + + final Set registered = new HashSet<>(Arrays.asList(notificationEvents)); + + final List newEnabled = new ArrayList<>(notifications.getEnabled()); + final List newDisabled = new ArrayList<>(notifications.getDisabled()); + for (final Iterator iter = newDisabled.listIterator(); iter.hasNext();) { + final Notification not = iter.next(); + final String mthName = not.getName(); + + if (mthName != null && registered.contains(mthName)) { + newEnabled.add(not); + iter.remove(); + } + } + + if (newEnabled.isEmpty()) { + // return the original (since nothing changed) + return notifications; + } else { + this.service.getTransport().addListener(listener); + // return the results rather than what we feed it since the server may reject some of ours + return execute(ScalarWebMethod.SWITCHNOTIFICATIONS, new Notifications(newEnabled, newDisabled)) + .as(Notifications.class); + } + } catch (final IOException e) { + logger.debug("switchNotifications doesn't exist - ignoring event processing"); + final List disabled = Arrays.stream(notificationEvents) + .map(s -> new Notification(s, ScalarWebMethod.V1_0)).collect(Collectors.toList()); + return new Notifications(Collections.emptyList(), disabled); + } + } + + final List disabled = Arrays.stream(notificationEvents) + .map(s -> new Notification(s, ScalarWebMethod.V1_0)).collect(Collectors.toList()); + return new Notifications(Collections.emptyList(), disabled); + } + + /** + * Default implementation for the eventReceived and does nothing + * + * @param event the event + * @throws IOException never thrown by the default implementation + */ + protected void eventReceived(final ScalarWebEvent event) throws IOException { + // do nothing + } + + /** + * Returns the {@link ScalarWebService} for the give service name + * + * @param serviceName a non-null, non-empty service name + * @return a {@link ScalarWebService} or null if not found + */ + protected @Nullable ScalarWebService getService(final String serviceName) { + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + if (SonyUtil.equals(serviceName, service.getServiceName())) { + return service; + } + + final ScalarWebProtocol protocol = factory.getProtocol(serviceName); + return protocol == null ? null : protocol.getService(); + } + + /** + * Returns the protocol factory used by this protocol + * + * @return a non-null {@link ScalarWebProtocolFactory} + */ + protected ScalarWebProtocolFactory getFactory() { + return factory; + } + + /** + * Returns the context used for the protocol + * + * @return a non-null {@link ScalarWebContext} + */ + protected ScalarWebContext getContext() { + return context; + } + + /** + * Returns the service related to this protocol + * + * @return the non-null service + */ + @Override + public ScalarWebService getService() { + return service; + } + + /** + * Execute the given method name with the specified parameters + * + * @param mthd a non-null non-empty method + * @param getParms a non-null get parameters method + * @return the scalar web result + * @throws IOException Signals that an I/O exception has occurred. + */ + protected ScalarWebResult execute(final String mthd, final GetParms getParms) throws IOException { + SonyUtil.validateNotEmpty(mthd, "mthd cannot be empty"); + Objects.requireNonNull(getParms, "getParms cannot be empty"); + final String version = getService().getVersion(mthd); + if (version == null || version.isEmpty()) { + logger.debug("Can't find a version for method {} - ignoring", mthd); + return ScalarWebResult.createNotImplemented(mthd); + } + final Object parms = getParms.getParms(version); + return execute(mthd, parms == null ? new Object[0] : parms); + } + + /** + * Execute the given method name with the specified parameters + * + * @param mthd a non-null non-empty method + * @param parms the parameters to use + * @return the scalar web result + * @throws IOException Signals that an I/O exception has occurred. + */ + protected ScalarWebResult execute(final String mthd, final Object... parms) throws IOException { + SonyUtil.validateNotEmpty(mthd, "mthd cannot be empty"); + final ScalarWebResult result = handleExecute(mthd, parms); + if (result.isError()) { + throw result.getHttpResponse().createException(); + } + + return result; + } + + /** + * Handles the execution of a method with parameters + * + * @param mthd a non-null non-empty method + * @param getParms a non-null get parameters method + * @return a non-null result + */ + protected ScalarWebResult handleExecute(final String mthd, final GetParms getParms) { + SonyUtil.validateNotEmpty(mthd, "mthd cannot be empty"); + Objects.requireNonNull(getParms, "getParms cannot be empty"); + + final String version = getService().getVersion(mthd); + if (version == null || version.isEmpty()) { + logger.debug("Can't find a version for method {} - ignoring", mthd); + return ScalarWebResult.createNotImplemented(mthd); + } + final Object parms = getParms.getParms(version); + return handleExecute(mthd, parms == null ? new Object[0] : parms); + } + + /** + * Handles the execution of a method with parameters + * + * @param mthd the method name to execute + * @param parms the parameters to use + * @return the scalar web result + */ + protected ScalarWebResult handleExecute(final String mthd, final Object... parms) { + SonyUtil.validateNotEmpty(mthd, "mthd cannot be empty"); + final ScalarWebResult result = service.execute(mthd, parms); + if (result.isError()) { + switch (result.getDeviceErrorCode()) { + case ScalarWebError.NOTIMPLEMENTED: + logger.debug("Method is not implemented on service {} - {}({}): {}", service.getServiceName(), mthd, + Arrays.stream(parms).map(String::valueOf).collect(Collectors.joining(",")), + result.getDeviceErrorDesc()); + break; + + case ScalarWebError.ILLEGALARGUMENT: + logger.debug("Method arguments are incorrect on service {} - {}({}): {}", service.getServiceName(), + mthd, Arrays.stream(parms).map(String::valueOf).collect(Collectors.joining(",")), + result.getDeviceErrorDesc()); + break; + + case ScalarWebError.ILLEGALSTATE: + logger.debug("Method state is incorrect on service {} - {}({}): {}", service.getServiceName(), mthd, + Arrays.stream(parms).map(String::valueOf).collect(Collectors.joining(",")), + result.getDeviceErrorDesc()); + break; + + case ScalarWebError.DISPLAYISOFF: + logger.debug("The display is off and command cannot be executed on service {} - {}({}): {}", + service.getServiceName(), mthd, + Arrays.stream(parms).map(String::valueOf).collect(Collectors.joining(",")), + result.getDeviceErrorDesc()); + break; + + case ScalarWebError.FAILEDTOLAUNCH: + logger.debug("The application failed to launch (probably display is off) {} - {}({}): {}", + service.getServiceName(), mthd, + Arrays.stream(parms).map(String::valueOf).collect(Collectors.joining(",")), + result.getDeviceErrorDesc()); + break; + + case ScalarWebError.HTTPERROR: + final IOException e = result.getHttpResponse().createException(); + logger.debug("Communication error executing method {}({}) on service {}: {}", mthd, + Arrays.stream(parms).map(String::valueOf).collect(Collectors.joining(",")), + service.getServiceName(), e.getMessage(), e); + callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + break; + + default: + logger.debug("Device error ({}) on service {} - {}({}): {}", result.getDeviceErrorCode(), + service.getServiceName(), mthd, + Arrays.stream(parms).map(String::valueOf).collect(Collectors.joining(",")), + result.getDeviceErrorDesc()); + break; + } + } + + return result; + } + + /** + * Creates a scalar web channel for the given id with potentially additional + * paths + * + * @param id the non-null, non-empty channel identifier + * @return the scalar web channel + */ + protected ScalarWebChannel createChannel(final String id) { + SonyUtil.validateNotEmpty(id, "id cannot be empty"); + return createChannel(id, id, new String[0]); + } + + /** + * Creates a scalar web channel for the given id with potentially additional + * paths + * + * @param category the non-null, non-empty channel category + * @param id the non-null, non-empty channel identifier + * @param addtlPaths the potential other paths + * @return the scalar web channel + */ + protected ScalarWebChannel createChannel(final String category, final String id, final String... addtlPaths) { + SonyUtil.validateNotEmpty(category, "category cannot be empty"); + SonyUtil.validateNotEmpty(id, "id cannot be empty"); + return new ScalarWebChannel(service.getServiceName(), category, id, addtlPaths); + } + + /** + * Creates the channel descriptor for the given channel, item type and channel + * type + * + * @param channel the non-null channel + * @param acceptedItemType the non-null, non-empty accepted item type + * @param channelType the non-null, non-empty channel type + * @return the scalar web channel descriptor + */ + protected ScalarWebChannelDescriptor createDescriptor(final ScalarWebChannel channel, final String acceptedItemType, + final String channelType) { + Objects.requireNonNull(channel, "channel cannot be empty"); + SonyUtil.validateNotEmpty(acceptedItemType, "acceptedItemType cannot be empty"); + SonyUtil.validateNotEmpty(channelType, "channelType cannot be empty"); + return createDescriptor(channel, acceptedItemType, channelType, null, null); + } + + /** + * Creates the descriptor from the given parameters + * + * @param channel the non-null channel + * @param acceptedItemType the non-null, non-empty accepted item type + * @param channelType the non-null, non-empty channel type + * @param label the potentially null, potentially empty label + * @param description the potentially null, potentially empty description + * @return the scalar web channel descriptor + */ + protected ScalarWebChannelDescriptor createDescriptor(final ScalarWebChannel channel, final String acceptedItemType, + final String channelType, final @Nullable String label, final @Nullable String description) { + Objects.requireNonNull(channel, "channel cannot be empty"); + SonyUtil.validateNotEmpty(acceptedItemType, "acceptedItemType cannot be empty"); + SonyUtil.validateNotEmpty(channelType, "channelType cannot be empty"); + return new ScalarWebChannelDescriptor(channel, acceptedItemType, channelType, label, description); + } + + /** + * Helper method to issue a state changed for the simple id (where category=id) + * + * @param id the non-null, non-empty id + * @param state the non-null new state + */ + protected void stateChanged(final String id, final State state) { + SonyUtil.validateNotEmpty(id, "id cannot be empty"); + Objects.requireNonNull(state, "state cannot be empty"); + stateChanged(id, id, state); + } + + /** + * Helper method to issue a state changed for the ctgy/id + * + * @param category the non-null, non-empty category + * @param id the non-null, non-empty id + * @param state the non-null new state + */ + protected void stateChanged(final String category, final String id, final State state) { + SonyUtil.validateNotEmpty(category, "category cannot be empty"); + SonyUtil.validateNotEmpty(id, "id cannot be empty"); + Objects.requireNonNull(state, "state cannot be empty"); + + callback.stateChanged( + SonyUtil.createChannelId(service.getServiceName(), ScalarWebChannel.createChannelId(category, id)), + state); + } + + /** + * Returns the channel tracker + * + * @return the non-null channel tracker + */ + protected ScalarWebChannelTracker getChannelTracker() { + return context.getTracker(); + } + + /** + * Helper method to return a method's latest version + * + * @param methodName a non-null, non-empty method name + * @return a possibly null (if not found) method's latest version + */ + protected @Nullable String getVersion(final String methodName) { + SonyUtil.validateNotEmpty(methodName, "methodName cannot be empty"); + return getService().getVersion(methodName); + } + + /** + * Adds one or more general settings descriptors based on the general settings retrived from the given menthod + * + * @param descriptors a non-null, possibly empty list of descriptors + * @param getMethodName a non-null, non-empty get method name to retrieve settings from + * @param ctgy a non-null, non-empty openhab category + * @param prefix a non-null, non-empty prefix for descriptor names/labels + */ + protected void addGeneralSettingsDescriptor(final List descriptors, + final String getMethodName, final String ctgy, final String prefix) { + Objects.requireNonNull(descriptors, "descriptors cannot be null"); + SonyUtil.validateNotEmpty(getMethodName, "getMethodName cannot be empty"); + SonyUtil.validateNotEmpty(ctgy, "ctgy cannot be empty"); + SonyUtil.validateNotEmpty(prefix, "prefix cannot be empty"); + + try { + // TODO support versioning of the getMethodName? + final GeneralSettings_1_0 ss = execute(getMethodName, new Target()).as(GeneralSettings_1_0.class); + + for (final GeneralSetting set : ss.getSettings()) { + // Seems isAvailable is not a reliable call (many false when in fact it's true) + // if (!set.isAvailable()) { + // logger.debug("{} isn't available {} - ignoring", prefix, set); + // continue; + // } + final String target = set.getTarget(); + if (target == null || target.isEmpty()) { + logger.debug("Target not valid for {} {} - ignoring", prefix, set); + continue; + } + + final String uri = SonyUtil.defaultIfEmpty(set.getUri(), null); + final String uriLabel = uri == null ? null : SonyUtil.defaultIfEmpty(Source.getSourcePart(uri), null); + + final String label = textLookup(set.getTitle(), target) + + (uriLabel == null ? "" : String.format(" (%s)", uriLabel)); + final String id = getGeneralSettingChannelId(target, uri); + final List candidates = SonyUtil.convertNull(set.getCandidate()); + + String settingType = set.getType(); + // -- no explicit type - try guessing it from the value + if (settingType == null || settingType.isEmpty()) { + String currValue = set.getCurrentValue(); + if (currValue == null || currValue.isEmpty()) { + // No value - get the first non-null/non-empty one from the candidates + currValue = candidates.stream().map(e -> e.getValue()).filter(e -> !e.isEmpty()).findFirst() + .orElse(null); + } + + if (SonyUtil.toBooleanObject(currValue) != null) { + // we'll further validate the boolean target candidates below + settingType = GeneralSetting.BOOLEANTARGET; + } else if (SonyUtil.isNumeric(currValue)) { + settingType = GeneralSetting.INTEGERTARGET; + } else if (SonyUtil.isNumber(currValue)) { + settingType = GeneralSetting.DOUBLETARGET; + } else { + settingType = !candidates.isEmpty() ? GeneralSetting.ENUMTARGET : GeneralSetting.STRINGTARGET; + } + } + + final ScalarWebChannel channel = createChannel(ctgy, id != null ? id : "", target, + uri != null ? uri : ""); + + final String ui = set.getDeviceUIInfo(); + if (ui == null || ui.isEmpty()) { + if (!candidates.isEmpty()) { + final GeneralSettingsCandidate candidate = candidates.get(0); + if (candidate != null && candidate.getMax() != null && candidate.getMin() != null + && candidate.getStep() != null) { + channel.addProperty(PROP_DEVICEUI, GeneralSetting.SLIDER); + } + } + } else { + channel.addProperty(PROP_DEVICEUI, ui); + } + channel.addProperty(PROP_SETTINGTYPE, settingType); + + StateDescriptionFragmentBuilder bld = StateDescriptionFragmentBuilder.create(); + if (candidates.isEmpty()) { + bld = bld.withReadOnly(Boolean.TRUE); + } + + // Make sure we actually have a boolean target: + // 1. Must be 2 in size + // 2. Both need to be a valid Boolean (must be on/off, true/false, yes/no) + // 3. Both cannot be the same (must be true/false or false/true) + // If all three aren't true - revert to an enum target + // If they are true - save the actual value used for the boolean (on/off or true/false or yes/no) + if (SonyUtil.equals(settingType, GeneralSetting.BOOLEANTARGET) && !candidates.isEmpty()) { + if (candidates.size() != 2) { + settingType = GeneralSetting.ENUMTARGET; + } else { + final @Nullable String value1 = candidates.get(0).getValue(); + final @Nullable String value2 = candidates.get(1).getValue(); + final @Nullable Boolean bool1 = SonyUtil.toBooleanObject(value1); + final @Nullable Boolean bool2 = SonyUtil.toBooleanObject(value2); + + if (value1 == null || value2 == null || bool1 == null || bool2 == null || bool1.equals(bool2)) { + settingType = GeneralSetting.ENUMTARGET; + } else { + channel.addProperty(PROP_OFFVALUE, bool1.equals(Boolean.FALSE) ? value1 : value2); + channel.addProperty(PROP_ONVALUE, bool1.equals(Boolean.TRUE) ? value1 : value2); + } + } + } + + switch (settingType) { + case GeneralSetting.BOOLEANTARGET: + descriptors.add(createDescriptor(channel, "Switch", "scalargeneralsettingswitch", + prefix + " " + label, prefix + " for " + label)); + + break; + case GeneralSetting.DOUBLETARGET: + if (set.isUiSlider()) { + descriptors.add(createDescriptor(channel, "Dimmer", "scalargeneralsettingdimmer", + prefix + " " + label, prefix + " for " + label)); + + } else { + descriptors.add(createDescriptor(channel, "Number", "scalargeneralsettingnumber", + prefix + " " + label, prefix + " for " + label)); + } + + if (!candidates.isEmpty()) { + final GeneralSettingsCandidate candidate = candidates.get(0); + + final Double min = candidate.getMin(), max = candidate.getMax(), step = candidate.getStep(); + if (min != null || max != null || step != null) { + final List options = new ArrayList<>(); + if (set.isUiPicker()) { + final double dmin = min == null || min.isInfinite() || min.isNaN() ? 0 + : min.doubleValue(); + final double dmax = max == null || max.isInfinite() || max.isNaN() ? 100 + : max.doubleValue(); + final double dstep = step == null || step.isInfinite() || step.isNaN() ? 1 + : step.doubleValue(); + for (double p = dmin; p <= dmax; p += (dstep == 0 ? 1 : dstep)) { + final String opt = Double.toString(p); + options.add(new StateOption(opt, opt)); + } + } + if (min != null) { + bld = bld.withMinimum(new BigDecimal(min)); + } + if (max != null) { + bld = bld.withMaximum(new BigDecimal(max)); + } + if (step != null) { + bld = bld.withStep(new BigDecimal(step)); + } + if (!options.isEmpty()) { + bld = bld.withOptions(options); + } + } + } + + break; + case GeneralSetting.INTEGERTARGET: + if (set.isUiSlider()) { + descriptors.add(createDescriptor(channel, "Dimmer", "scalargeneralsettingdimmer", + prefix + " " + label, prefix + " for " + label)); + + } else { + descriptors.add(createDescriptor(channel, "Number", "scalargeneralsettingnumber", + prefix + " " + label, prefix + " for " + label)); + } + + if (!candidates.isEmpty()) { + final GeneralSettingsCandidate candidate = candidates.get(0); + + final Double min = candidate.getMin(), max = candidate.getMax(), step = candidate.getStep(); + if (min != null || max != null || step != null) { + final List options = new ArrayList<>(); + if (set.isUiPicker()) { + final int imin = min == null || min.isInfinite() || min.isNaN() ? 0 + : min.intValue(); + final int imax = max == null || max.isInfinite() || max.isNaN() ? 100 + : max.intValue(); + final int istep = step == null || step.isInfinite() || step.isNaN() ? 1 + : step.intValue(); + for (int p = imin; p <= imax; p += (istep == 0 ? 1 : istep)) { + final String opt = Double.toString(p); + options.add(new StateOption(opt, opt)); + } + } + + if (min != null) { + bld = bld.withMinimum(new BigDecimal(min)); + } + if (max != null) { + bld = bld.withMaximum(new BigDecimal(max)); + } + if (step != null) { + bld = bld.withStep(new BigDecimal(step)); + } + if (!options.isEmpty()) { + bld = bld.withOptions(options); + } + } + } + + break; + + case GeneralSetting.ENUMTARGET: + descriptors.add(createDescriptor(channel, "String", "scalargeneralsettingstring", + prefix + " " + label, prefix + " for " + label)); + + final List stateInfo = candidates.stream().map(c -> { + Optional so = Optional.empty(); + if (c != null) { + final String stateVal = c.getValue(); + final String stateTitle = textLookup(c.getTitle(), null); + if (stateVal != null && !stateVal.isEmpty() && stateTitle != null + && !stateTitle.isEmpty()) { + so = Optional.of(new StateOption(stateVal, stateTitle)); + } + } + return so; + }).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList()); + + if (!stateInfo.isEmpty()) { + bld = bld.withOptions(stateInfo); + } + + break; + default: + descriptors.add(createDescriptor(channel, "String", "scalargeneralsettingstring", + prefix + " " + label, prefix + " for " + label)); + break; + } + + final StateDescription sd = bld.build().toStateDescription(); + if (sd != null) { + getContext().getStateProvider().addStateOverride(getContext().getThingUID(), channel.getChannelId(), + sd); + } + } + + apiToCtgy.put(getMethodName, ctgy); + + } catch (final IOException e) { + // ignore - probably not handled + } + } + + /** + * Helper method to create a general settings channel id + * + * @param target a non-null, non-empty target + * @param uri a possibly null, possibly empty uri + * @return + */ + private @Nullable String getGeneralSettingChannelId(String target, @Nullable String uri) { + SonyUtil.validateNotEmpty(target, "target cannot be empty"); + return SonyUtil.createValidChannelUId(target + (uri == null || uri.isEmpty() ? "" : "-" + uri)); + } + + /** + * Refreshs the general sttings for a list of channels + * + * @param channels a non-null, possibly empty set of {@link ScalarWebChannel} + * @param getMethodName a non-null, non-empty method name to get settings from + */ + protected void refreshGeneralSettings(final Set channels, final String getMethodName) { + Objects.requireNonNull(channels, "channels cannot be null"); + SonyUtil.validateNotEmpty(getMethodName, "getMethodName cannot be empty"); + + try { + final GeneralSettings_1_0 ss = handleExecute(getMethodName, new Target()).as(GeneralSettings_1_0.class); + refreshGeneralSettings(ss.getSettings(), channels); + } catch (final IOException e) { + logger.debug("Error in refreshing general settings: {}", e.getMessage(), e); + } + } + + /** + * Refreshs the general settings for a list of channels and their openHAB category + * + * @param settings a non-null, possibly empty list of {@link GeneralSetting} + * @param channels a non-null, possibly empty set of {@link ScalarWebChannel} + */ + protected void refreshGeneralSettings(final List settings, final Set channels) { + Objects.requireNonNull(settings, "settings cannot be null"); + Objects.requireNonNull(channels, "channels cannot be null"); + + final Map, GeneralSetting> settingValues = new HashMap<>(); + for (final GeneralSetting set : settings) { + final String target = set.getTarget(); + if (target == null || target.isEmpty()) { + continue; + } + final Map.Entry key = new AbstractMap.SimpleEntry<>(target, + SonyUtil.defaultIfEmpty(set.getUri(), "")); + settingValues.put(key, set); + } + + for (final ScalarWebChannel chl : channels) { + final String target = chl.getPathPart(0); + if (target == null) { + logger.debug("Cannot refresh general setting {} - has no target", chl); + continue; + } + + final String uri = SonyUtil.defaultIfEmpty(chl.getPathPart(1), ""); + final Map.Entry key = new AbstractMap.SimpleEntry<>(target, uri); + + final GeneralSetting setting = settingValues.get(key); + if (setting == null) { + logger.debug("Could not find a setting for {} ({})", target, uri); + continue; + } + + final String currentValue = setting.getCurrentValue(); + + final String settingType = chl.getProperty(PROP_SETTINGTYPE, + SonyUtil.defaultIfEmpty(setting.getType(), GeneralSetting.STRINGTARGET)); + final String ui = chl.getProperty(PROP_DEVICEUI, SonyUtil.defaultIfEmpty(setting.getDeviceUIInfo(), "")); + + switch (settingType) { + case GeneralSetting.BOOLEANTARGET: + final String onValue = SonyUtil.defaultIfEmpty(chl.getProperty(PROP_ONVALUE), + GeneralSetting.DEFAULTON); + + stateChanged(chl.getCategory(), chl.getId(), + currentValue.equalsIgnoreCase(onValue) ? OnOffType.ON : OnOffType.OFF); + break; + + case GeneralSetting.DOUBLETARGET: + case GeneralSetting.INTEGERTARGET: + if (ui.toLowerCase().contains(GeneralSetting.SLIDER.toLowerCase())) { + final StateDescription sd = getContext().getStateProvider() + .getStateDescription(getContext().getThingUID(), chl.getChannelId()); + final BigDecimal min = sd == null ? BigDecimal.ZERO : sd.getMinimum(); + final BigDecimal max = sd == null ? SonyUtil.BIGDECIMAL_HUNDRED : sd.getMaximum(); + try { + final BigDecimal currVal = currentValue == null ? BigDecimal.ZERO + : new BigDecimal(currentValue); + final BigDecimal val = SonyUtil.scale(currVal, min, max); + chl.addProperty(PROP_CURRVALUE, currVal.toString()); + + if (settingType.equals(GeneralSetting.INTEGERTARGET)) { + stateChanged(chl.getCategory(), chl.getId(), + SonyUtil.newPercentType(val.setScale(0, RoundingMode.FLOOR))); + } else { + stateChanged(chl.getCategory(), chl.getId(), SonyUtil.newPercentType(val)); + } + } catch (final NumberFormatException e) { + logger.debug("Current value {} was not a valid integer", currentValue); + } + } else { + stateChanged(chl.getCategory(), chl.getId(), SonyUtil.newDecimalType(currentValue)); + } + break; + + default: + stateChanged(chl.getCategory(), chl.getId(), SonyUtil.newStringType(currentValue)); + break; + } + } + } + + /** + * Sets a general setting. This method will take a method and channel and execute a command against it. + * + * @param method a non-null, non-empty method + * @param chl a non-null channel describing the setting + * @param cmd a non-null command to execute + */ + protected void setGeneralSetting(final String method, final ScalarWebChannel chl, final Command cmd) { + SonyUtil.validateNotEmpty(method, "method cannot be empty"); + Objects.requireNonNull(chl, "chl cannot be null"); + Objects.requireNonNull(cmd, "cmd cannot be null"); + + final String target = SonyUtil.defaultIfEmpty(chl.getPathPart(0), null); + if (target == null) { + logger.debug("Cannot set general setting {} for channel {} because it has no target: {}", method, chl, cmd); + return; + } + + final String uri = SonyUtil.defaultIfEmpty(chl.getPathPart(1), null); + + final String settingType = chl.getProperty(PROP_SETTINGTYPE, GeneralSetting.STRINGTARGET); + final String deviceUi = chl.getProperty(PROP_DEVICEUI); + + final SonyDynamicStateProvider provider = getContext().getStateProvider(); + final StateDescription sd = provider.getStateDescription(getContext().getThingUID(), chl.getChannelId()); + if ((sd != null) && sd.isReadOnly()) { + logger.debug("Method {} ({}) is readonly - ignoring: {}", method, target, cmd); + return; + } + + switch (settingType) { + case GeneralSetting.BOOLEANTARGET: + if (cmd instanceof OnOffType) { + final String onValue = SonyUtil.defaultIfEmpty(chl.getProperty(PROP_ONVALUE), + GeneralSetting.DEFAULTON); + final String offValue = SonyUtil.defaultIfEmpty(chl.getProperty(PROP_OFFVALUE), + GeneralSetting.DEFAULTOFF); + + handleExecute(method, + new GeneralSettingsRequest(target, cmd == OnOffType.ON ? onValue : offValue, uri)); + } else { + logger.debug("{} command not an OnOffType: {}", method, cmd); + } + break; + + case GeneralSetting.DOUBLETARGET: + case GeneralSetting.INTEGERTARGET: + if (deviceUi != null && deviceUi.contains(GeneralSetting.SLIDER)) { + final BigDecimal sdMin = sd == null ? null : sd.getMinimum(); + final BigDecimal sdMax = sd == null ? null : sd.getMaximum(); + final BigDecimal min = sdMin == null ? BigDecimal.ZERO : sdMin; + final BigDecimal max = sdMax == null ? SonyUtil.BIGDECIMAL_HUNDRED : sdMax; + + final String propVal = chl.getProperty(PROP_CURRVALUE, "0"); + final String offPropVal = chl.removeProperty(PROP_OFFVALUE); + + try { + final BigDecimal currVal = SonyUtil.guard(new BigDecimal(propVal), min, max); + + BigDecimal newVal; + if (cmd instanceof OnOffType) { + if (cmd == OnOffType.OFF) { + chl.addProperty(PROP_OFFVALUE, propVal); + newVal = min; + } else { + // if no prior off value, go with min instead + newVal = offPropVal == null ? min + : SonyUtil.guard(new BigDecimal(offPropVal), min, max); + } + + } else if (cmd instanceof PercentType) { + newVal = SonyUtil.unscale(((PercentType) cmd).toBigDecimal(), min, max); + } else if (cmd instanceof IncreaseDecreaseType) { + newVal = SonyUtil.guard(cmd == IncreaseDecreaseType.INCREASE ? currVal.add(BigDecimal.ONE) + : currVal.subtract(BigDecimal.ONE), min, max); + } else { + logger.debug("{} command not an dimmer type: {}", method, cmd); + return; + } + if (settingType.equals(GeneralSetting.INTEGERTARGET)) { + handleExecute(method, + new GeneralSettingsRequest(target, Integer.toString(newVal.intValue()), uri)); + } else { + handleExecute(method, + new GeneralSettingsRequest(target, Double.toString(newVal.doubleValue()), uri)); + } + } catch (final NumberFormatException e) { + logger.debug("{} command current/off value not a valid number - either {} or {}: {}", method, + propVal, offPropVal, e.getMessage()); + } + } else { + if (cmd instanceof DecimalType) { + if (settingType.equals(GeneralSetting.INTEGERTARGET)) { + handleExecute(method, new GeneralSettingsRequest(target, + Integer.toString(((DecimalType) cmd).intValue()), uri)); + } else { + handleExecute(method, new GeneralSettingsRequest(target, + Double.toString(((DecimalType) cmd).doubleValue()), uri)); + } + } else { + logger.debug("{} command not an DecimalType: {}", method, cmd); + } + } + break; + + // handles both String and Enum types + default: + if (cmd instanceof StringType) { + handleExecute(method, new GeneralSettingsRequest(target, ((StringType) cmd).toString(), uri)); + } else { + logger.debug("{} command not an StringType: {}", method, cmd); + } + break; + } + } + + /** + * Called when notifying the protocol of a settings update + * + * @param notify a non-null notify + */ + public void notifySettingUpdate(final NotifySettingUpdate notify) { + Objects.requireNonNull(notify, "notify cannot be null"); + + final @Nullable NotifySettingUpdateApiMapping apiMappingUpdate = notify.getApiMappingUpdate(); + if (apiMappingUpdate == null) { + logger.debug("Received a notifySettingUpdate with no api mapping - ignoring: {}", notify); + return; + } + + final String serviceName = apiMappingUpdate.getService(); + if (serviceName == null || serviceName.isEmpty()) { + logger.debug("Received a notifySettingUpdate with no service name - ignoring: {}", notify); + return; + } + + if (serviceName.equalsIgnoreCase(getService().getServiceName())) { + internalNotifySettingUpdate(notify); + } else { + final @Nullable ScalarWebProtocol protocol = getFactory().getProtocol(serviceName); + if (protocol == null) { + logger.debug("Received a notifySettingUpdate for an unknown service: {} - {}", serviceName, notify); + return; + } + protocol.notifySettingUpdate(notify); + } + } + + /** + * Internal application of the settings update notification. + * + * @param setting a non-null setting + */ + private void internalNotifySettingUpdate(final NotifySettingUpdate setting) { + Objects.requireNonNull(setting, "setting cannot be null"); + + final NotifySettingUpdateApiMapping apiMapping = setting.getApiMappingUpdate(); + if (apiMapping == null) { + logger.debug("Trying to update setting but apiMapping was null: {}", setting); + return; + } + + final String target = apiMapping.getTarget(); + if (target == null || target.isEmpty()) { + logger.debug("Trying to update setting but target is empty: {}", setting); + return; + } + + // Create our settings from the notification (only need a few of the attributes - see refreshGeneralSettings) + final List settings = Collections.singletonList( + new GeneralSetting(target, apiMapping.getUri(), setting.getType(), apiMapping.getCurrentValue())); + + final NotifySettingUpdateApi getApi = apiMapping.getGetApi(); + if (getApi == null) { + logger.debug("Trying to update setting but getAPI was missing: {}", setting); + return; + } + + final String getMethodName = getApi.getName(); + if (getMethodName == null || getMethodName.isEmpty()) { + logger.debug("Trying to update setting but getAPI had no name: {}", setting); + return; + } + + final String ctgy = apiToCtgy.get(getMethodName); + if (ctgy == null || ctgy.isEmpty()) { + logger.debug("Trying to update setting but couldn't find the category for the getAPI {}: {}", getMethodName, + setting); + return; + + } + + final String id = getGeneralSettingChannelId(target, apiMapping.getUri()); + + // Get the channels linked to that category and for that identifier + final Set channels = getChannelTracker().getLinkedChannelsForCategory(ctgy).stream() + .filter(e -> e.getId().equalsIgnoreCase(id)).collect(Collectors.toSet()); + + refreshGeneralSettings(settings, channels); + } + + /** + * Helper function to lookup common sony text and translate to a better name + * + * @param text a possibly null, possibly empty text string + * @param target a posssibly null, possibly empty target (as a text backup) + * @return the translated text or text if not recognized + */ + private static @Nullable String textLookup(final @Nullable String text, final @Nullable String target) { + if ("IDMR_TEXT_FOOTBALL_STRING".equalsIgnoreCase(text)) { + return "Football"; + } + + if ("IDMR_TEXT_NARRATION_OFF_STRING".equalsIgnoreCase(text)) { + return "Narration Off"; + } + + if ("IDMR_TEXT_NARRATION_ON_STRING".equalsIgnoreCase(text)) { + return "Narration On"; + } + + if ("IDMR_TEXT_XMBSETUP_GRACENOTESETTING_STRING".equalsIgnoreCase(text)) { + return "Gracenote"; + } + + if ("IDMR_TEXT_SETUP_PULLDOWN_MANUAL_STRING".equalsIgnoreCase(text)) { + return "Manual"; + } + + if ("IDMR_TEXT_COMMON_AUTO_STRING".equalsIgnoreCase(text)) { + return "Auto"; + } + + if ("IDMR_TEXT_XMBSETUP_REMOTE_START_STRING".equalsIgnoreCase(text)) { + return "Remote Start"; + } + + if ("IDMR_TEXT_BT_DEVICE_NAME_STRING".equalsIgnoreCase(text)) { + return "Device Name"; + } + + if (text != null && !text.isEmpty()) { + return text; + } + + // If we have a target, un-camel case it + return target == null || target.isEmpty() ? "Unknown" + : target.replaceAll(String.format("%s|%s|%s", "(?<=[A-Z])(?=[A-Z][a-z])", "(?<=[^A-Z])(?=[A-Z])", + "(?<=[A-Za-z])(?=[^A-Za-z])"), " "); + } + + @Override + public void close() { + service.getTransport().close(); + } + + /** + * This class represents the listener to sony events and will forward those + * events on to the protocol implementation if they have a method of the same + * name as the event + * + * @author Tim Roberts - Initial contribution + */ + @NonNullByDefault + private class Listener implements SonyTransportListener { + @Override + public void onEvent(final ScalarWebEvent event) { + Objects.requireNonNull(event, "event cannot be null"); + context.getScheduler().execute(() -> { + try { + eventReceived(event); + } catch (final IOException e) { + logger.debug("IOException during event notification: {}", e.getMessage(), e); + } + }); + } + + @Override + public void onError(final Throwable t) { + } + } + + /** + * Functional interface to get parameters + */ + @NonNullByDefault + protected interface GetParms { + /** + * Gets the parameters for a specific version + * + * @param version a non-null, non-empty version + * @return a parameter + */ + @Nullable + Object getParms(String version); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/NotificationHelper.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/NotificationHelper.java new file mode 100644 index 0000000000000..a1142824967b4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/NotificationHelper.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.models.api.Notifications; + +/** + * This helper class provides services to determine if a notification is enabled. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class NotificationHelper { + /** Contains the (readonly) set of notification names that are enabled */ + private final Set notificationNames; + + /** + * Constructs the helper from the notifications + * + * @param notifications a non-null notifications + */ + public NotificationHelper(final Notifications notifications) { + Objects.requireNonNull(notifications, "notifications cannot be null"); + + final Set names = new HashSet<>(); + notifications.getEnabled().stream().map(e -> e.getName()).forEach(e -> { + if (e != null && !e.isEmpty()) { + names.add(e); + } + }); + + notificationNames = Collections.unmodifiableSet(names); + } + + /** + * Determines if the specified notification is enabled or not. + * + * @param name a non-null, non-empty notification name + * @return true if enabled, false otherwise + */ + public boolean isEnabled(final String name) { + SonyUtil.validateNotEmpty(name, "name cannot be empty"); + + return notificationNames.contains(name); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebAppControlProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebAppControlProtocol.java new file mode 100644 index 0000000000000..4cdb3024eb54d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebAppControlProtocol.java @@ -0,0 +1,539 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.ActiveApp; +import org.openhab.binding.sony.internal.scalarweb.models.api.ApplicationList; +import org.openhab.binding.sony.internal.scalarweb.models.api.ApplicationStatusList; +import org.openhab.binding.sony.internal.scalarweb.models.api.TextFormResult; +import org.openhab.binding.sony.internal.scalarweb.models.api.WebAppStatus; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.Command; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The implementation of the protocol handles the AppControl service. + * + * Note: haven't figured out text encryption yet - so I have the basics commented out for future use + * + * @author Tim Roberts - Initial contribution + * @param the generic type for the callback + */ +@NonNullByDefault +class ScalarWebAppControlProtocol<@NonNull T extends ThingCallback> extends AbstractScalarWebProtocol { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebAppControlProtocol.class); + + // Varous channel constants + private static final String APPTITLE = "apptitle"; + private static final String APPICON = "appicon"; + private static final String APPDATA = "appdata"; + private static final String APPSTATUS = "appstatus"; + private static final String TEXTFORM = "textform"; + private static final String STATUS = "status"; + private static final String START = "start"; + private static final String STOP = "stop"; + + // The intervals used for refresh + private static final int APPLISTINTERVAL = 60000; + private static final int ACTIVEAPPINTERVAL = 10000; + + /** The lock used to modify app information */ + private final Lock appListLock = new ReentrantLock(); + + /** The last time the app list was accessed */ + private long appListLastTime = 0; + + /** The applications */ + private final List apps = new CopyOnWriteArrayList(); + + /** The lock used to access activeApp */ + private final Lock webAppStatusLock = new ReentrantLock(); + + /** The last time the activeApp was accessed */ + private long webAppStatusLastTime = 0; + + /** The active app. */ + private @Nullable WebAppStatus webAppStatus = null; + + /** + * Instantiates a new scalar web app control protocol. + * + * @param factory the non-null factory to use + * @param context the non-null context to use + * @param service the non-null service to use + * @param callback the non-null callback to + */ + ScalarWebAppControlProtocol(final ScalarWebProtocolFactory factory, final ScalarWebContext context, + final ScalarWebService service, final @NonNull T callback) { + super(factory, context, service, callback); + } + + @Override + public Collection getChannelDescriptors(final boolean dynamicOnly) { + final Set cache = new HashSet<>(); + final List descriptors = new ArrayList(); + + // everything is dynamic so we ignore the dynamicOnly flag + + for (final ApplicationList app : getApplications()) { + final String uri = app.getUri(); + if (uri == null || uri.isEmpty()) { + logger.debug("uri cannot be empty: {}", app); + continue; + } + + final String title = app.getTitle(); + final String label = SonyUtil.capitalize(title); + + final String origId = SonyUtil.createValidChannelUId(title != null ? title : ""); + + // Make a unique channel ID for this + // We can't solely assume title is unique - there are bravias that have + // different apps that are titled the same (go figure) + // So do "-x" to the title if we have a duplicate + String validId = origId; + int i = 0; + while (cache.contains(validId)) { + validId = origId + "-" + (++i); + } + cache.add(validId); + + final String id = validId; + + descriptors.add(createDescriptor(createChannel(APPTITLE, id, uri), "String", "scalarappcontrolapptitle", + "App " + label + " Title", "Title for application " + label)); + descriptors.add(createDescriptor(createChannel(APPICON, id, uri), "Image", "scalarappcontrolappicon", + "App " + label + " Icon", "Icon for application " + label)); + descriptors.add(createDescriptor(createChannel(APPDATA, id, uri), "String", "scalarappcontrolappdata", + "App " + label + " Data", "Data for application " + label)); + descriptors.add(createDescriptor(createChannel(APPSTATUS, id, uri), "String", "scalarappcontrolappstatus", + "App " + label + " Status", "Status for " + label)); + } + + try { + final List statuses = execute(ScalarWebMethod.GETAPPLICATIONSTATUSLIST) + .asArray(ApplicationStatusList.class); + for (final ApplicationStatusList status : statuses) { + final String name = status.getName(); + if (name == null || name.isEmpty()) { + logger.debug("name cannot be empty: {}", status); + continue; + } + + final String id = SonyUtil.createValidChannelUId(name); + final String title = SonyUtil.capitalize(name); + descriptors.add(createDescriptor(createChannel(STATUS, id, name), "Switch", "scalarappcontrolstatus", + "Indicator " + title, "Indicator for " + title)); + } + } catch (final IOException e) { + logger.debug("Exception getting application status list: {}", e.getMessage()); + } + + // Haven't figured out encryption yet - assume non-encryption format + // try { + // final String textFormVersion = getService().getVersion(ScalarWebMethod.GETTEXTFORM); + // if (VersionUtilities.equals(textFormVersion, ScalarWebMethod.V1_0)) { + if (service.hasMethod(ScalarWebMethod.GETTEXTFORM)) { + descriptors.add(createDescriptor(createChannel(TEXTFORM), "String", "scalarappcontroltextform")); + } + // } else if (VersionUtilities.equals(textFormVersion, ScalarWebMethod.V1_1)) { + // final String localPubKey = pubKey; + // if (localPubKey == null || StringUtils.isEmpty(localPubKey)) { + // logger.debug("Can't get text form - no public key"); + // } else { + // execute(ScalarWebMethod.GETTEXTFORM, new TextFormRequest_1_1(localPubKey, null)); + // descriptors.add(createDescriptor(createChannel(TEXTFORM), "String", "scalarappcontroltextform")); + // } + // } + // } catch (final IOException e) { + // logger.debug("Exception getting text form: {}", e.getMessage()); + // } + + return descriptors; + } + + @Override + public void refreshState(boolean initial) { + final Set appChannels = getChannelTracker().getLinkedChannelsForCategory(APPTITLE, APPICON, + APPDATA, APPSTATUS, TEXTFORM, STATUS); + for (final ScalarWebChannel chl : appChannels) { + refreshChannel(chl); + } + } + + @Override + public void refreshChannel(final ScalarWebChannel channel) { + Objects.requireNonNull(channel, "channel cannot be null"); + + final String ctgy = channel.getCategory(); + if (TEXTFORM.equalsIgnoreCase(ctgy)) { + refreshTextForm(channel.getChannelId()); + } else { + final String[] paths = channel.getPaths(); + if (paths.length == 0) { + logger.debug("Refresh Channel path invalid: {}", channel); + } else { + final String target = paths[0]; + + switch (ctgy) { + case APPTITLE: + refreshAppTitle(channel.getChannelId(), target); + break; + + case APPICON: + refreshAppIcon(channel.getChannelId(), target); + break; + + case APPDATA: + refreshAppData(channel.getChannelId(), target); + break; + + case APPSTATUS: + refreshAppStatus(channel.getChannelId(), target); + break; + + case STATUS: + refreshStatus(channel.getChannelId(), target); + break; + + default: + logger.debug("Unknown refresh channel: {}", channel); + break; + } + } + } + } + + /** + * Gets the active application. Note that this method will cache the active application applications for + * {@link #ACTIVEAPPINTERVAL} milliseconds to prevent excessive retrieving of the application list (applications + * don't change that often!) + * + * @return the active application or null if none + */ + private @Nullable WebAppStatus getWebAppStatus() { + webAppStatusLock.lock(); + try { + final long now = System.currentTimeMillis(); + if (webAppStatus == null || webAppStatusLastTime + ACTIVEAPPINTERVAL < now) { + webAppStatus = execute(ScalarWebMethod.GETWEBAPPSTATUS).as(WebAppStatus.class); + webAppStatusLastTime = now; + } + + } catch (final IOException e) { + // already handled by execute + } finally { + webAppStatusLock.unlock(); + } + + return webAppStatus; + } + + /** + * Gets the list of applications. Note that this method will cache the applications for {@link #APPLISTINTERVAL} + * milliseconds to prevent excessive retrieving of the application list (applications don't change that often!) + * + * @return the non-null, possibly empty unmodifiable list of applications + */ + private List getApplications() { + appListLock.lock(); + try { + final long now = System.currentTimeMillis(); + if (appListLastTime + APPLISTINTERVAL < now) { + apps.clear(); + apps.addAll(execute(ScalarWebMethod.GETAPPLICATIONLIST).asArray(ApplicationList.class)); + appListLastTime = now; + } + + return Collections.unmodifiableList(apps); + } catch (final IOException e) { + // already handled by execute + return Collections.unmodifiableList(apps); + } finally { + appListLock.unlock(); + } + } + + /** + * Gets the application list for the given URI + * + * @param appUri the non-null, non-empty application URI + * @return the application list or null if none + */ + private @Nullable ApplicationList getApplicationList(final String appUri) { + SonyUtil.validateNotEmpty(appUri, "appUri cannot be empty"); + for (final ApplicationList app : getApplications()) { + if (appUri.equalsIgnoreCase(app.getUri())) { + return app; + } + } + return null; + } + + /** + * Refresh app title for the given URI + * + * @param channelId the non-null, non-empty channel ID + * @param appUri the non-null, non-empty application URI + */ + private void refreshAppTitle(final String channelId, final String appUri) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + SonyUtil.validateNotEmpty(appUri, "appUri cannot be empty"); + final ApplicationList app = getApplicationList(appUri); + if (app != null) { + callback.stateChanged(channelId, SonyUtil.newStringType(app.getTitle())); + } + } + + /** + * Refresh app icon for the given URI + * + * @param channelId the non-null, non-empty channel ID + * @param appUri the non-null, non-empty application URI + */ + private void refreshAppIcon(final String channelId, final String appUri) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + SonyUtil.validateNotEmpty(appUri, "appUri cannot be empty"); + final ApplicationList app = getApplicationList(appUri); + if (app != null) { + final String iconUrl = app.getIcon(); + if (iconUrl == null || iconUrl.isEmpty()) { + callback.stateChanged(channelId, UnDefType.UNDEF); + } else { + try (SonyHttpTransport transport = SonyTransportFactory.createHttpTransport( + getService().getTransport().getBaseUri().toString(), getContext().getClientBuilder())) { + final RawType rawType = NetUtil.getRawType(transport, iconUrl); + callback.stateChanged(channelId, rawType == null ? UnDefType.UNDEF : rawType); + } catch (final URISyntaxException e) { + logger.debug("Exception occurred getting application icon: {}", e.getMessage()); + } + } + } + } + + /** + * Refresh app data for the given URI + * + * @param channelId the non-null, non-empty channel ID + * @param appUri the non-null, non-empty application URI + */ + private void refreshAppData(final String channelId, final String appUri) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + SonyUtil.validateNotEmpty(appUri, "appUri cannot be empty"); + final ApplicationList app = getApplicationList(appUri); + if (app != null) { + callback.stateChanged(channelId, SonyUtil.newStringType(app.getData())); + } + } + + /** + * Refresh app status for the given URI + * + * @param channelId the non-null, non-empty channel ID + * @param appUri the non-null, non-empty application URI + */ + private void refreshAppStatus(final String channelId, final String appUri) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + SonyUtil.validateNotEmpty(appUri, "appUri cannot be empty"); + final WebAppStatus webAppStatus = getWebAppStatus(); + callback.stateChanged(channelId, + webAppStatus != null && webAppStatus.isActive() && appUri.equalsIgnoreCase(webAppStatus.getUrl()) + ? SonyUtil.newStringType(START) + : SonyUtil.newStringType(STOP)); + } + + /** + * Refresh status for the status name + * + * @param channelId the non-null, non-empty channel ID + * @param statusName the non-null, non-empty status name + */ + private void refreshStatus(final String channelId, final String statusName) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + SonyUtil.validateNotEmpty(statusName, "statusName cannot be empty"); + try { + final List statuses = execute(ScalarWebMethod.GETAPPLICATIONSTATUSLIST) + .asArray(ApplicationStatusList.class); + for (final ApplicationStatusList status : statuses) { + if (statusName.equalsIgnoreCase(status.getName())) { + callback.stateChanged(channelId, status.isOn() ? OnOffType.ON : OnOffType.OFF); + } + } + callback.stateChanged(channelId, OnOffType.OFF); + } catch (final IOException e) { + logger.debug("Exception getting application status list: {}", e.getMessage()); + } + } + + /** + * Refresh text from a text form + * + * @param channelId the non-null, non-empty channel ID + */ + private void refreshTextForm(final String channelId) { + SonyUtil.validateNotEmpty(channelId, "channelId cannot be empty"); + + // String pubKey = null; + // final ScalarWebService enc = getService(ScalarWebService.ENCRYPTION); + // if (enc != null) { + // try { + // final PublicKey publicKey = enc.execute(ScalarWebMethod.GETPUBLICKEY).as(PublicKey.class); + // pubKey = publicKey.getPublicKey(); + // } catch (final IOException e) { + // logger.debug("Exception getting public key: {}", e.getMessage()); + // } + // } + + try { + // if (StringUtils.isNotEmpty(pubKey)) { + // byte[] byteKey = Base64.getDecoder().decode(pubKey); + // X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(byteKey); + // KeyFactory kf = KeyFactory.getInstance("RSA"); + // java.security.PublicKey sonyPublicKey = kf.generatePublic(X509publicKey); + // KeyGenerator gen = KeyGenerator.getInstance("AES"); + // gen.init(128); /* 128-bit AES */ + // SecretKey secretKey = gen.generateKey(); + // Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); + // cipher.init(Cipher.ENCRYPT_MODE, sonyPublicKey); + // byte[] a = cipher.doFinal(secretKey.getEncoded()); + // String encKey = new String(Base64.getEncoder().encode(a), "UTF-8"); + + // final TextFormResult ff = execute(ScalarWebMethod.GETTEXTFORM, new TextFormRequest_1_1(encKey, null)) + // .as(TextFormResult.class); + // } + // Alwasy assume non encrypted version + // final String localEncKey = pubKey; + // if (localEncKey != null && StringUtils.isNotEmpty(localEncKey)) { + final TextFormResult form = execute(ScalarWebMethod.GETTEXTFORM, version -> { + // if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + return null; + // } + // return new TextFormRequest_1_1(rsa encrypted aes key, null); + }).as(TextFormResult.class); + callback.stateChanged(channelId, SonyUtil.newStringType(form.getText())); + // } + // } catch (final IOException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException + // | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { + } catch (final IOException e) { + logger.debug("Exception getting text form: {}", e.getMessage()); + } + } + + @Override + public void setChannel(final ScalarWebChannel channel, final Command command) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + switch (channel.getCategory()) { + case TEXTFORM: + if (command instanceof StringType) { + setTextForm(command.toString()); + } else { + logger.debug("TEXTFORM command not an StringType: {}", command); + } + + break; + + case APPSTATUS: + final String[] paths = channel.getPaths(); + if (paths.length == 0) { + logger.debug("Set APPSTATUS Channel path invalid: {}", channel); + } else { + if (command instanceof StringType) { + setAppStatus(paths[0], START.equalsIgnoreCase(command.toString())); + } else { + logger.debug("APPSTATUS command not an StringType: {}", command); + } + if (command instanceof OnOffType) { + setAppStatus(paths[0], command == OnOffType.ON); + } else { + logger.debug("APPSTATUS command not an OnOffType: {}", command); + } + } + + break; + + default: + logger.debug("Unhandled channel command: {} - {}", channel, command); + break; + } + } + + /** + * Sets the text in a form + * + * @param text the possibly null, possibly empty text + */ + private void setTextForm(final @Nullable String text) { + // Assume non-encrypted form + // final String version = getService().getVersion(ScalarWebMethod.SETTEXTFORM); + // if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + handleExecute(ScalarWebMethod.SETTEXTFORM, text == null ? "" : text); + // } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + // final String localEncKey = pubKey; + // if (localEncKey != null && StringUtils.isNotEmpty(localEncKey)) { + // do encryption + // handleExecute(ScalarWebMethod.SETTEXTFORM, + // new TextFormRequest_1_1(localEncKey, text == null ? "" : text)); + // } + // } else { + // logger.debug("Unknown {} method version: {}", ScalarWebMethod.SETTEXTFORM, version); + // } + } + + /** + * Sets the application status + * + * @param appUri the non-null, non-empty application URI + * @param on true if active, false otherwise + */ + private void setAppStatus(final String appUri, final boolean on) { + SonyUtil.validateNotEmpty(appUri, "appUri cannot be empty"); + if (on) { + handleExecute(ScalarWebMethod.SETACTIVEAPP, new ActiveApp(appUri, null)); + } else { + handleExecute(ScalarWebMethod.TERMINATEAPPS); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebAudioProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebAudioProtocol.java new file mode 100644 index 0000000000000..caeb3f041da79 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebAudioProtocol.java @@ -0,0 +1,639 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelTracker; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.VersionUtilities; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebEvent; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.AudioMute_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.AudioMute_1_1; +import org.openhab.binding.sony.internal.scalarweb.models.api.AudioVolume_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.AudioVolume_1_1; +import org.openhab.binding.sony.internal.scalarweb.models.api.AudioVolume_1_2; +import org.openhab.binding.sony.internal.scalarweb.models.api.CurrentExternalTerminalsStatus_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.GeneralSetting; +import org.openhab.binding.sony.internal.scalarweb.models.api.GeneralSettings_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.Output; +import org.openhab.binding.sony.internal.scalarweb.models.api.Target; +import org.openhab.binding.sony.internal.scalarweb.models.api.VolumeInformation_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.VolumeInformation_1_1; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.types.Command; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The implementation of the protocol handles the Audio service + * + * @author Tim Roberts - Initial contribution + * @param the generic type for the callback + */ +@NonNullByDefault +class ScalarWebAudioProtocol<@NonNull T extends ThingCallback> extends AbstractScalarWebProtocol { + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebAudioProtocol.class); + + // Constants used by this protocol + private static final String CUSTOMEQUALIZER = "customequalizer"; + private static final String SPEAKERSETTING = "speakersetting"; + private static final String SOUNDSETTING = "soundsetting"; + private static final String MUTE = "mute"; + private static final String VOLUME = "volume"; + private static final String DEFAULTKEY = "main"; + + /** The notifications that are enabled */ + private final NotificationHelper notificationHelper; + + /** The HDMI/CEC settings */ + private final boolean enableHdmiCec; + private final boolean forceHdmiCec; + private final int cecDelay; + + /** + * Instantiates a new scalar web audio protocol. + * + * @param factory the non-null factory to use + * @param context the non-null context to use + * @param audioService the non-null audio service to use + * @param callback the non-null callback to use + */ + ScalarWebAudioProtocol(final ScalarWebProtocolFactory factory, final ScalarWebContext context, + final ScalarWebService audioService, final @NonNull T callback) { + super(factory, context, audioService, callback); + notificationHelper = new NotificationHelper(enableNotifications(ScalarWebEvent.NOTIFYVOLUMEINFORMATION)); + + final Map osgiProperties = getContext().getOsgiProperties(); + this.enableHdmiCec = Boolean.TRUE.equals(SonyUtil.toBooleanObject(osgiProperties.get("audio-enablecec"))); + this.forceHdmiCec = Boolean.TRUE.equals(SonyUtil.toBooleanObject(osgiProperties.get("audio-forcecec"))); + int cecDelay = 250; + try { + cecDelay = Integer.parseInt(SonyUtil.defaultIfEmpty(osgiProperties.get("audio-cecDelay"), "")); + } catch (NumberFormatException nfe) { + } + this.cecDelay = cecDelay; + } + + @Override + public Collection getChannelDescriptors(final boolean dynamicOnly) { + final List descriptors = new ArrayList<>(); + + // no dynamic channels + if (dynamicOnly) { + return descriptors; + } + + final Map termTitles = new HashMap<>(); + final ScalarWebService avService = getService(ScalarWebService.AVCONTENT); + if (avService != null) { + try { + for (final CurrentExternalTerminalsStatus_1_0 term : avService + .execute(ScalarWebMethod.GETCURRENTEXTERNALTERMINALSSTATUS) + .asArray(CurrentExternalTerminalsStatus_1_0.class)) { + final String uri = term.getUri(); + if (uri != null && !uri.isEmpty()) { + termTitles.put(uri, term.getTitle(uri)); + } + } + } catch (final IOException e) { + logger.debug("Could not retrieve {}: {}", ScalarWebMethod.GETCURRENTEXTERNALTERMINALSSTATUS, + e.getMessage()); + } + } + + try { + final String version = getService().getVersion(ScalarWebMethod.GETVOLUMEINFORMATION); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + for (final VolumeInformation_1_0 vi : execute(ScalarWebMethod.GETVOLUMEINFORMATION) + .asArray(VolumeInformation_1_0.class)) { + addVolumeDescriptors(descriptors, vi.getTarget(), vi.getTarget(), vi.getMinVolume(), + vi.getMaxVolume()); + } + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_1)) { + for (final VolumeInformation_1_1 vi : execute(ScalarWebMethod.GETVOLUMEINFORMATION, new Output()) + .asArray(VolumeInformation_1_1.class)) { + addVolumeDescriptors(descriptors, vi.getOutput(), termTitles.get(vi.getOutput()), vi.getMinVolume(), + vi.getMaxVolume()); + } + + } else { + logger.debug("Unknown {} method version: {}", ScalarWebMethod.GETVOLUMEINFORMATION, version); + } + } catch (final IOException e) { + logger.debug("Exception getting volume information: {}", e.getMessage()); + } + + if (service.hasMethod(ScalarWebMethod.GETSOUNDSETTINGS)) { + addGeneralSettingsDescriptor(descriptors, ScalarWebMethod.GETSOUNDSETTINGS, SOUNDSETTING, "Sound Setting"); + } + + if (service.hasMethod(ScalarWebMethod.GETSPEAKERSETTINGS)) { + addGeneralSettingsDescriptor(descriptors, ScalarWebMethod.GETSPEAKERSETTINGS, SPEAKERSETTING, + "Speaker Setting"); + } + + if (service.hasMethod(ScalarWebMethod.GETCUSTOMEQUALIZERSETTINGS)) { + addGeneralSettingsDescriptor(descriptors, ScalarWebMethod.GETCUSTOMEQUALIZERSETTINGS, CUSTOMEQUALIZER, + "Custom Equalizer"); + } + + return descriptors; + } + + /** + * Helper method to add volume descriptors for the specified parameters + * + * @param descriptors a non-null, possibly empty list of descriptors to add too + * @param viKey a possibly null, possibly empty volume information key + * @param outputLabel a possibly null, possibly emtpy output label to assign + * @param min a possibly null minimum volume level + * @param max a possibly null maximum volume level + */ + private void addVolumeDescriptors(final List descriptors, final @Nullable String viKey, + final @Nullable String outputLabel, final @Nullable Integer min, final @Nullable Integer max) { + Objects.requireNonNull(descriptors, "descriptors cannot be null"); + + final String key = SonyUtil.defaultIfEmpty(viKey, DEFAULTKEY); + final String label = outputLabel == null ? SonyUtil.capitalize(key) : outputLabel; + final String id = SonyUtil.createValidChannelUId(key); + final ScalarWebChannel volChannel = createChannel(VOLUME, id, key); + + descriptors.add( + createDescriptor(volChannel, "Dimmer", "scalaraudiovolume", "Volume " + label, "Volume for " + label)); + + descriptors.add(createDescriptor(createChannel(MUTE, id, key), "Switch", "scalaraudiomute", "Mute " + label, + "Mute " + label)); + + StateDescriptionFragmentBuilder bld = StateDescriptionFragmentBuilder.create().withStep(BigDecimal.ONE); + if (min != null) { + bld = bld.withMinimum(new BigDecimal(min)); + } + if (max != null) { + bld = bld.withMaximum(new BigDecimal(max)); + } + + final StateDescription sd = bld.build().toStateDescription(); + + if (sd != null) { + getContext().getStateProvider().addStateOverride(getContext().getThingUID(), volChannel.getChannelId(), sd); + } + } + + @Override + public void refreshState(boolean initial) { + final ScalarWebChannelTracker tracker = getContext().getTracker(); + + if (initial || !notificationHelper.isEnabled(ScalarWebEvent.NOTIFYVOLUMEINFORMATION)) { + if (tracker.isCategoryLinked(VOLUME, MUTE)) { + refreshVolume(getChannelTracker().getLinkedChannelsForCategory(VOLUME, MUTE)); + } + } + + if (tracker.isCategoryLinked(SOUNDSETTING)) { + refreshGeneralSettings(tracker.getLinkedChannelsForCategory(SOUNDSETTING), + ScalarWebMethod.GETSOUNDSETTINGS); + } + if (tracker.isCategoryLinked(SPEAKERSETTING)) { + refreshGeneralSettings(tracker.getLinkedChannelsForCategory(SPEAKERSETTING), + ScalarWebMethod.GETSPEAKERSETTINGS); + } + if (tracker.isCategoryLinked(CUSTOMEQUALIZER)) { + refreshGeneralSettings(tracker.getLinkedChannelsForCategory(CUSTOMEQUALIZER), + ScalarWebMethod.GETCUSTOMEQUALIZERSETTINGS); + } + } + + @Override + public void refreshChannel(final ScalarWebChannel channel) { + Objects.requireNonNull(channel, "channel cannot be null"); + + final String ctgy = channel.getCategory(); + if (MUTE.equalsIgnoreCase(ctgy) || VOLUME.equalsIgnoreCase(ctgy)) { + refreshVolume(Collections.singleton(channel)); + } else if (SOUNDSETTING.equalsIgnoreCase(ctgy)) { + refreshGeneralSettings(Collections.singleton(channel), ScalarWebMethod.GETSOUNDSETTINGS); + } else if (SPEAKERSETTING.equalsIgnoreCase(ctgy)) { + refreshGeneralSettings(Collections.singleton(channel), ScalarWebMethod.GETSPEAKERSETTINGS); + } else if (CUSTOMEQUALIZER.equalsIgnoreCase(ctgy)) { + refreshGeneralSettings(Collections.singleton(channel), ScalarWebMethod.GETCUSTOMEQUALIZERSETTINGS); + } + } + + /** + * Helper method to refresh volume information based on a set of channels + * + * @param channels a non-null, possibly empty set of channels + */ + private void refreshVolume(final Set channels) { + Objects.requireNonNull(channels, "channels cannot be null"); + + try { + final String version = getService().getVersion(ScalarWebMethod.GETVOLUMEINFORMATION); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + for (final VolumeInformation_1_0 vi : handleExecute(ScalarWebMethod.GETVOLUMEINFORMATION) + .asArray(VolumeInformation_1_0.class)) { + notifyVolumeInformation(vi, channels); + } + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_1)) { + for (final VolumeInformation_1_1 vi : handleExecute(ScalarWebMethod.GETVOLUMEINFORMATION, new Output()) + .asArray(VolumeInformation_1_1.class)) { + notifyVolumeInformation(vi, channels); + } + } else { + logger.debug("Unknown {} method version: {}", ScalarWebMethod.GETVOLUMEINFORMATION, version); + } + } catch (final IOException e) { + // already handled + } + } + + @Override + public void setChannel(final ScalarWebChannel channel, final Command command) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + final String[] paths = channel.getPaths(); + if (paths.length != 1 || paths[0] == null) { + logger.debug("Channel path invalid: {}", channel); + return; + } + final String path0 = paths[0]; + final String key = DEFAULTKEY.equalsIgnoreCase(path0) ? "" : path0; + + switch (channel.getCategory()) { + case MUTE: + if (command instanceof OnOffType) { + setMute(key, command == OnOffType.ON); + } else { + logger.debug("Mute command not an OnOffType: {}", command); + } + + break; + + case VOLUME: + if (command instanceof PercentType) { + setVolume(key, channel, ((PercentType) command)); + } else if (command instanceof OnOffType) { + setMute(key, command == OnOffType.ON); + } else if (command instanceof IncreaseDecreaseType) { + setVolume(key, ((IncreaseDecreaseType) command) == IncreaseDecreaseType.INCREASE); + } else { + logger.debug("Volume command not an PercentType/OnOffType/IncreaseDecreaseType: {}", command); + } + + break; + + case SOUNDSETTING: + setGeneralSetting(ScalarWebMethod.SETSOUNDSETTINGS, channel, command); + break; + + case SPEAKERSETTING: + setGeneralSetting(ScalarWebMethod.SETSPEAKERSETTINGS, channel, command); + break; + + case CUSTOMEQUALIZER: + setGeneralSetting(ScalarWebMethod.SETCUSTOMEQUALIZERSETTINGS, channel, command); + break; + + default: + logger.debug("Unhandled channel command: {} - {}", channel, command); + break; + } + } + + /** + * Sets the mute for the specified target + * + * @param key the non-null, possibly empty (default zone) key + * @param muted the muted + */ + private void setMute(final String key, final boolean muted) { + Objects.requireNonNull(key, "key cannot be null"); + final String version = getService().getVersion(ScalarWebMethod.SETAUDIOMUTE); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + handleExecute(ScalarWebMethod.SETAUDIOMUTE, new AudioMute_1_0(key, muted)); + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_1)) { + handleExecute(ScalarWebMethod.SETAUDIOMUTE, new AudioMute_1_1(key, muted)); + } else { + logger.debug("Unknown {} method version: {}", ScalarWebMethod.SETAUDIOMUTE, version); + } + } + + /** + * Sets the volume for the specified target + * + * @param key the non-null, possibly empty (default zone) key + * @param chl the non-null target channel + * @param cmd the non-null command + */ + private void setVolume(final String key, final ScalarWebChannel chl, final PercentType cmd) { + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(chl, "chl cannot be null"); + Objects.requireNonNull(cmd, "cmd cannot be null"); + + final StateDescription sd = getContext().getStateProvider().getStateDescription(getContext().getThingUID(), + chl.getChannelId()); + + final BigDecimal min = sd == null ? BigDecimal.ZERO : sd.getMinimum(); + final BigDecimal max = sd == null ? SonyUtil.BIGDECIMAL_HUNDRED : sd.getMaximum(); + + final int unscaled = SonyUtil.unscale(cmd.toBigDecimal(), min, max).setScale(0, RoundingMode.FLOOR).intValue(); + + /** + * The following tries to overcome an HDMI/CEC issue. HDMI/CEC will ONLY allow increment/decrement of the + * volume. So if we set to a specific level, we need to determine if we need to increment/decrement up to that + * level if connected + */ + if (enableHdmiCec) { + try { + final Integer currVol = getVolume(); + if (currVol == null) { + logger.debug("Unknown current volume level - can't increment hdmi audio"); + } else { + boolean doIncrement = forceHdmiCec; // are we forcing cec processing? + if (!doIncrement) { + // If not forced, see if we can detect... + final GeneralSettings_1_0 ss = handleExecute(ScalarWebMethod.GETSOUNDSETTINGS, + new Target(Target.OUTPUTTERMINAL)).as(GeneralSettings_1_0.class); + + // OrElse(null) doesn't seem to go well with nullable attributes here for some reason + final Optional ogs = ss.getSettings(Target.OUTPUTTERMINAL).findFirst(); + if (ogs.isPresent()) { + final String val = ogs.get().getCurrentValue(); + if (GeneralSetting.SOUNDSETTING_SPEAKERHDMI.equalsIgnoreCase(val) + || GeneralSetting.SOUNDSETTING_HDMI.equalsIgnoreCase(val) + || GeneralSetting.SOUNDSETTING_AUDIOSYSTEM.equalsIgnoreCase(val)) { + doIncrement = true; + } else { + logger.debug("No HDMI/CEC connected - ignoring ({})", val); + } + } else { + logger.debug("No general sound setting for {} - ignoring incrementing", + Target.OUTPUTTERMINAL); + } + } + + if (doIncrement) { + final int maxIncr = Math.abs(currVol - unscaled); + final boolean up = unscaled > currVol; + logger.debug("HDMI/CEC incremental processing - sending {} ({}, {}) UP({})", maxIncr, currVol, + unscaled, up); + scheduleVolumeChange(key, up, 0, maxIncr); + return; + } + } + + } catch (final IOException e) { + logger.debug("IOException occurred during autoscaling HDMI audio (ignoring): {}", e.getMessage()); + } + } + + final String version = getService().getVersion(ScalarWebMethod.SETAUDIOVOLUME); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + handleExecute(ScalarWebMethod.SETAUDIOVOLUME, new AudioVolume_1_0(key, unscaled)); + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_1)) { + handleExecute(ScalarWebMethod.SETAUDIOVOLUME, new AudioVolume_1_1(key, unscaled)); + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_2)) { + handleExecute(ScalarWebMethod.SETAUDIOVOLUME, new AudioVolume_1_2(key, unscaled)); + } else { + logger.debug("Unknown {} method version: {}", ScalarWebMethod.SETAUDIOVOLUME, version); + } + } + + /** + * Get's the current volume level or null if none found + * + * @return the current volume level or none if not found + * @throws IOException if an IOException occurs + */ + @Nullable + private Integer getVolume() throws IOException { + final String version = getService().getVersion(ScalarWebMethod.GETVOLUMEINFORMATION); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + for (final VolumeInformation_1_0 vi : handleExecute(ScalarWebMethod.GETVOLUMEINFORMATION) + .asArray(VolumeInformation_1_0.class)) { + final @Nullable Integer vol = vi.getVolume(); + if (vi != null) { + return vol; + } + } + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_1)) { + for (final VolumeInformation_1_1 vi : handleExecute(ScalarWebMethod.GETVOLUMEINFORMATION, new Output()) + .asArray(VolumeInformation_1_1.class)) { + final Integer vol = vi.getVolume(); + if (vi != null) { + return vol; + } + } + } else { + logger.debug("Unknown {} method version: {}", ScalarWebMethod.GETVOLUMEINFORMATION, version); + } + return null; + } + + /** + * Will schedule a volume change after each delay + * + * @param key a non-null, non-empty key to use + * @param up true to increment up, false to increment down + * @param ct the current increment count + * @param max the maximum increment count + */ + private void scheduleVolumeChange(final String key, final boolean up, final int ct, final int max) { + SonyUtil.validateNotEmpty(key, "key cannot be empty"); + if (ct < 0) { + throw new IllegalArgumentException("ct cannot be less than 0: " + ct); + } + + if (max < 0) { + throw new IllegalArgumentException("max cannot be less than 0: " + max); + } + + if (ct >= max) { + logger.debug("HDMI/CEC increment is done: {} for {}", max, key); + return; + } + + getContext().getScheduler().schedule(() -> { + logger.debug("HDMI/CEC increment: {}/{} for {}", ct, max, key); + setVolume(key, up); + scheduleVolumeChange(key, up, ct + 1, max); + }, cecDelay, TimeUnit.MILLISECONDS); + } + + /** + * Adjust volume up or down on the specified target + * + * @param key the non-null, possibly empty (default zone) key + * @param up true to turn volume up by 1, false to turn volume down by 1 + */ + private void setVolume(final String key, final boolean up) { + Objects.requireNonNull(key, "key cannot be null"); + final String adj = up ? "+1" : "-1"; + final String version = getService().getVersion(ScalarWebMethod.SETAUDIOVOLUME); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + handleExecute(ScalarWebMethod.SETAUDIOVOLUME, new AudioVolume_1_0(key, adj)); + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_1)) { + handleExecute(ScalarWebMethod.SETAUDIOVOLUME, new AudioVolume_1_1(key, adj)); + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_2)) { + handleExecute(ScalarWebMethod.SETAUDIOVOLUME, new AudioVolume_1_2(key, adj)); + } else { + logger.debug("Unknown {} method version: {}", ScalarWebMethod.SETAUDIOVOLUME, version); + } + } + + @Override + protected void eventReceived(final ScalarWebEvent event) throws IOException { + Objects.requireNonNull(event, "event cannot be null"); + final @Nullable String mtd = event.getMethod(); + if (mtd == null || mtd.isEmpty()) { + logger.debug("Unhandled event received (no method): {}", event); + } else { + switch (mtd) { + case ScalarWebEvent.NOTIFYVOLUMEINFORMATION: + final String version = getVersion(ScalarWebMethod.GETVOLUMEINFORMATION); + final Set channels = getChannelTracker().getLinkedChannelsForCategory(VOLUME, + MUTE); + + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + final VolumeInformation_1_0 vi = event.as(VolumeInformation_1_0.class); + notifyVolumeInformation(vi, channels); + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_1)) { + final VolumeInformation_1_1 vi = event.as(VolumeInformation_1_1.class); + notifyVolumeInformation(vi, channels); + } else { + logger.debug("Unknown {} method version: {}", ScalarWebEvent.NOTIFYVOLUMEINFORMATION, version); + } + + break; + + default: + logger.debug("Unhandled event received (unknown method): {}", event); + break; + } + } + } + + /** + * The method that will handle notification of a volume change + * + * @param vi a non-null volume information + * @param channels a non-null, possibly empty set of channels to notify + */ + private void notifyVolumeInformation(final VolumeInformation_1_0 vi, final Set channels) { + Objects.requireNonNull(vi, "vi cannot be null"); + Objects.requireNonNull(channels, "channels cannot be null"); + + for (final ScalarWebChannel chnl : channels) { + final String viKey = vi.getTarget(); + final String key = SonyUtil.defaultIfEmpty(viKey, DEFAULTKEY); + if (key.equalsIgnoreCase(chnl.getPathPart(0))) { + final String cid = chnl.getChannelId(); + switch (chnl.getCategory()) { + case VOLUME: + final Integer vol = vi.getVolume(); + + final StateDescription sd = getContext().getStateProvider() + .getStateDescription(getContext().getThingUID(), cid); + final BigDecimal min = sd == null ? BigDecimal.ZERO : sd.getMinimum(); + final BigDecimal max = sd == null ? SonyUtil.BIGDECIMAL_HUNDRED : sd.getMaximum(); + + final BigDecimal scaled = SonyUtil.scale(vol == null ? BigDecimal.ZERO : new BigDecimal(vol), + min, max); + + callback.stateChanged(cid, SonyUtil.newPercentType(scaled)); + break; + + case MUTE: + callback.stateChanged(cid, vi.isMute() ? OnOffType.ON : OnOffType.OFF); + break; + + default: + logger.debug("Unhandled channel category: {} - {}", chnl, chnl.getCategory()); + break; + } + } + } + } + + /** + * The method that will handle notification of a volume change + * + * @param vi a non-null volume information + * @param channels a non-null, possibly empty set of chanenls to notify + */ + private void notifyVolumeInformation(final VolumeInformation_1_1 vi, final Set channels) { + Objects.requireNonNull(vi, "vi cannot be null"); + Objects.requireNonNull(channels, "channels cannot be null"); + + for (final ScalarWebChannel chnl : channels) { + final String viKey = vi.getOutput(); + final String key = SonyUtil.defaultIfEmpty(viKey, DEFAULTKEY); + if (key.equalsIgnoreCase(chnl.getPathPart(0))) { + final String cid = chnl.getChannelId(); + switch (chnl.getCategory()) { + case VOLUME: + final Integer vol = vi.getVolume(); + + final StateDescription sd = getContext().getStateProvider() + .getStateDescription(getContext().getThingUID(), cid); + final BigDecimal min = sd == null ? BigDecimal.ZERO : sd.getMinimum(); + final BigDecimal max = sd == null ? SonyUtil.BIGDECIMAL_HUNDRED : sd.getMaximum(); + + final BigDecimal scaled = SonyUtil + .scale(vol == null ? BigDecimal.ZERO : new BigDecimal(vol), min, max) + .setScale(0, RoundingMode.FLOOR); + + callback.stateChanged(cid, SonyUtil.newPercentType(scaled)); + break; + + case MUTE: + callback.stateChanged(cid, vi.isMute() ? OnOffType.ON : OnOffType.OFF); + break; + + default: + logger.debug("Unhandled channel category: {} - {}", chnl, chnl.getCategory()); + break; + } + } + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebAvContentProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebAvContentProtocol.java new file mode 100644 index 0000000000000..8c1249b35be01 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebAvContentProtocol.java @@ -0,0 +1,3404 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Scanner; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelTracker; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.VersionUtilities; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebError; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebEvent; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.ActiveTerminal; +import org.openhab.binding.sony.internal.scalarweb.models.api.AudioInfo; +import org.openhab.binding.sony.internal.scalarweb.models.api.BivlInfo; +import org.openhab.binding.sony.internal.scalarweb.models.api.BroadcastFreq; +import org.openhab.binding.sony.internal.scalarweb.models.api.ContentCount_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.ContentCount_1_3; +import org.openhab.binding.sony.internal.scalarweb.models.api.ContentListRequest_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.ContentListRequest_1_4; +import org.openhab.binding.sony.internal.scalarweb.models.api.ContentListResult_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.ContentListResult_1_2; +import org.openhab.binding.sony.internal.scalarweb.models.api.ContentListResult_1_4; +import org.openhab.binding.sony.internal.scalarweb.models.api.ContentListResult_1_5; +import org.openhab.binding.sony.internal.scalarweb.models.api.Count; +import org.openhab.binding.sony.internal.scalarweb.models.api.CurrentExternalInputsStatus_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.CurrentExternalInputsStatus_1_1; +import org.openhab.binding.sony.internal.scalarweb.models.api.CurrentExternalTerminalsStatus_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.DabInfo; +import org.openhab.binding.sony.internal.scalarweb.models.api.DeleteContent; +import org.openhab.binding.sony.internal.scalarweb.models.api.DeleteProtection; +import org.openhab.binding.sony.internal.scalarweb.models.api.Duration; +import org.openhab.binding.sony.internal.scalarweb.models.api.Output; +import org.openhab.binding.sony.internal.scalarweb.models.api.ParentalInfo; +import org.openhab.binding.sony.internal.scalarweb.models.api.ParentalRatingSetting_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.PlayContent_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.PlayContent_1_2; +import org.openhab.binding.sony.internal.scalarweb.models.api.PlayingContentInfoRequest_1_2; +import org.openhab.binding.sony.internal.scalarweb.models.api.PlayingContentInfoResult_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.PlayingContentInfoResult_1_2; +import org.openhab.binding.sony.internal.scalarweb.models.api.PresetBroadcastStation; +import org.openhab.binding.sony.internal.scalarweb.models.api.ScanPlayingContent_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.Scheme; +import org.openhab.binding.sony.internal.scalarweb.models.api.SeekBroadcastStation_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.Source; +import org.openhab.binding.sony.internal.scalarweb.models.api.StateInfo; +import org.openhab.binding.sony.internal.scalarweb.models.api.SubtitleInfo; +import org.openhab.binding.sony.internal.scalarweb.models.api.TvContentVisibility; +import org.openhab.binding.sony.internal.scalarweb.models.api.VideoInfo; +import org.openhab.binding.sony.internal.scalarweb.models.api.Visibility; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.openhab.core.OpenHAB; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.MetricPrefix; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Channel; +import org.openhab.core.types.Command; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.openhab.core.types.StateOption; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The implementation of the protocol handles the Av Content service + * + * @author Tim Roberts - Initial contribution + * @param the generic type for the callback + */ +@NonNullByDefault +class ScalarWebAvContentProtocol<@NonNull T extends ThingCallback> extends AbstractScalarWebProtocol { + + // Default values for devices that have only a single output + private static final String MAINOUTPUT = "main"; // cannot be empty as it's used as a channel id + private static final String MAINTITLE = "Main"; + + // Constants used by this protocol + private static final String BLUETOOTHSETTINGS = "bluetoothsetting"; + private static final String PLAYBACKSETTINGS = "playbackmode"; + private static final String SCHEMES = "schemes"; + private static final String SOURCES = "sources"; + + // parental rating channel constants + private static final String PARENTRATING = "pr_"; + private static final String PR_RATINGTYPEAGE = PARENTRATING + "ratingtypeage"; + private static final String PR_RATINGTYPESONY = PARENTRATING + "ratingtypesony"; + private static final String PR_RATINGCOUNTRY = PARENTRATING + "ratingcountry"; + private static final String PR_RATINGCUSTOMTYPETV = PARENTRATING + "ratingcustomtypetv"; + private static final String PR_RATINGCUSTOMTYPEMPAA = PARENTRATING + "ratingcustomtypempaa"; + private static final String PR_RATINGCUSTOMTYPECAENGLISH = PARENTRATING + "ratingcustomtypecaenglish"; + private static final String PR_RATINGCUSTOMTYPECAFRENCH = PARENTRATING + "ratingcustomtypecafrench"; + private static final String PR_UNRATEDLOCK = PARENTRATING + "unratedlock"; + + // now playing channel constants + private static final String PLAYING = "pl_"; + + private static final String PL_ALBUMNAME = PLAYING + "albumname"; + private static final String PL_APPLICATIONNAME = PLAYING + "applicationname"; + private static final String PL_ARTIST = PLAYING + "artist"; + private static final String PL_AUDIOCHANNEL = PLAYING + "audiochannel"; + private static final String PL_AUDIOCODEC = PLAYING + "audiocodec"; + private static final String PL_AUDIOFREQUENCY = PLAYING + "audiofrequency"; + private static final String PL_BIVLASSETID = PLAYING + "bivlassetid"; + private static final String PL_BIVLPROVIDER = PLAYING + "bivlprovider"; + private static final String PL_BIVLSERVICEID = PLAYING + "bivlserviceid"; + private static final String PL_BROADCASTFREQ = PLAYING + "broadcastfreq"; + private static final String PL_BROADCASTFREQBAND = PLAYING + "broadcastfreqband"; + private static final String PL_CHANNELNAME = PLAYING + "channelname"; + private static final String PL_CHAPTERCOUNT = PLAYING + "chaptercount"; + private static final String PL_CHAPTERINDEX = PLAYING + "chapterindex"; + private static final String PL_CMD = PLAYING + "cmd"; + private static final String PL_CONTENTKIND = PLAYING + "contentkind"; + private static final String PL_DABCOMPONENTLABEL = PLAYING + "dabcomponentlabel"; + private static final String PL_DABDYNAMICLABEL = PLAYING + "dabdynamiclabel"; + private static final String PL_DABENSEMBLELABEL = PLAYING + "dabensemblelabel"; + private static final String PL_DABSERVICELABEL = PLAYING + "dabservicelabel"; + private static final String PL_DISPNUM = PLAYING + "dispnum"; + private static final String PL_DURATIONMSEC = PLAYING + "durationmsec"; + private static final String PL_DURATIONSEC = PLAYING + "durationsec"; + private static final String PL_FILENO = PLAYING + "fileno"; + private static final String PL_GENRE = PLAYING + "genre"; + private static final String PL_INDEX = PLAYING + "index"; + private static final String PL_IS3D = PLAYING + "is3d"; + private static final String PL_MEDIATYPE = PLAYING + "mediatype"; + private static final String PL_ORIGINALDISPNUM = PLAYING + "originaldispnum"; + private static final String PL_OUTPUT = PLAYING + "output"; + private static final String PL_PARENTINDEX = PLAYING + "parentindex"; + private static final String PL_PARENTURI = PLAYING + "parenturi"; + private static final String PL_PATH = PLAYING + "path"; + private static final String PL_PLAYLISTNAME = PLAYING + "playlistname"; + private static final String PL_PLAYSPEED = PLAYING + "playspeed"; + private static final String PL_PLAYSTEPSPEED = PLAYING + "playstepspeed"; + private static final String PL_PODCASTNAME = PLAYING + "podcastname"; + private static final String PL_POSITIONMSEC = PLAYING + "positionmsec"; + private static final String PL_POSITIONSEC = PLAYING + "positionsec"; + private static final String PL_PRESET = PLAYING + "presetid"; + private static final String PL_PROGRAMNUM = PLAYING + "programnum"; + private static final String PL_PROGRAMTITLE = PLAYING + "programtitle"; + private static final String PL_REPEATTYPE = PLAYING + "repeattype"; + private static final String PL_SERVICE = PLAYING + "service"; + private static final String PL_SOURCE = PLAYING + "source"; + private static final String PL_SOURCELABEL = PLAYING + "sourcelabel"; + private static final String PL_STARTDATETIME = PLAYING + "startdatetime"; + private static final String PL_STATE = PLAYING + "state"; + private static final String PL_STATESUPPLEMENT = PLAYING + "statesupplement"; + private static final String PL_SUBTITLEINDEX = PLAYING + "subtitleindex"; + private static final String PL_TITLE = PLAYING + "title"; + private static final String PL_TOTALCOUNT = PLAYING + "totalcount"; + private static final String PL_TRIPLETSTR = PLAYING + "tripletstr"; + private static final String PL_URI = PLAYING + "uri"; + private static final String PL_VIDEOCODEC = PLAYING + "videocodec"; + + // virtual presets channel constants + private static final String PRESETS = "ps_"; + private static final String PS_CHANNEL = PRESETS + "channel"; + private static final String PS_REFRESH = "--REFRESH--"; + + // input channel constants + private static final String INPUT = "in_"; + private static final String IN_URI = INPUT + "uri"; + private static final String IN_TITLE = INPUT + "title"; + private static final String IN_CONNECTION = INPUT + "connection"; + private static final String IN_LABEL = INPUT + "label"; + private static final String IN_ICON = INPUT + "icon"; + private static final String IN_STATUS = INPUT + "status"; + + // terminal channel constants + private static final String TERM = "tm_"; + private static final String TERM_SOURCE = TERM + "source"; + private static final String TERM_URI = TERM + "uri"; + private static final String TERM_TITLE = TERM + "title"; + private static final String TERM_CONNECTION = TERM + "connection"; + private static final String TERM_LABEL = TERM + "label"; + private static final String TERM_ICON = TERM + "icon"; + private static final String TERM_ACTIVE = TERM + "active"; + + // content channel constants + private static final String CONTENT = "cn_"; + private static final String CN_ALBUMNAME = CONTENT + "albumname"; + private static final String CN_APPLICATIONNAME = CONTENT + "applicationname"; + private static final String CN_ARTIST = CONTENT + "artist"; + private static final String CN_AUDIOCHANNEL = CONTENT + "audiochannel"; + private static final String CN_AUDIOCODEC = CONTENT + "audiocodec"; + private static final String CN_AUDIOFREQUENCY = CONTENT + "audiofrequency"; + private static final String CN_BROADCASTFREQ = CONTENT + "broadcastfreq"; + private static final String CN_BIVLSERVICEID = CONTENT + "bivlserviceid"; + private static final String CN_BIVLASSETID = CONTENT + "bivleassetid"; + private static final String CN_BIVLPROVIDER = CONTENT + "bivlprovider"; + private static final String CN_BROADCASTFREQBAND = CONTENT + "broadcastfreqband"; + private static final String CN_CHANNELNAME = CONTENT + "channelname"; + private static final String CN_CHANNELSURFINGVISIBILITY = CONTENT + "channelsurfingvisibility"; + private static final String CN_CHAPTERCOUNT = CONTENT + "chaptercount"; + private static final String CN_CHAPTERINDEX = CONTENT + "chapterindex"; + private static final String CN_CHILDCOUNT = CONTENT + "childcount"; + private static final String CN_CLIPCOUNT = CONTENT + "clipcount"; + private static final String CN_CMD = CONTENT + "cmd"; + private static final String CN_CONTENTKIND = CONTENT + "contentkind"; + private static final String CN_CONTENTTYPE = CONTENT + "contenttype"; + private static final String CN_CREATEDTIME = CONTENT + "createdtime"; + private static final String CN_DABCOMPONENTLABEL = CONTENT + "dabcomponentlabel"; + private static final String CN_DABDYNAMICLABEL = CONTENT + "dabdynamiclabel"; + private static final String CN_DABENSEMBLELABEL = CONTENT + "dabensemblelabel"; + private static final String CN_DABSERVICELABEL = CONTENT + "dabservicelabel"; + private static final String CN_DESCRIPTION = CONTENT + "description"; + private static final String CN_DIRECTREMOTENUM = CONTENT + "directremotenum"; + private static final String CN_DISPNUM = CONTENT + "dispnum"; + private static final String CN_DURATIONMSEC = CONTENT + "durationmsec"; + private static final String CN_DURATIONSEC = CONTENT + "durationsec"; + private static final String CN_EPGVISIBILITY = CONTENT + "epgvisibility"; + private static final String CN_EVENTID = CONTENT + "eventid"; + private static final String CN_FILENO = CONTENT + "fileno"; + private static final String CN_FILESIZEBYTE = CONTENT + "filesizebyte"; + private static final String CN_FOLDERNO = CONTENT + "folderno"; + private static final String CN_GENRE = CONTENT + "genre"; + private static final String CN_GLOBALPLAYBACKCOUNT = CONTENT + "globalplaybackcount"; + private static final String CN_HASRESUME = CONTENT + "hasresume"; + private static final String CN_INDEX = CONTENT + "idx"; + private static final String CN_IS3D = CONTENT + "is3d"; + private static final String CN_IS4K = CONTENT + "is4k"; + private static final String CN_ISALREADYPLAYED = CONTENT + "isalreadyplayed"; + private static final String CN_ISAUTODELETE = CONTENT + "isautodelete"; + private static final String CN_ISBROWSABLE = CONTENT + "isbrowsable"; + private static final String CN_ISNEW = CONTENT + "isnew"; + private static final String CN_ISPLAYABLE = CONTENT + "isplayable"; + private static final String CN_ISPLAYLIST = CONTENT + "isplaylist"; + private static final String CN_ISPROTECTED = CONTENT + "isprotected"; + private static final String CN_ISSOUNDPHOTO = CONTENT + "issoundphoto"; + private static final String CN_MEDIATYPE = CONTENT + "mediatype"; + private static final String CN_ORIGINALDISPNUM = CONTENT + "originaldispnum"; + private static final String CN_OUTPUT = CONTENT + "output"; + private static final String CN_PARENTALCOUNTRY = CONTENT + "parentalcountry"; + private static final String CN_PARENTALRATING = CONTENT + "parentalrating"; + private static final String CN_PARENTALSYSTEM = CONTENT + "parentalsystem"; + private static final String CN_PARENTINDEX = CONTENT + "parentindex"; + private static final String CN_PARENTURI = CONTENT + "parenturi"; + private static final String CN_PATH = CONTENT + "path"; + private static final String CN_PLAYLISTNAME = CONTENT + "playlistname"; + private static final String CN_PODCASTNAME = CONTENT + "podcastname"; + private static final String CN_PRODUCTID = CONTENT + "productid"; + private static final String CN_PROGRAMMEDIATYPE = CONTENT + "programmediatype"; + private static final String CN_PROGRAMNUM = CONTENT + "programnum"; + private static final String CN_PROGRAMSERVICETYPE = CONTENT + "programservicetype"; + private static final String CN_PROGRAMTITLE = CONTENT + "programtitle"; + private static final String CN_REMOTEPLAYTYPE = CONTENT + "remoteplaytype"; + private static final String CN_REPEATTYPE = CONTENT + "repeattype"; + private static final String CN_SERVICE = CONTENT + "service"; + private static final String CN_SIZEMB = CONTENT + "sizemb"; + private static final String CN_SOURCE = CONTENT + "source"; + private static final String CN_SOURCELABEL = CONTENT + "sourcelabel"; + private static final String CN_STARTDATETIME = CONTENT + "startdatetime"; + private static final String CN_STATE = CONTENT + "state"; + private static final String CN_STATESUPPLEMENT = CONTENT + "statesupplement"; + private static final String CN_STORAGEURI = CONTENT + "storageuri"; + private static final String CN_SUBTITLELANGUAGE = CONTENT + "subtitlelanguage"; + private static final String CN_SUBTITLETITLE = CONTENT + "subtitletitle"; + private static final String CN_SYNCCONTENTPRIORITY = CONTENT + "synccontentpriority"; + private static final String CN_TITLE = CONTENT + "title"; + private static final String CN_TOTALCOUNT = CONTENT + "totalcount"; + private static final String CN_TRIPLETSTR = CONTENT + "tripletstr"; + private static final String CN_URI = CONTENT + "uri"; + private static final String CN_USERCONTENTFLAG = CONTENT + "usercontentflag"; + private static final String CN_VIDEOCODEC = CONTENT + "videocodec"; + private static final String CN_VISIBILITY = CONTENT + "visibility"; + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebAvContentProtocol.class); + + /** The cached schemes */ + private final AtomicReference> stateSchemes = new AtomicReference<>(new HashSet<>()); + + /** The cached sources by scheme */ + private final ConcurrentMap> stateSources = new ConcurrentHashMap<>(); + + /** The cached last played tv uris by TV source */ + private final ConcurrentMap lastPlayedTVUriBySource = new ConcurrentHashMap<>(); + + /** The source uri to title mapping */ + private final ConcurrentMap sourceUriTitleMap = new ConcurrentHashMap<>(); + + /** The cached uris by and source display number (used for presets) */ + private final ConcurrentMap> displayNumberUriMapBySource = new ConcurrentHashMap<>(); + + /** The cached terminals */ + private final AtomicReference> stateTerminals = new AtomicReference<>( + new ArrayList<>()); + + /** The cached inputs */ + private final AtomicReference<@Nullable ScalarWebResult> stateInputs = new AtomicReference<>(null); + + /** The cached content state (ie what our current url, index is) */ + private final AtomicReference stateContent = new AtomicReference<>(new ContentState()); + + /** The cached now playing state */ + private final ConcurrentMap statePlaying = new ConcurrentHashMap<>(); + + /** Maximum amount of content to pull in one request */ + private static final int MAX_CT = 150; + + /** The notifications that are enabled */ + private final NotificationHelper notificationHelper; + + /** + * Function interface to process a content list result + */ + @NonNullByDefault + private interface ContentListCallback { + /** + * Called to process a content list result + * + * @return true if processed, false otherwise + */ + boolean processContentListResult(ContentListResult_1_0 result); + } + + /** + * Instantiates a new scalar web av content protocol. + * + * @param factory the non-null factory + * @param context the non-null context + * @param service the non-null service + * @param callback the non-null callback + */ + ScalarWebAvContentProtocol(final ScalarWebProtocolFactory factory, final ScalarWebContext context, + final ScalarWebService service, final @NonNull T callback) { + super(factory, context, service, callback); + notificationHelper = new NotificationHelper(enableNotifications(ScalarWebEvent.NOTIFYPLAYINGCONTENTINFO, + /** ScalarWebEvent.NOTIFYAVAILABLEPLAYBACKFUNCTION, */ + ScalarWebEvent.NOTIFYEXTERNALTERMINALSTATUS)); + } + + @Override + public Collection getChannelDescriptors(final boolean dynamicOnly) { + final List descriptors = new ArrayList(); + + // Add the predefined channels (dynamic) + if (service.hasMethod(ScalarWebMethod.GETCONTENTLIST)) { + addPresetChannelDescriptors(descriptors); + } + + if (!dynamicOnly) { + if (service.hasMethod(ScalarWebMethod.GETCONTENTLIST)) { + addContentListDescriptors(descriptors); + } + + if (service.hasMethod(ScalarWebMethod.GETSCHEMELIST)) { + descriptors.add(createDescriptor(createChannel(SCHEMES), "String", "scalarwebavcontrolschemes")); + } + + if (service.hasMethod(ScalarWebMethod.GETSOURCELIST)) { + descriptors.add(createDescriptor(createChannel(SOURCES), "String", "scalarwebavcontrolsources")); + } + + // don't check has here since we create a dummy terminal for single output devices + addTerminalStatusDescriptors(descriptors); + + if (service.hasMethod(ScalarWebMethod.GETCURRENTEXTERNALINPUTSSTATUS)) { + addInputStatusDescriptors(descriptors); + } + + if (service.hasMethod(ScalarWebMethod.GETPARENTALRATINGSETTINGS)) { + addParentalRatingDescriptors(descriptors); + } + + // Note: must come AFTER terminal descriptors since we use the IDs generated from it + if (service.hasMethod(ScalarWebMethod.GETPLAYINGCONTENTINFO)) { + addPlayingContentDescriptors(descriptors); + } + + if (service.hasMethod(ScalarWebMethod.GETBLUETOOTHSETTINGS)) { + addGeneralSettingsDescriptor(descriptors, ScalarWebMethod.GETBLUETOOTHSETTINGS, BLUETOOTHSETTINGS, + "Bluetooth Setting"); + } + + if (service.hasMethod(ScalarWebMethod.GETPLAYBACKMODESETTINGS)) { + addGeneralSettingsDescriptor(descriptors, ScalarWebMethod.GETPLAYBACKMODESETTINGS, PLAYBACKSETTINGS, + "Playback Setting"); + } + } + + // update the terminal sources + updateTermSource(); + + return descriptors; + } + + /** + * Adds the content list descriptors + * + * @param descriptors the non-null, possibly empty list of descriptors + */ + private void addContentListDescriptors(final List descriptors) { + Objects.requireNonNull(descriptors, "descriptors cannot be null"); + + // The control (parent uri/uri/index) and virtual channels (childcount/selected) + descriptors.add(createDescriptor(createChannel(CN_PARENTURI), "String", "scalarwebavcontrolcontentparenturi")); + descriptors.add(createDescriptor(createChannel(CN_URI), "String", "scalarwebavcontrolcontenturi")); + descriptors.add(createDescriptor(createChannel(CN_INDEX), "Number", "scalarwebavcontrolcontentidx")); + descriptors + .add(createDescriptor(createChannel(CN_CHILDCOUNT), "Number", "scalarwebavcontrolcontentchildcount")); + + // final Map outputs = getTerminalOutputs(getTerminalStatuses()); + // + // for (Entry entry : outputs.entrySet()) { + // descriptors.add(createDescriptor(createChannel(CN_SELECTED, entry.getKey()), "Switch", + // "scalarwebavcontroltermstatusselected", "Content Play on Output " + entry.getValue(), null)); + // } + descriptors.add(createDescriptor(createChannel(CN_CMD), "String", "scalarwebavcontrolcontentcmd")); + + final String version = getVersion(ScalarWebMethod.GETCONTENTLIST); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0, ScalarWebMethod.V1_1)) { + descriptors.add( + createDescriptor(createChannel(CN_CHANNELNAME), "String", "scalarwebavcontrolcontentchannelname")); + descriptors.add(createDescriptor(createChannel(CN_DIRECTREMOTENUM), "Number", + "scalarwebavcontrolcontentdirectremotenum")); + descriptors.add(createDescriptor(createChannel(CN_DISPNUM), "String", "scalarwebavcontrolcontentdispnum")); + descriptors.add(createDescriptor(createChannel(CN_DURATIONSEC), "Number:Time", + "scalarwebavcontrolcontentdurationsec")); + descriptors.add(createDescriptor(createChannel(CN_FILESIZEBYTE), "Number:DataAmount", + "scalarwebavcontrolcontentfilesizebyte")); + descriptors.add(createDescriptor(createChannel(CN_ISALREADYPLAYED), "String", + "scalarwebavcontrolcontentisalreadyplayed")); + descriptors.add( + createDescriptor(createChannel(CN_ISPROTECTED), "String", "scalarwebavcontrolcontentisprotected")); + descriptors.add(createDescriptor(createChannel(CN_ORIGINALDISPNUM), "String", + "scalarwebavcontrolcontentoriginaldispnum")); + descriptors.add(createDescriptor(createChannel(CN_PROGRAMMEDIATYPE), "String", + "scalarwebavcontrolcontentprogrammediatype")); + descriptors.add( + createDescriptor(createChannel(CN_PROGRAMNUM), "Number", "scalarwebavcontrolcontentprogramnum")); + descriptors.add(createDescriptor(createChannel(CN_STARTDATETIME), "String", + "scalarwebavcontrolcontentstartdatetime")); + descriptors.add(createDescriptor(createChannel(CN_TITLE), "String", "scalarwebavcontrolcontenttitle")); + descriptors.add( + createDescriptor(createChannel(CN_TRIPLETSTR), "String", "scalarwebavcontrolcontenttripletstr")); + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_2, ScalarWebMethod.V1_3)) { + descriptors.add(createDescriptor(createChannel(CN_AUDIOCHANNEL), "String", + "scalarwebavcontrolcontentaudiochannel")); + descriptors.add( + createDescriptor(createChannel(CN_AUDIOCODEC), "String", "scalarwebavcontrolcontentaudiocodec")); + descriptors.add(createDescriptor(createChannel(CN_AUDIOFREQUENCY), "String", + "scalarwebavcontrolcontentaudiofrequency")); + descriptors.add( + createDescriptor(createChannel(CN_CHANNELNAME), "String", "scalarwebavcontrolcontentchannelname")); + descriptors.add(createDescriptor(createChannel(CN_CHANNELSURFINGVISIBILITY), "String", + "scalarwebavcontrolcontentchannelsurfingvisibility")); + descriptors.add(createDescriptor(createChannel(CN_CHAPTERCOUNT), "Number", + "scalarwebavcontrolcontentchaptercount")); + descriptors.add( + createDescriptor(createChannel(CN_CONTENTTYPE), "String", "scalarwebavcontrolcontentcontenttype")); + descriptors.add( + createDescriptor(createChannel(CN_CREATEDTIME), "String", "scalarwebavcontrolcontentcreatedtime")); + descriptors.add(createDescriptor(createChannel(CN_DIRECTREMOTENUM), "Number", + "scalarwebavcontrolcontentdirectremotenum")); + descriptors.add(createDescriptor(createChannel(CN_DISPNUM), "String", "scalarwebavcontrolcontentdispnum")); + descriptors.add(createDescriptor(createChannel(CN_DURATIONSEC), "Number:Time", + "scalarwebavcontrolcontentdurationsec")); + descriptors.add(createDescriptor(createChannel(CN_EPGVISIBILITY), "String", + "scalarwebavcontrolcontentepgvisibility")); + descriptors.add(createDescriptor(createChannel(CN_FILESIZEBYTE), "Number:DataAmount", + "scalarwebavcontrolcontentfilesizebyte")); + descriptors.add(createDescriptor(createChannel(CN_ISALREADYPLAYED), "String", + "scalarwebavcontrolcontentisalreadyplayed")); + descriptors.add( + createDescriptor(createChannel(CN_ISPROTECTED), "String", "scalarwebavcontrolcontentisprotected")); + descriptors.add(createDescriptor(createChannel(CN_ORIGINALDISPNUM), "String", + "scalarwebavcontrolcontentoriginaldispnum")); + descriptors.add(createDescriptor(createChannel(CN_PARENTALCOUNTRY), "String", + "scalarwebavcontrolcontentparentalcountry")); + descriptors.add(createDescriptor(createChannel(CN_PARENTALRATING), "String", + "scalarwebavcontrolcontentparentalrating")); + descriptors.add(createDescriptor(createChannel(CN_PARENTALSYSTEM), "String", + "scalarwebavcontrolcontentparentalsystem")); + descriptors + .add(createDescriptor(createChannel(CN_PRODUCTID), "String", "scalarwebavcontrolcontentproductid")); + descriptors.add(createDescriptor(createChannel(CN_PROGRAMMEDIATYPE), "String", + "scalarwebavcontrolcontentprogrammediatype")); + descriptors.add( + createDescriptor(createChannel(CN_PROGRAMNUM), "Number", "scalarwebavcontrolcontentprogramnum")); + descriptors.add( + createDescriptor(createChannel(CN_SIZEMB), "Number:DataAmount", "scalarwebavcontrolcontentsizemb")); + descriptors.add(createDescriptor(createChannel(CN_STARTDATETIME), "String", + "scalarwebavcontrolcontentstartdatetime")); + descriptors.add( + createDescriptor(createChannel(CN_STORAGEURI), "String", "scalarwebavcontrolcontentstorageuri")); + descriptors.add(createDescriptor(createChannel(CN_SUBTITLELANGUAGE), "String", + "scalarwebavcontrolcontentsubtitlelanguage")); + descriptors.add(createDescriptor(createChannel(CN_SUBTITLETITLE), "String", + "scalarwebavcontrolcontentsubtitletitle")); + descriptors.add(createDescriptor(createChannel(CN_TITLE), "String", "scalarwebavcontrolcontenttitle")); + descriptors.add( + createDescriptor(createChannel(CN_TRIPLETSTR), "String", "scalarwebavcontrolcontenttripletstr")); + descriptors.add(createDescriptor(createChannel(CN_USERCONTENTFLAG), "Switch", + "scalarwebavcontrolcontentusercontentflag")); + descriptors.add( + createDescriptor(createChannel(CN_VIDEOCODEC), "String", "scalarwebavcontrolcontentvideocodec")); + descriptors.add( + createDescriptor(createChannel(CN_VISIBILITY), "String", "scalarwebavcontrolcontentvisibility")); + + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_4)) { + descriptors + .add(createDescriptor(createChannel(CN_ALBUMNAME), "String", "scalarwebavcontrolcontentalbumname")); + descriptors.add(createDescriptor(createChannel(CN_ARTIST), "String", "scalarwebavcontrolcontentartist")); + descriptors.add(createDescriptor(createChannel(CN_AUDIOCHANNEL), "String", + "scalarwebavcontrolcontentaudiochannel")); + descriptors.add( + createDescriptor(createChannel(CN_AUDIOCODEC), "String", "scalarwebavcontrolcontentaudiocodec")); + descriptors.add(createDescriptor(createChannel(CN_AUDIOFREQUENCY), "String", + "scalarwebavcontrolcontentaudiofrequency")); + descriptors.add(createDescriptor(createChannel(CN_BROADCASTFREQ), "Number:Frequency", + "scalarwebavcontrolcontentbroadcastfreq")); + descriptors.add(createDescriptor(createChannel(CN_BROADCASTFREQBAND), "String", + "scalarwebavcontrolcontentbroadcastband")); + descriptors.add( + createDescriptor(createChannel(CN_CHANNELNAME), "String", "scalarwebavcontrolcontentchannelname")); + descriptors.add(createDescriptor(createChannel(CN_CHANNELSURFINGVISIBILITY), "String", + "scalarwebavcontrolcontentchannelsurfingvisibility")); + descriptors.add(createDescriptor(createChannel(CN_CHAPTERCOUNT), "Number", + "scalarwebavcontrolcontentchaptercount")); + descriptors.add( + createDescriptor(createChannel(CN_CONTENTKIND), "String", "scalarwebavcontrolcontentcontentkind")); + descriptors.add( + createDescriptor(createChannel(CN_CONTENTTYPE), "String", "scalarwebavcontrolcontentcontenttype")); + descriptors.add( + createDescriptor(createChannel(CN_CREATEDTIME), "String", "scalarwebavcontrolcontentcreatedtime")); + descriptors.add(createDescriptor(createChannel(CN_DIRECTREMOTENUM), "Number", + "scalarwebavcontrolcontentdirectremotenum")); + descriptors.add(createDescriptor(createChannel(CN_DISPNUM), "String", "scalarwebavcontrolcontentdispnum")); + descriptors.add(createDescriptor(createChannel(CN_DURATIONMSEC), "Number:Time", + "scalarwebavcontrolcontentdurationmsec")); + descriptors.add(createDescriptor(createChannel(CN_EPGVISIBILITY), "String", + "scalarwebavcontrolcontentepgvisibility")); + descriptors.add(createDescriptor(createChannel(CN_FILENO), "String", "scalarwebavcontrolcontentfileno")); + descriptors.add(createDescriptor(createChannel(CN_FILESIZEBYTE), "Number:DataAmount", + "scalarwebavcontrolcontentfilesizebyte")); + descriptors + .add(createDescriptor(createChannel(CN_FOLDERNO), "String", "scalarwebavcontrolcontentfolderno")); + descriptors.add(createDescriptor(createChannel(CN_GENRE), "String", "scalarwebavcontrolcontentgenre")); + descriptors.add(createDescriptor(createChannel(CN_IS3D), "String", "scalarwebavcontrolcontentis3d")); + descriptors.add(createDescriptor(createChannel(CN_ISALREADYPLAYED), "String", + "scalarwebavcontrolcontentisalreadyplayed")); + descriptors.add( + createDescriptor(createChannel(CN_ISBROWSABLE), "String", "scalarwebavcontrolcontentisbrowsable")); + descriptors.add( + createDescriptor(createChannel(CN_ISPLAYABLE), "String", "scalarwebavcontrolcontentisplayable")); + descriptors.add( + createDescriptor(createChannel(CN_ISPROTECTED), "String", "scalarwebavcontrolcontentisprotected")); + descriptors.add(createDescriptor(createChannel(CN_ORIGINALDISPNUM), "String", + "scalarwebavcontrolcontentoriginaldispnum")); + descriptors.add(createDescriptor(createChannel(CN_PARENTALCOUNTRY), "String", + "scalarwebavcontrolcontentparentalcountry")); + descriptors.add(createDescriptor(createChannel(CN_PARENTALRATING), "String", + "scalarwebavcontrolcontentparentalrating")); + descriptors.add(createDescriptor(createChannel(CN_PARENTALSYSTEM), "String", + "scalarwebavcontrolcontentparentalsystem")); + descriptors.add( + createDescriptor(createChannel(CN_PARENTINDEX), "Number", "scalarwebavcontrolcontentparentindex")); + descriptors.add(createDescriptor(createChannel(CN_PATH), "String", "scalarwebavcontrolcontentpath")); + descriptors.add(createDescriptor(createChannel(CN_PLAYLISTNAME), "String", + "scalarwebavcontrolcontentplaylistname")); + descriptors.add( + createDescriptor(createChannel(CN_PODCASTNAME), "String", "scalarwebavcontrolcontentpodcastname")); + descriptors + .add(createDescriptor(createChannel(CN_PRODUCTID), "String", "scalarwebavcontrolcontentproductid")); + descriptors.add(createDescriptor(createChannel(CN_PROGRAMMEDIATYPE), "String", + "scalarwebavcontrolcontentprogrammediatype")); + descriptors.add( + createDescriptor(createChannel(CN_PROGRAMNUM), "Number", "scalarwebavcontrolcontentprogramnum")); + descriptors.add(createDescriptor(createChannel(CN_REMOTEPLAYTYPE), "String", + "scalarwebavcontrolcontentremoteplaytype")); + descriptors.add( + createDescriptor(createChannel(CN_SIZEMB), "Number:DataAmount", "scalarwebavcontrolcontentsizemb")); + descriptors.add(createDescriptor(createChannel(CN_STARTDATETIME), "String", + "scalarwebavcontrolcontentstartdatetime")); + descriptors.add( + createDescriptor(createChannel(CN_STORAGEURI), "String", "scalarwebavcontrolcontentstorageuri")); + descriptors.add(createDescriptor(createChannel(CN_SUBTITLELANGUAGE), "String", + "scalarwebavcontrolcontentsubtitlelanguage")); + descriptors.add(createDescriptor(createChannel(CN_SUBTITLETITLE), "String", + "scalarwebavcontrolcontentsubtitletitle")); + descriptors.add(createDescriptor(createChannel(CN_TITLE), "String", "scalarwebavcontrolcontenttitle")); + descriptors.add( + createDescriptor(createChannel(CN_TRIPLETSTR), "String", "scalarwebavcontrolcontenttripletstr")); + descriptors.add(createDescriptor(createChannel(CN_USERCONTENTFLAG), "Switch", + "scalarwebavcontrolcontentusercontentflag")); + descriptors.add( + createDescriptor(createChannel(CN_VIDEOCODEC), "String", "scalarwebavcontrolcontentvideocodec")); + descriptors.add( + createDescriptor(createChannel(CN_VISIBILITY), "String", "scalarwebavcontrolcontentvisibility")); + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_5)) { + descriptors + .add(createDescriptor(createChannel(CN_ALBUMNAME), "String", "scalarwebavcontrolcontentalbumname")); + descriptors.add(createDescriptor(createChannel(CN_APPLICATIONNAME), "String", + "scalarwebavcontrolplapplicationname")); + descriptors.add(createDescriptor(createChannel(CN_ARTIST), "String", "scalarwebavcontrolcontentartist")); + descriptors.add(createDescriptor(createChannel(CN_AUDIOCHANNEL), "String", + "scalarwebavcontrolcontentaudiochannel")); + descriptors.add( + createDescriptor(createChannel(CN_AUDIOCODEC), "String", "scalarwebavcontrolcontentaudiocodec")); + descriptors.add(createDescriptor(createChannel(CN_AUDIOFREQUENCY), "String", + "scalarwebavcontrolcontentaudiofrequency")); + descriptors.add(createDescriptor(createChannel(CN_BIVLSERVICEID), "String", + "scalarwebavcontrolcontentbivlserviceid")); + descriptors.add( + createDescriptor(createChannel(CN_BIVLASSETID), "String", "scalarwebavcontrolcontentbivlassetid")); + descriptors.add(createDescriptor(createChannel(CN_BIVLPROVIDER), "String", + "scalarwebavcontrolcontentbivlprovider")); + descriptors.add(createDescriptor(createChannel(CN_BROADCASTFREQ), "Number:Frequency", + "scalarwebavcontrolcontentbroadcastfreq")); + descriptors.add(createDescriptor(createChannel(CN_BROADCASTFREQBAND), "String", + "scalarwebavcontrolcontentbroadcastband")); + // descriptors.add(createDescriptor(createChannel(CN_BROADCASTGENREINFO), "String", + // "scalarwebavcontrolcontextbroadcastGenreInfo")); + descriptors.add( + createDescriptor(createChannel(CN_CHANNELNAME), "String", "scalarwebavcontrolcontentchannelname")); + descriptors.add(createDescriptor(createChannel(CN_CHANNELSURFINGVISIBILITY), "String", + "scalarwebavcontrolcontentchannelsurfingvisibility")); + descriptors.add(createDescriptor(createChannel(CN_CHAPTERCOUNT), "Number", + "scalarwebavcontrolcontentchaptercount")); + descriptors.add( + createDescriptor(createChannel(CN_CHAPTERINDEX), "Number", "scalarwebavcontrolplchapterindex")); + descriptors + .add(createDescriptor(createChannel(CN_CLIPCOUNT), "Number", "scalarwebavcontrolcontentclipcount")); + // descriptors.add(createDescriptor(createChannel(CN_CONTENT), "String", + // "scalarwebavcontrolcontextcontent")); + descriptors.add( + createDescriptor(createChannel(CN_CONTENTKIND), "String", "scalarwebavcontrolcontentcontentkind")); + descriptors.add( + createDescriptor(createChannel(CN_CONTENTTYPE), "String", "scalarwebavcontrolcontentcontenttype")); + descriptors.add( + createDescriptor(createChannel(CN_CREATEDTIME), "String", "scalarwebavcontrolcontentcreatedtime")); + descriptors.add( + createDescriptor(createChannel(CN_CREATEDTIME), "String", "scalarwebavcontrolcontentcreatedtime")); + descriptors.add(createDescriptor(createChannel(CN_DABCOMPONENTLABEL), "String", + "scalarwebavcontrolcontentdabcomponentlabel")); + descriptors.add(createDescriptor(createChannel(CN_DABDYNAMICLABEL), "String", + "scalarwebavcontrolcontentdabdynamiclabel")); + descriptors.add(createDescriptor(createChannel(CN_DABENSEMBLELABEL), "String", + "scalarwebavcontrolcontentdabensemblelabel")); + descriptors.add(createDescriptor(createChannel(CN_DABSERVICELABEL), "String", + "scalarwebavcontrolcontentdabservicelabel")); + // descriptors.add(createDescriptor(createChannel(CN_DATAINFO), "String", + // "scalarwebavcontrolcontextdataInfo")); + // // todo + descriptors.add( + createDescriptor(createChannel(CN_DESCRIPTION), "String", "scalarwebavcontrolcontentdescription")); + descriptors.add(createDescriptor(createChannel(CN_DIRECTREMOTENUM), "Number", + "scalarwebavcontrolcontentdirectremotenum")); + descriptors.add(createDescriptor(createChannel(CN_DISPNUM), "String", "scalarwebavcontrolcontentdispnum")); + // descriptors.add(createDescriptor(createChannel(CN_DUBBINGINFO), "String", + // "scalarwebavcontrolcontextdubbingInfo")); // todo + descriptors.add(createDescriptor(createChannel(CN_DURATIONMSEC), "Number:Time", + "scalarwebavcontrolcontentdurationmsec")); + descriptors.add(createDescriptor(createChannel(CN_DURATIONSEC), "Number:Time", + "scalarwebavcontrolcontentdurationsec")); + descriptors.add(createDescriptor(createChannel(CN_EPGVISIBILITY), "String", + "scalarwebavcontrolcontentepgvisibility")); + descriptors.add(createDescriptor(createChannel(CN_EVENTID), "String", "scalarwebavcontrolcontenteventid")); + descriptors.add(createDescriptor(createChannel(CN_FILENO), "String", "scalarwebavcontrolcontentfileno")); + descriptors.add(createDescriptor(createChannel(CN_FILESIZEBYTE), "Number:DataAmount", + "scalarwebavcontrolcontentfilesizebyte")); + descriptors + .add(createDescriptor(createChannel(CN_FOLDERNO), "String", "scalarwebavcontrolcontentfolderno")); + descriptors.add(createDescriptor(createChannel(CN_GENRE), "String", "scalarwebavcontrolcontentgenre")); + descriptors.add(createDescriptor(createChannel(CN_GLOBALPLAYBACKCOUNT), "Number", + "scalarwebavcontrolcontentglobalplaybackcount")); + // descriptors.add(createDescriptor(createChannel(CN_GROUPINFO), "String", + // "scalarwebavcontrolcontextgroupInfo")); // TODO + descriptors + .add(createDescriptor(createChannel(CN_HASRESUME), "String", "scalarwebavcontrolcontenthasresume")); + descriptors.add(createDescriptor(createChannel(CN_IS3D), "String", "scalarwebavcontrolcontentis3d")); + descriptors.add(createDescriptor(createChannel(CN_IS4K), "String", "scalarwebavcontrolcontentis4k")); + descriptors.add(createDescriptor(createChannel(CN_ISALREADYPLAYED), "String", + "scalarwebavcontrolcontentisalreadyplayed")); + descriptors.add(createDescriptor(createChannel(CN_ISAUTODELETE), "String", + "scalarwebavcontrolcontentisautodelete")); + descriptors.add( + createDescriptor(createChannel(CN_ISBROWSABLE), "String", "scalarwebavcontrolcontentisbrowsable")); + descriptors.add(createDescriptor(createChannel(CN_ISNEW), "String", "scalarwebavcontrolcontentisnew")); + descriptors.add( + createDescriptor(createChannel(CN_ISPLAYABLE), "String", "scalarwebavcontrolcontentisplayable")); + descriptors.add( + createDescriptor(createChannel(CN_ISPLAYLIST), "String", "scalarwebavcontrolcontentisplaylist")); + descriptors.add( + createDescriptor(createChannel(CN_ISPROTECTED), "String", "scalarwebavcontrolcontentisprotected")); + descriptors.add(createDescriptor(createChannel(CN_ISSOUNDPHOTO), "String", + "scalarwebavcontrolcontentissoundphoto")); + descriptors + .add(createDescriptor(createChannel(CN_MEDIATYPE), "String", "scalarwebavcontrolcontentmediatype")); + descriptors.add(createDescriptor(createChannel(CN_ORIGINALDISPNUM), "String", + "scalarwebavcontrolcontentoriginaldispnum")); + descriptors.add(createDescriptor(createChannel(CN_OUTPUT), "String", "scalarwebavcontrolcontentoutput")); + descriptors.add(createDescriptor(createChannel(CN_PARENTALCOUNTRY), "String", + "scalarwebavcontrolcontentparentalcountry")); + descriptors.add(createDescriptor(createChannel(CN_PARENTALRATING), "String", + "scalarwebavcontrolcontentparentalrating")); + descriptors.add(createDescriptor(createChannel(CN_PARENTALSYSTEM), "String", + "scalarwebavcontrolcontentparentalsystem")); + descriptors.add( + createDescriptor(createChannel(CN_PARENTINDEX), "Number", "scalarwebavcontrolcontentparentindex")); + // descriptors.add(createDescriptor(createChannel(CN_PLAYLISTINFO), "String", + // "scalarwebavcontrolcontextplaylistInfo")); //todo + descriptors.add(createDescriptor(createChannel(CN_PLAYLISTNAME), "String", + "scalarwebavcontrolcontentplaylistname")); + // descriptors.add(createDescriptor(createChannel(CN_PLAYSPEED), "String", + // "scalarwebavcontrolcontextplaySpeed")); // todo + descriptors.add( + createDescriptor(createChannel(CN_PODCASTNAME), "String", "scalarwebavcontrolcontentpodcastname")); + // descriptors.add(createDescriptor(createChannel(CN_POSITION), "String", + // "scalarwebavcontrolcontextposition")); + // //todo + descriptors + .add(createDescriptor(createChannel(CN_PRODUCTID), "String", "scalarwebavcontrolcontentproductid")); + descriptors.add(createDescriptor(createChannel(CN_PROGRAMMEDIATYPE), "String", + "scalarwebavcontrolcontentprogrammediatype")); + descriptors.add( + createDescriptor(createChannel(CN_PROGRAMNUM), "Number", "scalarwebavcontrolcontentprogramnum")); + descriptors.add(createDescriptor(createChannel(CN_PROGRAMSERVICETYPE), "String", + "scalarwebavcontrolcontentprogramservicetype")); + descriptors.add(createDescriptor(createChannel(CN_PROGRAMTITLE), "String", + "scalarwebavcontrolcontentprogramtitle")); + // descriptors.add(createDescriptor(createChannel(CN_RECORDINGINFO), "String", + // "scalarwebavcontrolcontextrecordingInfo")); // todo + descriptors.add(createDescriptor(createChannel(CN_REMOTEPLAYTYPE), "String", + "scalarwebavcontrolcontentremoteplaytype")); + descriptors.add( + createDescriptor(createChannel(CN_REPEATTYPE), "String", "scalarwebavcontrolcontentrepeattype")); + descriptors.add(createDescriptor(createChannel(CN_SERVICE), "String", "scalarwebavcontrolcontentservice")); + descriptors.add( + createDescriptor(createChannel(CN_SIZEMB), "Number:DataAmount", "scalarwebavcontrolcontentsizemb")); + descriptors.add(createDescriptor(createChannel(CN_SOURCE), "String", "scalarwebavcontrolcontentsource")); + descriptors.add( + createDescriptor(createChannel(CN_SOURCELABEL), "String", "scalarwebavcontrolcontentsourcelabel")); + descriptors.add(createDescriptor(createChannel(CN_STARTDATETIME), "String", + "scalarwebavcontrolcontentstartdatetime")); + descriptors.add(createDescriptor(createChannel(CN_STATE), "String", "scalarwebavcontrolcontentstate")); + descriptors.add(createDescriptor(createChannel(CN_STATESUPPLEMENT), "String", + "scalarwebavcontrolcontentstatesupplement")); + descriptors.add( + createDescriptor(createChannel(CN_STORAGEURI), "String", "scalarwebavcontrolcontentstorageuri")); + descriptors.add(createDescriptor(createChannel(CN_SUBTITLELANGUAGE), "String", + "scalarwebavcontrolcontentsubtitlelanguage")); + descriptors.add(createDescriptor(createChannel(CN_SUBTITLETITLE), "String", + "scalarwebavcontrolcontentsubtitletitle")); + descriptors.add(createDescriptor(createChannel(CN_SYNCCONTENTPRIORITY), "String", + "scalarwebavcontrolcontentsynccontentpriority")); + descriptors.add(createDescriptor(createChannel(CN_TITLE), "String", "scalarwebavcontrolcontenttitle")); + descriptors.add( + createDescriptor(createChannel(CN_TOTALCOUNT), "Number", "scalarwebavcontrolcontenttotalcount")); + descriptors.add( + createDescriptor(createChannel(CN_TRIPLETSTR), "String", "scalarwebavcontrolcontenttripletstr")); + descriptors.add(createDescriptor(createChannel(CN_USERCONTENTFLAG), "Switch", + "scalarwebavcontrolcontentusercontentflag")); + descriptors.add( + createDescriptor(createChannel(CN_VIDEOCODEC), "String", "scalarwebavcontrolcontentvideocodec")); + descriptors.add( + createDescriptor(createChannel(CN_VISIBILITY), "String", "scalarwebavcontrolcontentvisibility")); + } + } + + /** + * Adds input status descriptors for a specific input + * + * @param descriptors a non-null, possibly empty list of descriptors + * @param id a non-null, non-empty channel id + * @param uri a non-null, non-empty input uri + * @param title a non-null, non-empty input title + * @param apiVersion a non-null, non-empty API version + */ + private void addInputStatusDescriptor(final List descriptors, final String id, + final String uri, final String title, final String apiVersion) { + Objects.requireNonNull(descriptors, "descriptors cannot be null"); + SonyUtil.validateNotEmpty(id, "id cannot be empty"); + SonyUtil.validateNotEmpty(uri, "uri cannot be empty"); + SonyUtil.validateNotEmpty(title, "title cannot be empty"); + SonyUtil.validateNotEmpty(apiVersion, "apiVersion cannot be empty"); + + descriptors.add(createDescriptor(createChannel(IN_URI, id, uri), "String", "scalarwebavcontrolinpstatusuri", + "Input " + title + " URI", null)); + + descriptors.add(createDescriptor(createChannel(IN_TITLE, id, uri), "String", "scalarwebavcontrolinpstatustitle", + "Input " + title + " Title", null)); + + descriptors.add(createDescriptor(createChannel(IN_CONNECTION, id, uri), "Switch", + "scalarwebavcontrolinpstatusconnection", "Input " + title + " Connected", uri)); + + descriptors.add(createDescriptor(createChannel(IN_LABEL, id, uri), "String", "scalarwebavcontrolinpstatuslabel", + "Input " + title + " Label", null)); + + descriptors.add(createDescriptor(createChannel(IN_ICON, id, uri), "String", "scalarwebavcontrolinpstatusicon", + "Input " + title + " Icon Type", null)); + + if (ScalarWebMethod.V1_1.equalsIgnoreCase(apiVersion)) { + descriptors.add(createDescriptor(createChannel(IN_STATUS, id, uri), "String", + "scalarwebavcontrolinpstatusstatus", "Input " + title + " Status", null)); + } + } + + /** + * Adds all input status descriptors + * + * @param descriptors a non-null, possibly empty list of descriptors + */ + private void addInputStatusDescriptors(final List descriptors) { + Objects.requireNonNull(descriptors, "descriptors cannot be null"); + + try { + final ScalarWebResult result = getInputStatus(); + final String version = getService().getVersion(ScalarWebMethod.GETCURRENTEXTERNALINPUTSSTATUS); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + for (final CurrentExternalInputsStatus_1_0 status : result + .asArray(CurrentExternalInputsStatus_1_0.class)) { + final String uri = status.getUri(); + if (uri == null || uri.isEmpty()) { + logger.debug("External Input status had no URI (which is required): {}", status); + continue; + } + + final String id = createChannelId(uri, true); + addInputStatusDescriptor(descriptors, id, uri, status.getTitle(MAINTITLE), ScalarWebMethod.V1_0); + } + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_1)) { + for (final CurrentExternalInputsStatus_1_1 status : result + .asArray(CurrentExternalInputsStatus_1_1.class)) { + final String uri = status.getUri(); + if (uri == null || uri.isEmpty()) { + logger.debug("External Input status had no URI (which is required): {}", status); + continue; + } + + final String id = createChannelId(uri, true); + addInputStatusDescriptor(descriptors, id, uri, status.getTitle(MAINTITLE), ScalarWebMethod.V1_1); + } + } + } catch (final IOException e) { + logger.debug("Error add input status description {}", e.getMessage()); + } + } + + /** + * Adds the parental rating descriptors + * + * @param descriptors a non-null, possibly empty list of descriptors + */ + private void addParentalRatingDescriptors(final List descriptors) { + Objects.requireNonNull(descriptors, "descriptors cannot be null"); + + try { + // execute to verify if it exists + getParentalRating(); + + descriptors.add( + createDescriptor(createChannel(PR_RATINGTYPEAGE), "Number", "scalarwebavcontrolprratingtypeage")); + descriptors.add( + createDescriptor(createChannel(PR_RATINGTYPESONY), "String", "scalarwebavcontrolprratingtypesony")); + descriptors.add( + createDescriptor(createChannel(PR_RATINGCOUNTRY), "String", "scalarwebavcontrolprratingcountry")); + descriptors.add(createDescriptor(createChannel(PR_RATINGCUSTOMTYPETV), "String", + "scalarwebavcontrolprratingcustomtypetv")); + descriptors.add(createDescriptor(createChannel(PR_RATINGCUSTOMTYPEMPAA), "String", + "scalarwebavcontrolprratingcustomtypempaa")); + descriptors.add(createDescriptor(createChannel(PR_RATINGCUSTOMTYPECAENGLISH), "String", + "scalarwebavcontrolprratingcustomtypecaenglish")); + descriptors.add(createDescriptor(createChannel(PR_RATINGCUSTOMTYPECAFRENCH), "String", + "scalarwebavcontrolprratingcustomtypecafrench")); + descriptors + .add(createDescriptor(createChannel(PR_UNRATEDLOCK), "Switch", "scalarwebavcontrolprunratedlock")); + } catch (final IOException e) { + logger.debug("Exception occurring getting the parental ratings: {}", e.getMessage()); + } + } + + /** + * Adds the playing content descriptors + * + * @param descriptors a non-null, possibly empty list of descriptors + */ + private void addPlayingContentDescriptors(final List descriptors) { + Objects.requireNonNull(descriptors, "descriptors cannot be null"); + + final Map outputs = getTerminalOutputs(getTerminalStatuses()); + + final String version = getService().getVersion(ScalarWebMethod.GETPLAYINGCONTENTINFO); + + for (final Entry entry : outputs.entrySet()) { + final String translatedOutput = getTranslatedOutput(entry.getKey()); + final String prefix = "Playing" + + (SonyUtil.isEmpty(translatedOutput) ? " " : (" (" + entry.getValue() + ") ")); + + final String uri = entry.getKey(); + final String id = getIdForOutput(uri); // use the same id as the related terminal + + descriptors.add(createDescriptor(createChannel(PL_CMD, id, uri), "String", "scalarwebavcontrolplcommand", + prefix + "Command", null)); + + descriptors.add(createDescriptor(createChannel(PL_PRESET, id, uri), "Number", "scalarwebavcontrolplpreset", + prefix + "Preset", null)); + + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0, ScalarWebMethod.V1_1, ScalarWebMethod.V1_2)) { + descriptors.add(createDescriptor(createChannel(PL_BIVLASSETID, id, uri), "String", + "scalarwebavcontrolplbivlassetid", prefix + "BIVL AssetID", null)); + + descriptors.add(createDescriptor(createChannel(PL_BIVLPROVIDER, id, uri), "String", + "scalarwebavcontrolplbivlprovider", prefix + "BIVL Provider", null)); + + descriptors.add(createDescriptor(createChannel(PL_BIVLSERVICEID, id, uri), "String", + "scalarwebavcontrolplbivlserviceid", prefix + "BIVL ServiceID", null)); + + descriptors.add(createDescriptor(createChannel(PL_DISPNUM, id, uri), "String", + "scalarwebavcontrolpldispnum", prefix + "Display Number", null)); + + descriptors.add(createDescriptor(createChannel(PL_DURATIONSEC, id, uri), "Number:Time", + "scalarwebavcontrolpldurationsec", prefix + "Duraction (in seconds)", null)); + + descriptors.add(createDescriptor(createChannel(PL_MEDIATYPE, id, uri), "String", + "scalarwebavcontrolplmediatype", prefix + "Media Type", null)); + + descriptors.add(createDescriptor(createChannel(PL_ORIGINALDISPNUM, id, uri), "String", + "scalarwebavcontrolploriginaldispnum", prefix + "Original Display Number", null)); + + descriptors.add(createDescriptor(createChannel(PL_PLAYSPEED, id, uri), "String", + "scalarwebavcontrolplplayspeed", prefix + "Play Speed", null)); + + descriptors.add(createDescriptor(createChannel(PL_PROGRAMNUM, id, uri), "Number", + "scalarwebavcontrolplprogramnum", prefix + "Program Number", null)); + + descriptors.add(createDescriptor(createChannel(PL_PROGRAMTITLE, id, uri), "String", + "scalarwebavcontrolplprogramtitle", prefix + "Program Title", null)); + + descriptors.add(createDescriptor(createChannel(PL_SOURCE, id, uri), "String", + "scalarwebavcontrolplsource", prefix + "Source", null)); + + descriptors.add(createDescriptor(createChannel(PL_STARTDATETIME, id, uri), "String", + "scalarwebavcontrolplstartdatetime", prefix + "Start Date/Time", null)); + + descriptors.add(createDescriptor(createChannel(PL_TITLE, id, uri), "String", + "scalarwebavcontrolpltitle", prefix + "Title", null)); + + descriptors.add(createDescriptor(createChannel(PL_TRIPLETSTR, id, uri), "String", + "scalarwebavcontrolpltripletstr", prefix + "Triplet", null)); + + descriptors.add(createDescriptor(createChannel(PL_URI, id, uri), "String", "scalarwebavcontrolpluri", + prefix + "URI", null)); + + } + + if (VersionUtilities.equals(version, ScalarWebMethod.V1_2)) { + descriptors.add(createDescriptor(createChannel(PL_ALBUMNAME, id, uri), "String", + "scalarwebavcontrolplalbumname", prefix + "Album Name", null)); + descriptors.add(createDescriptor(createChannel(PL_APPLICATIONNAME, id, uri), "String", + "scalarwebavcontrolplapplicationname", prefix + "Application Name", null)); + descriptors.add(createDescriptor(createChannel(PL_ARTIST, id, uri), "String", + "scalarwebavcontrolplartist", prefix + "Artist", null)); + descriptors.add(createDescriptor(createChannel(PL_AUDIOCHANNEL, id, uri), "String", + "scalarwebavcontrolplaudiochannel", prefix + "Audio Channel", null)); + descriptors.add(createDescriptor(createChannel(PL_AUDIOCODEC, id, uri), "String", + "scalarwebavcontrolplaudiocodec", prefix + "Audio Codec", null)); + descriptors.add(createDescriptor(createChannel(PL_AUDIOFREQUENCY, id, uri), "String", + "scalarwebavcontrolplaudiofrequency", prefix + "Audio Frequency", null)); + descriptors.add(createDescriptor(createChannel(PL_BROADCASTFREQ, id, uri), "Number:Frequency", + "scalarwebavcontrolplbroadcastfreq", prefix + "Broadcast Frequency", null)); + descriptors.add(createDescriptor(createChannel(PL_BROADCASTFREQBAND, id, uri), "String", + "scalarwebavcontrolplbroadcastfreqband", prefix + "Broadcast Frequency Band", null)); + descriptors.add(createDescriptor(createChannel(PL_CHANNELNAME, id, uri), "String", + "scalarwebavcontrolplchannelname", prefix + "Channel Name", null)); + descriptors.add(createDescriptor(createChannel(PL_CHAPTERCOUNT, id, uri), "Number", + "scalarwebavcontrolplchaptercount", prefix + "Chapter Count", null)); + descriptors.add(createDescriptor(createChannel(PL_CHAPTERINDEX, id, uri), "Number", + "scalarwebavcontrolplchapterindex", prefix + "Chapter Index", null)); + descriptors.add(createDescriptor(createChannel(PL_CONTENTKIND, id, uri), "String", + "scalarwebavcontrolplcontentkind", prefix + "Content Kind", null)); + descriptors.add(createDescriptor(createChannel(PL_DABCOMPONENTLABEL, id, uri), "String", + "scalarwebavcontrolpldabcomponentlabel", prefix + "DAB Component Label", null)); + descriptors.add(createDescriptor(createChannel(PL_DABDYNAMICLABEL, id, uri), "String", + "scalarwebavcontrolpldabdynamiclabel", prefix + "DAB Dynamic Label", null)); + descriptors.add(createDescriptor(createChannel(PL_DABENSEMBLELABEL, id, uri), "String", + "scalarwebavcontrolpldabensemblelabel", prefix + "DAB Ensemble Label", null)); + descriptors.add(createDescriptor(createChannel(PL_DABSERVICELABEL, id, uri), "String", + "scalarwebavcontrolpldabservicelabel", prefix + "DAB Service Label", null)); + descriptors.add(createDescriptor(createChannel(PL_DURATIONMSEC, id, uri), "Number:Time", + "scalarwebavcontrolpldurationmsec", prefix + "Duration (in milliseconds)", null)); + descriptors.add(createDescriptor(createChannel(PL_FILENO, id, uri), "String", + "scalarwebavcontrolplfileno", prefix + "File Number", null)); + descriptors.add(createDescriptor(createChannel(PL_GENRE, id, uri), "String", + "scalarwebavcontrolplgenre", prefix + "Genre", null)); + descriptors.add(createDescriptor(createChannel(PL_INDEX, id, uri), "Number", + "scalarwebavcontrolplindex", prefix + "Index", null)); + descriptors.add(createDescriptor(createChannel(PL_IS3D, id, uri), "String", "scalarwebavcontrolplis3d", + prefix + "is 3D", null)); + descriptors.add(createDescriptor(createChannel(PL_OUTPUT, id, uri), "String", + "scalarwebavcontrolploutput", prefix + "Output", null)); + descriptors.add(createDescriptor(createChannel(PL_PARENTINDEX, id, uri), "Number", + "scalarwebavcontrolplparentindex", prefix + "Parent Index", null)); + descriptors.add(createDescriptor(createChannel(PL_PARENTURI, id, uri), "String", + "scalarwebavcontrolplparenturi", prefix + "Parent URI", null)); + descriptors.add(createDescriptor(createChannel(PL_PATH, id, uri), "String", "scalarwebavcontrolplpath", + prefix + "Path", null)); + descriptors.add(createDescriptor(createChannel(PL_PLAYLISTNAME, id, uri), "String", + "scalarwebavcontrolplplaylistname", prefix + "Play List Name", null)); + descriptors.add(createDescriptor(createChannel(PL_PLAYSTEPSPEED, id, uri), "Number", + "scalarwebavcontrolplplaystepspeed", prefix + "Play Step Speed", null)); + descriptors.add(createDescriptor(createChannel(PL_PODCASTNAME, id, uri), "String", + "scalarwebavcontrolplpodcastname", prefix + "Podcast Name", null)); + descriptors.add(createDescriptor(createChannel(PL_POSITIONMSEC, id, uri), "Number:Time", + "scalarwebavcontrolplpositionmsec", prefix + "Position (in milliseconds)", null)); + descriptors.add(createDescriptor(createChannel(PL_POSITIONSEC, id, uri), "Number:Time", + "scalarwebavcontrolplpositionsec", prefix + "Position (in seconds)", null)); + descriptors.add(createDescriptor(createChannel(PL_REPEATTYPE, id, uri), "String", + "scalarwebavcontrolplrepeattype", prefix + "Repeat Type", null)); + descriptors.add(createDescriptor(createChannel(PL_SERVICE, id, uri), "String", + "scalarwebavcontrolplservice", prefix + "Service", null)); + descriptors.add(createDescriptor(createChannel(PL_SOURCELABEL, id, uri), "String", + "scalarwebavcontrolplsourcelabel", prefix + "Source Label", null)); + descriptors.add(createDescriptor(createChannel(PL_STATE, id, uri), "String", + "scalarwebavcontrolplstate", prefix + "State", null)); + descriptors.add(createDescriptor(createChannel(PL_STATESUPPLEMENT, id, uri), "String", + "scalarwebavcontrolplstatesupplement", prefix + "State Supplement", null)); + descriptors.add(createDescriptor(createChannel(PL_SUBTITLEINDEX, id, uri), "Number", + "scalarwebavcontrolplsubtitleindex", prefix + "Subtitle Index", null)); + descriptors.add(createDescriptor(createChannel(PL_TOTALCOUNT, id, uri), "Number", + "scalarwebavcontrolpltotalcount", prefix + "Total Count", null)); + descriptors.add(createDescriptor(createChannel(PL_VIDEOCODEC, id, uri), "String", + "scalarwebavcontrolplvideocodec", prefix + "Video Codec", null)); + } + } + } + + /** + * Adds the terminal status descriptors + * + * @param descriptors a non-null, possibly empty list of descriptors + */ + private void addTerminalStatusDescriptors(final List descriptors) { + Objects.requireNonNull(descriptors, "descriptors cannot be null"); + + for (final CurrentExternalTerminalsStatus_1_0 term : getTerminalStatuses()) { + final String uri = term.getUri(); + if (uri == null) { + logger.debug("External Terminal status had no URI (which is required): {}", term); + continue; + } + + final String title = term.getTitle(MAINTITLE); + final String id = createChannelId(uri, false); + + if (term.isOutput()) { + descriptors.add(createDescriptor(createChannel(TERM_SOURCE, id, uri), "String", + "scalarwebavcontroltermstatussource", "Terminal " + title + " Source", null)); + + // if not our dummy 'main', create an active switch + if (!MAINTITLE.equalsIgnoreCase(title)) { + descriptors.add(createDescriptor(createChannel(TERM_ACTIVE, id, uri), "Switch", + "scalarwebavcontroltermstatusactive", "Terminal " + title + " Active", null)); + } + } + + descriptors.add(createDescriptor(createChannel(TERM_URI, id, uri), "String", + "scalarwebavcontroltermstatusuri", "Terminal " + title + " URI", null)); + + descriptors.add(createDescriptor(createChannel(TERM_TITLE, id, uri), "String", + "scalarwebavcontroltermstatustitle", "Terminal " + title + " Title", null)); + + descriptors.add(createDescriptor(createChannel(TERM_CONNECTION, id, uri), "String", + "scalarwebavcontroltermstatusconnection", "Terminal " + title + " Connection", null)); + + descriptors.add(createDescriptor(createChannel(TERM_LABEL, id, uri), "String", + "scalarwebavcontroltermstatuslabel", "Terminal " + title + " Label", null)); + + descriptors.add(createDescriptor(createChannel(TERM_ICON, id, uri), "Image", + "scalarwebavcontroltermstatusicon", "Terminal " + title + " Icon", null)); + + } + } + + /** + * Adds the preset channel descriptors + * + * @param descriptors a non-null, possibly empty list of descriptors + */ + private void addPresetChannelDescriptors(final List descriptors) { + Objects.requireNonNull(descriptors, "descriptors cannot be null"); + + for (final Scheme scheme : getSchemes()) { + if (Scheme.TV.equalsIgnoreCase(scheme.getScheme()) || Scheme.RADIO.equalsIgnoreCase(scheme.getScheme())) { + for (final Source src : getSources(scheme)) { + addPresetChannelDescriptor(descriptors, src); + } + } + } + } + + /** + * Adds the preset channel descriptors for a specific source + * + * @param descriptors a non-null, possibly empty list of descriptors + * @param src a non-null source + */ + private void addPresetChannelDescriptor(final List descriptors, final Source src) { + Objects.requireNonNull(descriptors, "descriptors cannot be null"); + Objects.requireNonNull(src, "src cannot be null"); + + final String source = src.getSource(); + if (source == null || source.isEmpty()) { + logger.debug("Source did not have a source assigned: {}", src); + return; + } + + final String sourcePart = src.getSourcePart(); + if (sourcePart == null || sourcePart.isEmpty()) { + logger.debug("Source had a malformed source (no source part or no scheme): {}", src); + return; + } + + final ScalarWebChannel chl = createChannel(PS_CHANNEL, sourcePart, source); + + final String upperSrc = sourcePart.toUpperCase(); + descriptors.add(createDescriptor(chl, "String", "scalarwebavcontrolpresetchannel", "Presets for " + upperSrc, + "Set preset for " + upperSrc)); + + refreshPresetChannelStateDescription(Collections.singletonList(chl)); + } + + @Override + protected void eventReceived(final ScalarWebEvent event) throws IOException { + Objects.requireNonNull(event, "event cannot be null"); + final @Nullable String mtd = event.getMethod(); + if (mtd == null || mtd.isEmpty()) { + logger.debug("Unhandled event received (no method): {}", event); + } else { + switch (mtd) { + case ScalarWebEvent.NOTIFYPLAYINGCONTENTINFO: + final String version = getVersion(ScalarWebMethod.GETPLAYINGCONTENTINFO); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0, ScalarWebMethod.V1_1)) { + notifyPlayingContentInfo(event.as(PlayingContentInfoResult_1_0.class), + getIdForOutput(MAINOUTPUT)); + } else { + final PlayingContentInfoResult_1_2 res = event.as(PlayingContentInfoResult_1_2.class); + final String output = res.getOutput(MAINOUTPUT); + notifyPlayingContentInfo(res, getIdForOutput(output)); + } + + break; + + case ScalarWebEvent.NOTIFYAVAILABLEPLAYBACKFUNCTION: + // TODO + break; + + case ScalarWebEvent.NOTIFYEXTERNALTERMINALSTATUS: + notifyCurrentTerminalStatus(event.as(CurrentExternalTerminalsStatus_1_0.class)); + break; + + default: + logger.debug("Unhandled event received: {}", event); + break; + } + } + } + + /** + * Get's the channel id for the given output + * + * @param output a non-null, non-empty output identifier + * @return a channel identifier representing the output + */ + private String getIdForOutput(final String output) { + SonyUtil.validateNotEmpty(output, "output cannot be empty"); + + for (final Channel chl : getContext().getThing().getChannels()) { + final ScalarWebChannel swc = new ScalarWebChannel(chl); + if (TERM_URI.equalsIgnoreCase(swc.getCategory())) { + final String uri = swc.getPathPart(0); + if (SonyUtil.equals(uri, output)) { + return swc.getId(); + } + } + } + + return SonyUtil.createValidChannelUId(output); + } + + /** + * Get the source identifier for the given URL + * + * @param uid a non-null, non-empty source + * @return the source identifier (or uid if none found) + */ + private String getSourceFromUri(final String uid) { + SonyUtil.validateNotEmpty(uid, "uid cannot be empty"); + // Following finds the source from the uri (radio:fm&content=x to radio:fm) + // ToDo: Check + // if source uri map contains the provide uid as key use this to allow state updates conistent with the state + // descrrption of the + // terminal source channel. This is important for specific sources which uri contains parameters (like + // extInput:hdmi?port=1) + final String src = sourceUriTitleMap.containsKey(uid) ? uid + : getSources().stream() + .filter(s -> s.getSource() != null && uid.startsWith(Objects.requireNonNull(s.getSource()))) + .findFirst().map(s -> s.getSource()).orElse(""); + return SonyUtil.defaultIfEmpty(src, uid); + } + + /** + * Returns the current input statuses. This method simply calls getInputStatus(false) to get the cached result if it + * exists + * + * @return the ScalarWebResult containing the status information for all the inputs + * @throws IOException if an IOException occurrs getting the input status + */ + private ScalarWebResult getInputStatus() throws IOException { + return getInputStatus(false); + } + + /** + * Returns the current input status. If refresh is false, the cached version is used (if it exists) - otherwise we + * query the device for the statuses + * + * @param refresh true to refresh from the device, false to potentially use a cached version (if it exists) + * @return the ScalarWebResult containing the status information for all the inputs + * @throws IOException if an IOException occurrs getting the input status + */ + private ScalarWebResult getInputStatus(final boolean refresh) throws IOException { + if (!refresh) { + final ScalarWebResult rs = stateInputs.get(); + if (rs != null) { + return rs; + } + } + final ScalarWebResult res = execute(ScalarWebMethod.GETCURRENTEXTERNALINPUTSSTATUS); + stateInputs.set(res); + return res; + } + + /** + * Get's the parental rating from the device + * + * @return the ScalarWebResult containing the status information for all the inputs + * @throws IOException if an IOException occurrs getting the input status + */ + private ScalarWebResult getParentalRating() throws IOException { + return execute(ScalarWebMethod.GETPARENTALRATINGSETTINGS); + } + + /** + * Gets the playing content info + * + * @return the ScalarWebResult containing the status information for all the inputs + * @throws IOException if an IOException occurrs getting the input status + */ + private ScalarWebResult getPlayingContentInfo() throws IOException { + return execute(ScalarWebMethod.GETPLAYINGCONTENTINFO, version -> { + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0, ScalarWebMethod.V1_1)) { + return null; + } + return new PlayingContentInfoRequest_1_2(""); + }); + } + + /** + * Returns the current set of schemes. This method simply calls getSchemes(false) to get the cached result if it + * exists + * + * @return a non-null, possibly empty set of schemes + */ + private Set getSchemes() { + return getSchemes(false); + } + + /** + * Returns the current set of schemes. If refresh is false, the cached version is used (if it exists) - otherwise we + * query the device for the schemes + * + * @param refresh true to refresh from the device, false to potentially use a cached version (if it exists) + * @return a non-null, possibly empty set of schemes + */ + private Set getSchemes(final boolean refresh) { + final Set cacheSchemes = stateSchemes.get(); + if (!cacheSchemes.isEmpty() && !refresh) { + return cacheSchemes; + } + + final Set schemes = new HashSet<>(); + try { + for (final Scheme scheme : execute(ScalarWebMethod.GETSCHEMELIST).asArray(Scheme.class)) { + final String schemeName = scheme.getScheme(); + if (schemeName != null && !schemeName.isEmpty()) { + schemes.add(scheme); + } + } + } catch (final IOException e) { + logger.debug("Exception occurred retrieving the scheme list: {}", e.getMessage()); + } + + stateSchemes.set(schemes); + return schemes; + } + + /** + * Returns the current set of sources. This method simply calls getSources(false) to get the cached result if it + * exists + * + * @return a non-null, possibly empty set of sources + */ + private Set getSources() { + return getSources(false); + } + + /** + * Gets list of sources for a scheme. Some schemes are not valid for specific versions of the source (like DLNA is + * only valid 1.2+) - so query all sources by scheme and return a consolidated list + * + * @param refresh true to refresh from the device, false to potentially use a cached version (if it exists) + * @return the non-null, possibly empty list of sources + */ + private Set getSources(final boolean refresh) { + final Set sources = getSchemes().stream().flatMap(s -> getSources(s, refresh).stream()) + .collect(Collectors.toSet()); + + if (refresh) { + updateTermSource(); + } + + return sources; + } + + /** + * Gets a list of sources for a scheme + * + * @param scheme a non-null scheme + * @return a non-null, possibly empty list of sources + */ + private Set getSources(final Scheme scheme) { + Objects.requireNonNull(scheme, "scheme cannot be null"); + return getSources(scheme, false); + } + + /** + * Gets a list of sources for a scheme, possibly refreshing them first + * + * @param scheme a non-null scheme + * @param refresh true to refresh first, false to use cached + * @return a non-null, possibly empty list of sources + */ + private Set getSources(final Scheme scheme, final boolean refresh) { + Objects.requireNonNull(scheme, "scheme cannot be null"); + + final String schemeName = scheme.getScheme(); + if (schemeName == null || schemeName.isEmpty()) { + return Collections.emptySet(); + } + + return stateSources.compute(schemeName, (k, v) -> { + if (v != null && !v.isEmpty() && !refresh) { + return v; + } + + final Set srcs = new HashSet<>(); + try { + for (final String version : getService().getVersions(ScalarWebMethod.GETSOURCELIST)) { + final ScalarWebResult result = getService().executeSpecific(ScalarWebMethod.GETSOURCELIST, version, + scheme); + + // This can happen if the specific version source doesn't support the scheme for + if (result.getDeviceErrorCode() == ScalarWebError.NOTIMPLEMENTED + || result.getDeviceErrorCode() == ScalarWebError.UNSUPPORTEDOPERATION + || result.getDeviceErrorCode() == ScalarWebError.ILLEGALARGUMENT) { + logger.trace("Source version {} for scheme {} is not implemented", version, scheme); + } else { + for (final Source src : result.asArray(Source.class)) { + final String sourceName = src.getSource(); + if (sourceName != null && !sourceName.isEmpty()) { + srcs.add(src); + } + } + } + } + } catch (final IOException e) { + logger.debug("Exception occurred retrieving the source list for scheme {}: {}", scheme, e.getMessage()); + } + return srcs; + }); + } + + /** + * Updates the terminal sources + */ + private void updateTermSource() { + final List sources = new ArrayList<>(); + if (getService().hasMethod(ScalarWebMethod.GETCURRENTEXTERNALINPUTSSTATUS) + && !getService().hasMethod(ScalarWebMethod.GETCURRENTEXTERNALTERMINALSSTATUS)) { + // no need to do versioning since everything we want is in v1_0 (and v1_1 inherites from it) + try { + for (final CurrentExternalInputsStatus_1_0 inp : getInputStatus() + .asArray(CurrentExternalInputsStatus_1_0.class)) { + final String uri = inp.getUri(); + if (uri == null || uri.isEmpty()) { + continue; + } + + final String title = inp.getTitle(uri); + sources.add(new InputSource(uri, title, null)); + } + } catch (final IOException e) { + logger.debug("Error updating terminal source {}", e.getMessage()); + } + } + + stateSources.values().stream().flatMap(e -> e.stream()).forEach(src -> { + final String uri = src.getSource(); + if (uri != null && !uri.isEmpty()) { + final String[] outputs = src.getOutputs(); + + // Add the source if not duplicating an input/terminal + // (need to use starts with because hdmi source has a port number in it and + // the source is just hdmi [no ports]) + // ToDo: Check + if (!sources.stream().anyMatch(s -> s.getUri().toLowerCase().startsWith(uri.toLowerCase()))) { + sources.add(new InputSource(uri, src.getTitle(), + outputs == null ? new ArrayList<>() : Arrays.asList(outputs))); + } + } + }); + + for (final CurrentExternalTerminalsStatus_1_0 term : getTerminalStatuses()) { + if (term.isOutput()) { + final String uri = term.getUri(); + if (uri != null && !uri.isEmpty()) { + final List options = sources.stream() + .filter(s -> s.getOutputs().isEmpty() || s.getOutputs().contains(uri)) + .map(s -> new StateOption(s.getUri(), s.getTitle())) + .sorted(Comparator.comparing(a -> SonyUtil.defaultIfEmpty(a.getLabel(), ""))) + .collect(Collectors.toList()); + + // store full source uris and their title in a global map to allow consistent state updates + sourceUriTitleMap.clear(); + for (StateOption option : options) { + sourceUriTitleMap.put(option.getValue(), SonyUtil.defaultIfEmpty(option.getLabel(), "")); + } + final String id = getIdForOutput(uri); + final ScalarWebChannel cnl = createChannel(TERM_SOURCE, id, uri); + final StateDescription sd = StateDescriptionFragmentBuilder.create().withOptions(options).build() + .toStateDescription(); + if (sd != null) { + getContext().getStateProvider().addStateOverride(getContext().getThingUID(), cnl.getChannelId(), + sd); + } + } + } + } + } + + /** + * Gets a terminal status + * + * @return a non-null {@link ScalarWebResult} + * @throws IOException if an IO exception occurs + */ + private ScalarWebResult getTerminalStatus() throws IOException { + return execute(ScalarWebMethod.GETCURRENTEXTERNALTERMINALSSTATUS); + } + + /** + * Gets the list of terminal status + * + * @return a non-null, possibly empty list of terminal status + */ + private List getTerminalStatuses() { + return getTerminalStatuses(false); + } + + /** + * Gets the list of terminal status, possibly refreshing before returning + * + * @param refresh true to refresh statuses, false to use cached statuses + * @return a non-null, possibly empty list of terminal statuses + */ + private List getTerminalStatuses(final boolean refresh) { + final List cachedTerms = stateTerminals.get(); + if (!cachedTerms.isEmpty() && !refresh) { + return cachedTerms; + } + + final List terms = new ArrayList<>(); + try { + terms.addAll(getTerminalStatus().asArray(CurrentExternalTerminalsStatus_1_0.class)); + } catch (final IOException e) { + logger.debug("Error getting terminal statuses {}", e.getMessage()); + } + + // If no outputs, create our dummy 'main' output + if (!terms.stream().anyMatch(t -> t.isOutput())) { + terms.add(new CurrentExternalTerminalsStatus_1_0(MAINOUTPUT, MAINTITLE)); + } + + stateTerminals.set(terms); + return terms; + } + + /** + * Updates all content channels to undefined + */ + private void notifyContentListResult() { + // Set everything to undefined except for uri, index, childcount and selected + stateChanged(CN_ALBUMNAME, UnDefType.UNDEF); + stateChanged(CN_APPLICATIONNAME, UnDefType.UNDEF); + stateChanged(CN_ARTIST, UnDefType.UNDEF); + stateChanged(CN_AUDIOCHANNEL, UnDefType.UNDEF); + stateChanged(CN_AUDIOCODEC, UnDefType.UNDEF); + stateChanged(CN_AUDIOFREQUENCY, UnDefType.UNDEF); + stateChanged(CN_BIVLSERVICEID, UnDefType.UNDEF); + stateChanged(CN_BIVLASSETID, UnDefType.UNDEF); + stateChanged(CN_BIVLPROVIDER, UnDefType.UNDEF); + stateChanged(CN_BROADCASTFREQ, UnDefType.UNDEF); + stateChanged(CN_BROADCASTFREQBAND, UnDefType.UNDEF); + stateChanged(CN_CHANNELNAME, UnDefType.UNDEF); + stateChanged(CN_CHANNELSURFINGVISIBILITY, UnDefType.UNDEF); + stateChanged(CN_CHAPTERCOUNT, UnDefType.UNDEF); + stateChanged(CN_CHAPTERINDEX, UnDefType.UNDEF); + stateChanged(CN_CLIPCOUNT, UnDefType.UNDEF); + stateChanged(CN_CONTENTKIND, UnDefType.UNDEF); + stateChanged(CN_CONTENTTYPE, UnDefType.UNDEF); + stateChanged(CN_CREATEDTIME, UnDefType.UNDEF); + stateChanged(CN_DABCOMPONENTLABEL, UnDefType.UNDEF); + stateChanged(CN_DABDYNAMICLABEL, UnDefType.UNDEF); + stateChanged(CN_DABENSEMBLELABEL, UnDefType.UNDEF); + stateChanged(CN_DABSERVICELABEL, UnDefType.UNDEF); + stateChanged(CN_DESCRIPTION, UnDefType.UNDEF); + stateChanged(CN_DIRECTREMOTENUM, UnDefType.UNDEF); + stateChanged(CN_DISPNUM, UnDefType.UNDEF); + stateChanged(CN_DURATIONMSEC, UnDefType.UNDEF); + stateChanged(CN_DURATIONSEC, UnDefType.UNDEF); + stateChanged(CN_EPGVISIBILITY, UnDefType.UNDEF); + stateChanged(CN_EVENTID, UnDefType.UNDEF); + stateChanged(CN_FILENO, UnDefType.UNDEF); + stateChanged(CN_FILESIZEBYTE, UnDefType.UNDEF); + stateChanged(CN_FOLDERNO, UnDefType.UNDEF); + stateChanged(CN_GENRE, UnDefType.UNDEF); + stateChanged(CN_GLOBALPLAYBACKCOUNT, UnDefType.UNDEF); + stateChanged(CN_HASRESUME, UnDefType.UNDEF); + stateChanged(CN_IS3D, UnDefType.UNDEF); + stateChanged(CN_IS4K, UnDefType.UNDEF); + stateChanged(CN_ISALREADYPLAYED, UnDefType.UNDEF); + stateChanged(CN_ISAUTODELETE, UnDefType.UNDEF); + stateChanged(CN_ISBROWSABLE, UnDefType.UNDEF); + stateChanged(CN_ISNEW, UnDefType.UNDEF); + stateChanged(CN_ISPLAYABLE, UnDefType.UNDEF); + stateChanged(CN_ISPLAYLIST, UnDefType.UNDEF); + stateChanged(CN_ISPROTECTED, UnDefType.UNDEF); + stateChanged(CN_ISSOUNDPHOTO, UnDefType.UNDEF); + stateChanged(CN_MEDIATYPE, UnDefType.UNDEF); + stateChanged(CN_ORIGINALDISPNUM, UnDefType.UNDEF); + stateChanged(CN_OUTPUT, UnDefType.UNDEF); + stateChanged(CN_PARENTALCOUNTRY, UnDefType.UNDEF); + stateChanged(CN_PARENTALRATING, UnDefType.UNDEF); + stateChanged(CN_PARENTALSYSTEM, UnDefType.UNDEF); + stateChanged(CN_PARENTINDEX, UnDefType.UNDEF); + stateChanged(CN_PATH, UnDefType.UNDEF); + stateChanged(CN_PLAYLISTNAME, UnDefType.UNDEF); + stateChanged(CN_PODCASTNAME, UnDefType.UNDEF); + stateChanged(CN_PRODUCTID, UnDefType.UNDEF); + stateChanged(CN_PROGRAMMEDIATYPE, UnDefType.UNDEF); + stateChanged(CN_PROGRAMNUM, UnDefType.UNDEF); + stateChanged(CN_PROGRAMSERVICETYPE, UnDefType.UNDEF); + stateChanged(CN_PROGRAMTITLE, UnDefType.UNDEF); + stateChanged(CN_REMOTEPLAYTYPE, UnDefType.UNDEF); + stateChanged(CN_REPEATTYPE, UnDefType.UNDEF); + stateChanged(CN_SERVICE, UnDefType.UNDEF); + stateChanged(CN_SIZEMB, UnDefType.UNDEF); + stateChanged(CN_SOURCE, UnDefType.UNDEF); + stateChanged(CN_SOURCELABEL, UnDefType.UNDEF); + stateChanged(CN_STATE, UnDefType.UNDEF); + stateChanged(CN_STATESUPPLEMENT, UnDefType.UNDEF); + stateChanged(CN_STARTDATETIME, UnDefType.UNDEF); + stateChanged(CN_STORAGEURI, UnDefType.UNDEF); + stateChanged(CN_SUBTITLELANGUAGE, UnDefType.UNDEF); + stateChanged(CN_SUBTITLETITLE, UnDefType.UNDEF); + stateChanged(CN_SYNCCONTENTPRIORITY, UnDefType.UNDEF); + stateChanged(CN_TITLE, UnDefType.UNDEF); + stateChanged(CN_TOTALCOUNT, UnDefType.UNDEF); + stateChanged(CN_TRIPLETSTR, UnDefType.UNDEF); + stateChanged(CN_USERCONTENTFLAG, UnDefType.UNDEF); + stateChanged(CN_VIDEOCODEC, UnDefType.UNDEF); + stateChanged(CN_VISIBILITY, UnDefType.UNDEF); + } + + /** + * Updates the content channels from the 1.0 result + * + * @param clr the non-null content list result + */ + private void notifyContentListResult(final ContentListResult_1_0 clr) { + Objects.requireNonNull(clr, "clr cannot be null"); + + stateChanged(CN_CHANNELNAME, SonyUtil.newStringType(clr.getChannelName())); + + stateChanged(CN_DIRECTREMOTENUM, SonyUtil.newDecimalType(clr.getDirectRemoteNum())); + stateChanged(CN_DISPNUM, SonyUtil.newStringType(clr.getDispNum())); + stateChanged(CN_DURATIONSEC, SonyUtil.newQuantityType(clr.getDurationSec(), Units.SECOND)); + stateChanged(CN_FILESIZEBYTE, SonyUtil.newQuantityType(clr.getFileSizeByte(), Units.BYTE)); + stateChanged(CN_ISALREADYPLAYED, + SonyUtil.newStringType(Boolean.toString(clr.isAlreadyPlayed() != null && clr.isAlreadyPlayed()))); + stateChanged(CN_ISPROTECTED, + SonyUtil.newStringType(Boolean.toString(clr.isProtected() != null && clr.isProtected()))); + stateChanged(CN_ORIGINALDISPNUM, SonyUtil.newStringType(clr.getOriginalDispNum())); + + stateChanged(CN_PROGRAMMEDIATYPE, SonyUtil.newStringType(clr.getProgramMediaType())); + stateChanged(CN_PROGRAMNUM, SonyUtil.newDecimalType(clr.getProgramNum())); + + stateChanged(CN_STARTDATETIME, SonyUtil.newStringType(clr.getStartDateTime())); + + stateChanged(CN_TITLE, SonyUtil.newStringType(clr.getTitle())); + stateChanged(CN_TRIPLETSTR, SonyUtil.newStringType(clr.getTripletStr())); + } + + /** + * Updates the content channels from the 1.2 result + * + * @param clr the non-null content list result + */ + private void notifyContentListResult(final ContentListResult_1_2 clr) { + Objects.requireNonNull(clr, "clr cannot be null"); + + stateChanged(CN_AUDIOCHANNEL, SonyUtil.newStringType(SonyUtil.join(",", clr.getAudioChannel()))); + stateChanged(CN_AUDIOCODEC, SonyUtil.newStringType(SonyUtil.join(",", clr.getAudioCodec()))); + stateChanged(CN_AUDIOFREQUENCY, SonyUtil.newStringType(SonyUtil.join(",", clr.getAudioFrequency()))); + + stateChanged(CN_CHANNELNAME, SonyUtil.newStringType(clr.getChannelName())); + + stateChanged(CN_CHANNELSURFINGVISIBILITY, SonyUtil.newStringType(clr.getChannelSurfingVisibility())); + stateChanged(CN_EPGVISIBILITY, SonyUtil.newStringType(clr.getEpgVisibility())); + stateChanged(CN_VISIBILITY, SonyUtil.newStringType(clr.getVisibility())); + + stateChanged(CN_CHAPTERCOUNT, SonyUtil.newDecimalType(clr.getChapterCount())); + stateChanged(CN_CONTENTTYPE, SonyUtil.newStringType(clr.getContentType())); + stateChanged(CN_CREATEDTIME, SonyUtil.newStringType(clr.getCreatedTime())); + + stateChanged(CN_DIRECTREMOTENUM, SonyUtil.newDecimalType(clr.getDirectRemoteNum())); + stateChanged(CN_DISPNUM, SonyUtil.newStringType(clr.getDispNum())); + stateChanged(CN_DURATIONSEC, SonyUtil.newQuantityType(clr.getDurationSec(), Units.SECOND)); + stateChanged(CN_FILESIZEBYTE, SonyUtil.newQuantityType(clr.getFileSizeByte(), Units.BYTE)); + stateChanged(CN_ISALREADYPLAYED, + SonyUtil.newStringType(Boolean.toString(clr.isAlreadyPlayed() != null && clr.isAlreadyPlayed()))); + stateChanged(CN_ISPROTECTED, + SonyUtil.newStringType(Boolean.toString(clr.isProtected() != null && clr.isProtected()))); + stateChanged(CN_ORIGINALDISPNUM, SonyUtil.newStringType(clr.getOriginalDispNum())); + + stateChanged(CN_PARENTALCOUNTRY, SonyUtil.newStringType(SonyUtil.join(",", clr.getParentalCountry()))); + stateChanged(CN_PARENTALRATING, SonyUtil.newStringType(SonyUtil.join(",", clr.getParentalRating()))); + stateChanged(CN_PARENTALSYSTEM, SonyUtil.newStringType(SonyUtil.join(",", clr.getParentalSystem()))); + + stateChanged(CN_PRODUCTID, SonyUtil.newStringType(clr.getProductID())); + stateChanged(CN_PROGRAMMEDIATYPE, SonyUtil.newStringType(clr.getProgramMediaType())); + stateChanged(CN_PROGRAMNUM, SonyUtil.newDecimalType(clr.getProgramNum())); + stateChanged(CN_SIZEMB, SonyUtil.newQuantityType(clr.getSizeMB(), MetricPrefix.MEGA(Units.BYTE))); + + stateChanged(CN_STARTDATETIME, SonyUtil.newStringType(clr.getStartDateTime())); + stateChanged(CN_STORAGEURI, SonyUtil.newStringType(clr.getStorageUri())); + + stateChanged(CN_SUBTITLELANGUAGE, SonyUtil.newStringType(SonyUtil.join(",", clr.getSubtitleLanguage()))); + stateChanged(CN_SUBTITLETITLE, SonyUtil.newStringType(SonyUtil.join(",", clr.getSubtitleTitle()))); + + stateChanged(CN_TITLE, SonyUtil.newStringType(clr.getTitle())); + stateChanged(CN_TRIPLETSTR, SonyUtil.newStringType(clr.getTripletStr())); + stateChanged(CN_USERCONTENTFLAG, SonyUtil.newBooleanType(clr.isUserContentFlag())); + + stateChanged(CN_VIDEOCODEC, SonyUtil.newStringType(clr.getVideoCodec())); + } + + /** + * Updates the content channels from the 1.4 result + * + * @param clr the non-null content list result + */ + private void notifyContentListResult(final ContentListResult_1_4 clr) { + Objects.requireNonNull(clr, "clr cannot be null"); + + stateChanged(CN_ALBUMNAME, SonyUtil.newStringType(clr.getAlbumName())); + stateChanged(CN_ARTIST, SonyUtil.newStringType(clr.getArtist())); + + final AudioInfo[] audioInfo = clr.getAudioInfo(); + if (audioInfo != null) { + stateChanged(CN_AUDIOCHANNEL, SonyUtil.newStringType( + Arrays.stream(audioInfo).map(ai -> ai.getChannel()).collect(Collectors.joining(",")))); + stateChanged(CN_AUDIOCODEC, SonyUtil + .newStringType(Arrays.stream(audioInfo).map(ai -> ai.getCodec()).collect(Collectors.joining(",")))); + stateChanged(CN_AUDIOFREQUENCY, SonyUtil.newStringType( + Arrays.stream(audioInfo).map(ai -> ai.getFrequency()).collect(Collectors.joining(",")))); + } + + stateChanged(CN_BROADCASTFREQ, SonyUtil.newQuantityType(clr.getBroadcastFreq(), Units.HERTZ)); + stateChanged(CN_BROADCASTFREQBAND, SonyUtil.newStringType(clr.getBroadcastFreqBand())); + stateChanged(CN_CHANNELNAME, SonyUtil.newStringType(clr.getChannelName())); + + stateChanged(CN_CHANNELSURFINGVISIBILITY, SonyUtil.newStringType(clr.getChannelSurfingVisibility())); + stateChanged(CN_EPGVISIBILITY, SonyUtil.newStringType(clr.getEpgVisibility())); + stateChanged(CN_VISIBILITY, SonyUtil.newStringType(clr.getVisibility())); + + stateChanged(CN_CHAPTERCOUNT, SonyUtil.newDecimalType(clr.getChapterCount())); + stateChanged(CN_CONTENTKIND, SonyUtil.newStringType(clr.getContentKind())); + stateChanged(CN_CONTENTTYPE, SonyUtil.newStringType(clr.getContentType())); + stateChanged(CN_CREATEDTIME, SonyUtil.newStringType(clr.getCreatedTime())); + + stateChanged(CN_DIRECTREMOTENUM, SonyUtil.newDecimalType(clr.getDirectRemoteNum())); + stateChanged(CN_DISPNUM, SonyUtil.newStringType(clr.getDispNum())); + stateChanged(CN_DURATIONMSEC, + SonyUtil.newQuantityType(clr.getDurationMSec(), MetricPrefix.MILLI(Units.SECOND))); + stateChanged(CN_FILENO, SonyUtil.newStringType(clr.getFileNo())); + stateChanged(CN_FILESIZEBYTE, SonyUtil.newQuantityType(clr.getFileSizeByte(), Units.BYTE)); + stateChanged(CN_FOLDERNO, SonyUtil.newStringType(clr.getFolderNo())); + stateChanged(CN_GENRE, SonyUtil.newStringType(clr.getGenre())); + stateChanged(CN_IS3D, SonyUtil.newStringType(clr.is3D())); + stateChanged(CN_ISALREADYPLAYED, SonyUtil.newStringType(clr.isAlreadyPlayed())); + stateChanged(CN_ISBROWSABLE, SonyUtil.newStringType(clr.isBrowsable())); + stateChanged(CN_ISPLAYABLE, SonyUtil.newStringType(clr.isPlayable())); + stateChanged(CN_ISPROTECTED, SonyUtil.newStringType(clr.isProtected())); + stateChanged(CN_ORIGINALDISPNUM, SonyUtil.newStringType(clr.getOriginalDispNum())); + + final ParentalInfo[] pis = clr.getParentalInfo(); + if (pis != null) { + stateChanged(CN_PARENTALCOUNTRY, SonyUtil + .newStringType(Arrays.stream(pis).map(pi -> pi.getCountry()).collect(Collectors.joining(",")))); + stateChanged(CN_PARENTALRATING, SonyUtil + .newStringType(Arrays.stream(pis).map(pi -> pi.getRating()).collect(Collectors.joining(",")))); + stateChanged(CN_PARENTALSYSTEM, SonyUtil + .newStringType(Arrays.stream(pis).map(pi -> pi.getSystem()).collect(Collectors.joining(",")))); + } + stateChanged(CN_PARENTINDEX, SonyUtil.newDecimalType(clr.getParentIndex())); + stateChanged(CN_PATH, SonyUtil.newStringType(clr.getPath())); + stateChanged(CN_PLAYLISTNAME, SonyUtil.newStringType(clr.getPlaylistName())); + stateChanged(CN_PODCASTNAME, SonyUtil.newStringType(clr.getPodcastName())); + stateChanged(CN_PRODUCTID, SonyUtil.newStringType(clr.getProductID())); + stateChanged(CN_PROGRAMMEDIATYPE, SonyUtil.newStringType(clr.getProgramMediaType())); + stateChanged(CN_PROGRAMNUM, SonyUtil.newDecimalType(clr.getProgramNum())); + stateChanged(CN_REMOTEPLAYTYPE, SonyUtil.newStringType(clr.getRemotePlayType())); + stateChanged(CN_SIZEMB, SonyUtil.newQuantityType(clr.getSizeMB(), MetricPrefix.MEGA(Units.BYTE))); + + stateChanged(CN_STARTDATETIME, SonyUtil.newStringType(clr.getStartDateTime())); + stateChanged(CN_STORAGEURI, SonyUtil.newStringType(clr.getStorageUri())); + + final SubtitleInfo[] subInfos = clr.getSubtitleInfo(); + if (subInfos != null) { + stateChanged(CN_SUBTITLELANGUAGE, SonyUtil.newStringType( + Arrays.stream(subInfos).map(subi -> subi.getLangauge()).collect(Collectors.joining(",")))); + stateChanged(CN_SUBTITLETITLE, SonyUtil.newStringType( + Arrays.stream(subInfos).map(subi -> subi.getTitle()).collect(Collectors.joining(",")))); + } + stateChanged(CN_TITLE, SonyUtil.newStringType(clr.getTitle())); + stateChanged(CN_TRIPLETSTR, SonyUtil.newStringType(clr.getTripletStr())); + stateChanged(CN_USERCONTENTFLAG, SonyUtil.newBooleanType(clr.getUserContentFlag())); + + final VideoInfo vis = clr.getVideoInfo(); + if (vis != null) { + stateChanged(CN_VIDEOCODEC, SonyUtil.newStringType(vis.getCodec())); + } + } + + /** + * Updates the content channels from the 1.5 result + * + * @param clr the non-null content list result + */ + private void notifyContentListResult(final ContentListResult_1_5 clr) { + Objects.requireNonNull(clr, "clr cannot be null"); + stateChanged(CN_ALBUMNAME, SonyUtil.newStringType(clr.getAlbumName())); + stateChanged(CN_APPLICATIONNAME, SonyUtil.newStringType(clr.getApplicationName())); + stateChanged(CN_ARTIST, SonyUtil.newStringType(clr.getArtist())); + + final AudioInfo[] audioInfo = clr.getAudioInfo(); + if (audioInfo != null) { + stateChanged(CN_AUDIOCHANNEL, SonyUtil.newStringType( + Arrays.stream(audioInfo).map(ai -> ai.getChannel()).collect(Collectors.joining(",")))); + stateChanged(CN_AUDIOCODEC, SonyUtil + .newStringType(Arrays.stream(audioInfo).map(ai -> ai.getCodec()).collect(Collectors.joining(",")))); + stateChanged(CN_AUDIOFREQUENCY, SonyUtil.newStringType( + Arrays.stream(audioInfo).map(ai -> ai.getFrequency()).collect(Collectors.joining(",")))); + } + + final BivlInfo bivlInfo = clr.getBivlInfo(); + if (bivlInfo != null) { + stateChanged(CN_BIVLSERVICEID, SonyUtil.newStringType(bivlInfo.getServiceId())); + stateChanged(CN_BIVLASSETID, SonyUtil.newStringType(bivlInfo.getAssetId())); + stateChanged(CN_BIVLPROVIDER, SonyUtil.newStringType(bivlInfo.getProvider())); + } + + final BroadcastFreq bf = clr.getBroadcastFreq(); + if (bf != null) { + stateChanged(CN_BROADCASTFREQ, SonyUtil.newQuantityType(bf.getFrequency(), Units.HERTZ)); + stateChanged(CN_BROADCASTFREQBAND, SonyUtil.newStringType(bf.getBand())); + } + stateChanged(CN_CHANNELNAME, SonyUtil.newStringType(clr.getChannelName())); + + final Visibility visibility = clr.getVisibility(); + if (visibility != null) { + stateChanged(CN_CHANNELSURFINGVISIBILITY, SonyUtil.newStringType(visibility.getChannelSurfingVisibility())); + stateChanged(CN_EPGVISIBILITY, SonyUtil.newStringType(visibility.getEpgVisibility())); + stateChanged(CN_VISIBILITY, SonyUtil.newStringType(visibility.getVisibility())); + } + + stateChanged(CN_CHAPTERCOUNT, SonyUtil.newDecimalType(clr.getChapterCount())); + stateChanged(CN_CHAPTERINDEX, SonyUtil.newDecimalType(clr.getChapterIndex())); + stateChanged(CN_CLIPCOUNT, SonyUtil.newDecimalType(clr.getClipCount())); + stateChanged(CN_CONTENTKIND, SonyUtil.newStringType(clr.getContentKind())); + stateChanged(CN_CONTENTTYPE, SonyUtil.newStringType(clr.getContentType())); + stateChanged(CN_CREATEDTIME, SonyUtil.newStringType(clr.getCreatedTime())); + + final DabInfo dab = clr.getDabInfo(); + if (dab != null) { + stateChanged(CN_DABCOMPONENTLABEL, SonyUtil.newStringType(dab.getComponentLabel())); + stateChanged(CN_DABDYNAMICLABEL, SonyUtil.newStringType(dab.getDynamicLabel())); + stateChanged(CN_DABENSEMBLELABEL, SonyUtil.newStringType(dab.getEnsembleLabel())); + stateChanged(CN_DABSERVICELABEL, SonyUtil.newStringType(dab.getServiceLabel())); + } + + // TODO: find out description format + // final Description desc = clr.getDescription(); + // if (desc != null) { + // stateChanged(CN_DESCRIPTION, SonyUtil.newStringType(desc.)); + // } + stateChanged(CN_DIRECTREMOTENUM, SonyUtil.newDecimalType(clr.getDirectRemoteNum())); + stateChanged(CN_DISPNUM, SonyUtil.newStringType(clr.getDispNum())); + + final Duration duration = clr.getDuration(); + if (duration != null) { + stateChanged(CN_DURATIONMSEC, + SonyUtil.newQuantityType(duration.getMillseconds(), MetricPrefix.MILLI(Units.SECOND))); + stateChanged(CN_DURATIONSEC, SonyUtil.newQuantityType(duration.getSeconds(), Units.SECOND)); + } + + stateChanged(CN_EVENTID, SonyUtil.newStringType(clr.getEventId())); + stateChanged(CN_FILENO, SonyUtil.newStringType(clr.getFileNo())); + stateChanged(CN_FILESIZEBYTE, SonyUtil.newQuantityType(clr.getFileSizeByte(), Units.BYTE)); + stateChanged(CN_FOLDERNO, SonyUtil.newStringType(clr.getFolderNo())); + stateChanged(CN_GENRE, SonyUtil.newStringType(clr.getGenre())); + stateChanged(CN_GLOBALPLAYBACKCOUNT, SonyUtil.newDecimalType(clr.getGlobalPlaybackCount())); + stateChanged(CN_HASRESUME, SonyUtil.newStringType(clr.getHasResume())); + stateChanged(CN_IS3D, SonyUtil.newStringType(clr.is3D())); + stateChanged(CN_IS4K, SonyUtil.newStringType(clr.is4K())); + stateChanged(CN_ISALREADYPLAYED, SonyUtil.newStringType(clr.isAlreadyPlayed())); + stateChanged(CN_ISAUTODELETE, SonyUtil.newStringType(clr.isAutoDelete())); + stateChanged(CN_ISBROWSABLE, SonyUtil.newStringType(clr.isBrowsable())); + stateChanged(CN_ISNEW, SonyUtil.newStringType(clr.isNew())); + stateChanged(CN_ISPLAYABLE, SonyUtil.newStringType(clr.isPlayable())); + stateChanged(CN_ISPLAYLIST, SonyUtil.newStringType(clr.isPlaylist())); + stateChanged(CN_ISPROTECTED, SonyUtil.newStringType(clr.isProtected())); + stateChanged(CN_ISSOUNDPHOTO, SonyUtil.newStringType(clr.isSoundPhoto())); + stateChanged(CN_MEDIATYPE, SonyUtil.newStringType(clr.getMediaType())); + stateChanged(CN_ORIGINALDISPNUM, SonyUtil.newStringType(clr.getOriginalDispNum())); + stateChanged(CN_OUTPUT, SonyUtil.newStringType(clr.getOutput())); + + final ParentalInfo[] pis = clr.getParentalInfo(); + if (pis != null) { + stateChanged(CN_PARENTALCOUNTRY, SonyUtil + .newStringType(Arrays.stream(pis).map(pi -> pi.getCountry()).collect(Collectors.joining(",")))); + stateChanged(CN_PARENTALRATING, SonyUtil + .newStringType(Arrays.stream(pis).map(pi -> pi.getRating()).collect(Collectors.joining(",")))); + stateChanged(CN_PARENTALSYSTEM, SonyUtil + .newStringType(Arrays.stream(pis).map(pi -> pi.getSystem()).collect(Collectors.joining(",")))); + } + stateChanged(CN_PARENTINDEX, SonyUtil.newDecimalType(clr.getParentIndex())); + stateChanged(CN_PLAYLISTNAME, SonyUtil.newStringType(clr.getPlaylistName())); + stateChanged(CN_PODCASTNAME, SonyUtil.newStringType(clr.getPodcastName())); + stateChanged(CN_PRODUCTID, SonyUtil.newStringType(clr.getProductID())); + stateChanged(CN_PROGRAMMEDIATYPE, SonyUtil.newStringType(clr.getProgramMediaType())); + stateChanged(CN_PROGRAMNUM, SonyUtil.newDecimalType(clr.getProgramNum())); + stateChanged(CN_PROGRAMSERVICETYPE, SonyUtil.newStringType(clr.getProgramServiceType())); + stateChanged(CN_PROGRAMTITLE, SonyUtil.newStringType(clr.getProgramTitle())); + stateChanged(CN_REMOTEPLAYTYPE, SonyUtil.newStringType(clr.getRemotePlayType())); + stateChanged(CN_REPEATTYPE, SonyUtil.newStringType(clr.getRepeatType())); + stateChanged(CN_SERVICE, SonyUtil.newStringType(clr.getService())); + stateChanged(CN_SIZEMB, SonyUtil.newQuantityType(clr.getSizeMB(), MetricPrefix.MEGA(Units.BYTE))); + stateChanged(CN_SOURCE, SonyUtil.newStringType(clr.getSource())); + stateChanged(CN_SOURCELABEL, SonyUtil.newStringType(clr.getSourceLabel())); + + final StateInfo si = clr.getStateInfo(); + if (si != null) { + stateChanged(CN_STATE, SonyUtil.newStringType(si.getState())); + stateChanged(CN_STATESUPPLEMENT, SonyUtil.newStringType(si.getSupplement())); + } + stateChanged(CN_STARTDATETIME, SonyUtil.newStringType(clr.getStartDateTime())); + stateChanged(CN_STORAGEURI, SonyUtil.newStringType(clr.getStorageUri())); + + final SubtitleInfo[] subInfos = clr.getSubtitleInfo(); + if (subInfos != null) { + stateChanged(CN_SUBTITLELANGUAGE, SonyUtil.newStringType( + Arrays.stream(subInfos).map(subi -> subi.getLangauge()).collect(Collectors.joining(",")))); + stateChanged(CN_SUBTITLETITLE, SonyUtil.newStringType( + Arrays.stream(subInfos).map(subi -> subi.getTitle()).collect(Collectors.joining(",")))); + } + stateChanged(CN_SYNCCONTENTPRIORITY, SonyUtil.newStringType(clr.getSyncContentPriority())); + stateChanged(CN_TITLE, SonyUtil.newStringType(clr.getTitle())); + stateChanged(CN_TOTALCOUNT, SonyUtil.newDecimalType(clr.getTotalCount())); + stateChanged(CN_TRIPLETSTR, SonyUtil.newStringType(clr.getTripletStr())); + stateChanged(CN_USERCONTENTFLAG, SonyUtil.newBooleanType(clr.getUserContentFlag())); + + final VideoInfo[] vis = clr.getVideoInfo(); + if (vis != null) { + stateChanged(CN_VIDEOCODEC, SonyUtil + .newStringType(Arrays.stream(vis).map(vi -> vi.getCodec()).collect(Collectors.joining(",")))); + } + } + + /** + * Handle the notification of a terminal status (and update all related channels) + * + * @param term a non-null terminal status + */ + private void notifyCurrentTerminalStatus(final CurrentExternalTerminalsStatus_1_0 term) { + Objects.requireNonNull(term, "term cannot be null"); + final String termUri = term.getUri(); + for (final ScalarWebChannel chnl : getChannelTracker() + .getLinkedChannelsForCategory(TERM_URI, TERM_TITLE, TERM_CONNECTION, TERM_LABEL, TERM_ICON, TERM_ACTIVE) + .stream().toArray(ScalarWebChannel[]::new)) { + if (termUri != null && termUri.equalsIgnoreCase(chnl.getPathPart(0))) { + notifyCurrentTerminalStatus(chnl, term); + } + } + } + + /** + * Notify the notification of a terminal status for a specific channel + * + * @param channel a non-null channel + */ + private void notifyCurrentTerminalStatus(final ScalarWebChannel channel) { + Objects.requireNonNull(channel, "channel cannot be null"); + try { + for (final CurrentExternalTerminalsStatus_1_0 term : execute( + ScalarWebMethod.GETCURRENTEXTERNALTERMINALSSTATUS) + .asArray(CurrentExternalTerminalsStatus_1_0.class)) { + final String termUri = term.getUri(); + if (termUri != null && termUri.equalsIgnoreCase(channel.getPathPart(0))) { + notifyCurrentTerminalStatus(channel, term); + } + } + } catch (final IOException e) { + logger.debug("Error notify current terminal status {}", e.getMessage()); + } + } + + /** + * Handl notification of a terminal status for a specific channel + * + * @param channel a non-null channel + * @param cets a non-null terminal status + */ + private void notifyCurrentTerminalStatus(final ScalarWebChannel channel, + final CurrentExternalTerminalsStatus_1_0 cets) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(cets, "cets cannot be null"); + + final String id = channel.getId(); + stateChanged(TERM_TITLE, id, SonyUtil.newStringType(cets.getTitle())); + stateChanged(TERM_CONNECTION, id, SonyUtil.newStringType(cets.getConnection())); + stateChanged(TERM_LABEL, id, SonyUtil.newStringType(cets.getLabel())); + stateChanged(TERM_ACTIVE, id, SonyUtil.newStringType(cets.getActive())); + + final String iconUrl = cets.getIconUrl(); + if (iconUrl == null || iconUrl.isEmpty()) { + stateChanged(TERM_ICON, id, UnDefType.UNDEF); + } else { + try (SonyHttpTransport transport = SonyTransportFactory.createHttpTransport( + getService().getTransport().getBaseUri().toString(), getContext().getClientBuilder())) { + final RawType rawType = NetUtil.getRawType(transport, iconUrl); + stateChanged(TERM_ICON, id, rawType == null ? UnDefType.UNDEF : rawType); + } catch (URISyntaxException e) { + logger.debug("Exception occurred getting application icon: {}", e.getMessage()); + } + } + } + + /** + * Handle notification of an input status (v1.0) for a specific channel + * + * @param channel a non-null channel + * @param status a non-null status + */ + private void notifyInputStatus(final ScalarWebChannel channel, final CurrentExternalInputsStatus_1_0 status) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(status, "status cannot be null"); + + final String id = channel.getId(); + stateChanged(IN_TITLE, id, SonyUtil.newStringType(status.getTitle())); + stateChanged(IN_CONNECTION, id, status.isConnection() ? OnOffType.ON : OnOffType.OFF); + stateChanged(IN_LABEL, id, SonyUtil.newStringType(status.getLabel())); + stateChanged(IN_ICON, id, SonyUtil.newStringType(status.getIcon())); + } + + /** + * Handle notification of an input status (v1.1) for a specific channel + * + * @param channel a non-null channel + * @param status a non-null status + */ + private void notifyInputStatus(final ScalarWebChannel channel, final CurrentExternalInputsStatus_1_1 status) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(status, "status cannot be null"); + + notifyInputStatus(channel, (CurrentExternalInputsStatus_1_0) status); + + final String id = channel.getId(); + stateChanged(IN_STATUS, id, SonyUtil.newStringType(status.getStatus())); + } + + /** + * Handle notification of an parental rating (v1.0) + * + * @param prs the non-null parental rating + */ + private void notifyParentalRating(final ParentalRatingSetting_1_0 prs) { + Objects.requireNonNull(prs, "prs cannot be null"); + stateChanged(PR_RATINGTYPEAGE, SonyUtil.newDecimalType(prs.getRatingTypeAge())); + stateChanged(PR_RATINGTYPESONY, SonyUtil.newStringType(prs.getRatingTypeSony())); + stateChanged(PR_RATINGCOUNTRY, SonyUtil.newStringType(prs.getRatingCountry())); + stateChanged(PR_RATINGCUSTOMTYPETV, SonyUtil.newStringType(SonyUtil.join("", prs.getRatingCustomTypeTV()))); + stateChanged(PR_RATINGCUSTOMTYPEMPAA, SonyUtil.newStringType(prs.getRatingCustomTypeMpaa())); + stateChanged(PR_RATINGCUSTOMTYPECAENGLISH, SonyUtil.newStringType(prs.getRatingCustomTypeCaEnglish())); + stateChanged(PR_RATINGCUSTOMTYPECAFRENCH, SonyUtil.newStringType(prs.getRatingCustomTypeCaFrench())); + stateChanged(PR_UNRATEDLOCK, prs.isUnratedLock() ? OnOffType.ON : OnOffType.OFF); + } + + /** + * Handle notification of an playing content (v1.0) for a specific output + * + * @param pci the non-null playing content info + * @param id the non-null, non-empty output + */ + private void notifyPlayingContentInfo(final PlayingContentInfoResult_1_0 pci, final String id) { + Objects.requireNonNull(pci, "pic cannot be null"); + SonyUtil.validateNotEmpty(id, "id cannot be null"); + stateChanged(PL_BIVLASSETID, id, SonyUtil.newStringType(pci.getBivlAssetId())); + stateChanged(PL_BIVLPROVIDER, id, SonyUtil.newStringType(pci.getBivlProvider())); + stateChanged(PL_BIVLSERVICEID, id, SonyUtil.newStringType(pci.getBivlServiceId())); + stateChanged(PL_DISPNUM, id, SonyUtil.newStringType(pci.getDispNum())); + stateChanged(PL_DURATIONSEC, id, SonyUtil.newQuantityType(pci.getDurationSec(), Units.SECOND)); + stateChanged(PL_MEDIATYPE, id, SonyUtil.newStringType(pci.getMediaType())); + stateChanged(PL_ORIGINALDISPNUM, id, SonyUtil.newStringType(pci.getOriginalDispNum())); + stateChanged(PL_PLAYSPEED, id, SonyUtil.newStringType(pci.getPlaySpeed())); + stateChanged(PL_PROGRAMNUM, id, SonyUtil.newDecimalType(pci.getProgramNum())); + stateChanged(PL_PROGRAMTITLE, id, SonyUtil.newStringType(pci.getProgramTitle())); + stateChanged(PL_SOURCE, id, SonyUtil.newStringType(pci.getSource())); + stateChanged(PL_STARTDATETIME, id, SonyUtil.newStringType(pci.getStartDateTime())); + stateChanged(PL_TITLE, id, SonyUtil.newStringType(pci.getTitle())); + stateChanged(PL_TRIPLETSTR, id, SonyUtil.newStringType(pci.getTripletStr())); + + final String pciSourceUri = pci.getSource(); + if (pciSourceUri != null && !pciSourceUri.isEmpty()) { + final String pciScheme = Source.getSchemePart(pciSourceUri); + final String pciSource = Source.getSourcePart(pciSourceUri); + + // only do the following for TV or RADIO schemes + if (pciScheme != null + && (Scheme.TV.equalsIgnoreCase(pciScheme) || Scheme.RADIO.equalsIgnoreCase(pciScheme))) { + + if (Scheme.TV.equalsIgnoreCase(pciScheme)) { + // cache TV uri for later use when switching to TV source without full uri + // complete pciSourceUri 'tv:dvbs' (not selectable via setPlayContent api call) + final String uri = pci.getUri(); + if (uri != null) { + lastPlayedTVUriBySource.compute(pciSourceUri, (k, v) -> v = uri); + // generic tv source 'tv:' (selectable via setPlayContent api call) + lastPlayedTVUriBySource.compute(Scheme.TV + ":", (k, v) -> v = uri); + } + } + + for (final Source src : getSources()) { + final String srcScheme = src.getSchemePart(); + final String srcSource = src.getSourcePart(); + + // if we have the same scheme... + if (pciSource != null && srcScheme != null && srcSource != null + && pciScheme.equalsIgnoreCase(srcScheme)) { + // set the value if the same source, otherwise move undef to it + stateChanged(PS_CHANNEL, srcSource, + srcSource.equalsIgnoreCase(pciSource) ? SonyUtil.newStringType(pci.getDispNum()) + : UnDefType.UNDEF); + } + } + } + } + + final String sourceUri = pci.getUri(); + if (sourceUri != null && !sourceUri.isEmpty()) { + // statePlaying.put(output, new PlayingState(sourceUri, preset)); + statePlaying.compute(id, (k, v) -> { + if (v == null) { + int preset = 1; + final Matcher m = Source.RADIOPATTERN.matcher(sourceUri); + if (m.matches() && m.groupCount() > 1) { + try { + preset = Integer.parseInt(m.group(2)); + } catch (final NumberFormatException e) { + logger.debug("Radio preset number is not a valid number: {}", sourceUri); + } + } + return new PlayingState(sourceUri, preset); + } else { + return new PlayingState(sourceUri, v.getPreset()); + } + }); + stateChanged(PL_URI, id, SonyUtil.newStringType(sourceUri)); + // stateChanged(PL_PRESET, id, SonyUtil.newDecimalType(preset)); + + stateChanged(TERM_SOURCE, id, SonyUtil.newStringType(getSourceFromUri(sourceUri))); + } + } + + /** + * Handle notification of an playing content (v1.2) for a specific output + * + * @param pci the non-null playing content info + * @param id the non-null, non-empty output + */ + private void notifyPlayingContentInfo(final PlayingContentInfoResult_1_2 pci, final String id) { + Objects.requireNonNull(pci, "pic cannot be null"); + SonyUtil.validateNotEmpty(id, "id cannot be null"); + notifyPlayingContentInfo((PlayingContentInfoResult_1_0) pci, id); + + stateChanged(PL_ALBUMNAME, id, SonyUtil.newStringType(pci.getAlbumName())); + stateChanged(PL_APPLICATIONNAME, id, SonyUtil.newStringType(pci.getApplicationName())); + stateChanged(PL_ARTIST, id, SonyUtil.newStringType(pci.getArtist())); + + final AudioInfo[] ais = pci.getAudioInfo(); + stateChanged(PL_AUDIOCHANNEL, id, SonyUtil.newStringType( + ais == null ? null : Arrays.stream(ais).map(a -> a.getChannel()).collect(Collectors.joining(",")))); + stateChanged(PL_AUDIOCODEC, id, SonyUtil.newStringType( + ais == null ? null : Arrays.stream(ais).map(a -> a.getCodec()).collect(Collectors.joining(",")))); + stateChanged(PL_AUDIOFREQUENCY, id, SonyUtil.newStringType( + ais == null ? null : Arrays.stream(ais).map(a -> a.getFrequency()).collect(Collectors.joining(",")))); + + stateChanged(PL_BROADCASTFREQ, id, SonyUtil.newQuantityType(pci.getBroadcastFreq(), Units.HERTZ)); + stateChanged(PL_BROADCASTFREQBAND, id, SonyUtil.newStringType(pci.getBroadcastFreqBand())); + stateChanged(PL_CHANNELNAME, id, SonyUtil.newStringType(pci.getChannelName())); + stateChanged(PL_CHAPTERCOUNT, id, SonyUtil.newDecimalType(pci.getChapterCount())); + stateChanged(PL_CHAPTERINDEX, id, SonyUtil.newDecimalType(pci.getChapterIndex())); + stateChanged(PL_CONTENTKIND, id, SonyUtil.newStringType(pci.getContentKind())); + + final DabInfo di = pci.getDabInfo(); + stateChanged(PL_DABCOMPONENTLABEL, id, SonyUtil.newStringType(di == null ? null : di.getComponentLabel())); + stateChanged(PL_DABDYNAMICLABEL, id, SonyUtil.newStringType(di == null ? null : di.getDynamicLabel())); + stateChanged(PL_DABENSEMBLELABEL, id, SonyUtil.newStringType(di == null ? null : di.getEnsembleLabel())); + stateChanged(PL_DABSERVICELABEL, id, SonyUtil.newStringType(di == null ? null : di.getServiceLabel())); + + stateChanged(PL_DURATIONMSEC, id, + SonyUtil.newQuantityType(pci.getDurationMsec(), MetricPrefix.MILLI(Units.SECOND))); + stateChanged(PL_FILENO, id, SonyUtil.newStringType(pci.getFileNo())); + stateChanged(PL_GENRE, id, SonyUtil.newStringType(pci.getGenre())); + stateChanged(PL_INDEX, id, SonyUtil.newDecimalType(pci.getIndex())); + stateChanged(PL_IS3D, id, SonyUtil.newStringType(pci.getIs3D())); + stateChanged(PL_OUTPUT, id, SonyUtil.newStringType(pci.getOutput())); + stateChanged(PL_PARENTINDEX, id, SonyUtil.newDecimalType(pci.getParentIndex())); + stateChanged(PL_PARENTURI, id, SonyUtil.newStringType(pci.getParentUri())); + stateChanged(PL_PATH, id, SonyUtil.newStringType(pci.getPath())); + stateChanged(PL_PLAYLISTNAME, id, SonyUtil.newStringType(pci.getPlaylistName())); + stateChanged(PL_PLAYSTEPSPEED, id, SonyUtil.newDecimalType(pci.getPlayStepSpeed())); + stateChanged(PL_PODCASTNAME, id, SonyUtil.newStringType(pci.getPodcastName())); + stateChanged(PL_POSITIONMSEC, id, + SonyUtil.newQuantityType(pci.getPositionMsec(), MetricPrefix.MILLI(Units.SECOND))); + stateChanged(PL_POSITIONSEC, id, SonyUtil.newQuantityType(pci.getPositionSec(), Units.SECOND)); + stateChanged(PL_REPEATTYPE, id, SonyUtil.newStringType(pci.getRepeatType())); + stateChanged(PL_SERVICE, id, SonyUtil.newStringType(pci.getService())); + stateChanged(PL_SOURCELABEL, id, SonyUtil.newStringType(pci.getSourceLabel())); + + final StateInfo si = pci.getStateInfo(); + stateChanged(PL_STATE, id, SonyUtil.newStringType(si == null ? null : si.getState())); + stateChanged(PL_STATESUPPLEMENT, id, SonyUtil.newStringType(si == null ? null : si.getSupplement())); + + stateChanged(PL_SUBTITLEINDEX, id, SonyUtil.newDecimalType(pci.getSubtitleIndex())); + stateChanged(PL_TOTALCOUNT, id, SonyUtil.newDecimalType(pci.getTotalCount())); + + final VideoInfo vi = pci.getVideoInfo(); + stateChanged(PL_VIDEOCODEC, id, SonyUtil.newStringType(vi == null ? null : vi.getCodec())); + } + + @Override + public void refreshChannel(final ScalarWebChannel channel) { + Objects.requireNonNull(channel, "channel cannot be null"); + + final String ctgy = channel.getCategory(); + if (SCHEMES.equalsIgnoreCase(ctgy)) { + refreshSchemes(); + } else if (SOURCES.equalsIgnoreCase(ctgy)) { + refreshSources(); + } else if (ctgy.startsWith(PARENTRATING)) { + refreshParentalRating(); + refreshParentalRating(); + } else if (ctgy.startsWith(PLAYING)) { + refreshPlayingContentInfo(); + } else if (ctgy.startsWith(INPUT)) { + refreshCurrentExternalInputStatus(Collections.singleton(channel)); + } else if (ctgy.startsWith(TERM)) { + notifyCurrentTerminalStatus(channel); + } else if (ctgy.startsWith(CONTENT)) { + refreshContent(); + } else if (BLUETOOTHSETTINGS.equalsIgnoreCase(ctgy)) { + refreshGeneralSettings(Collections.singleton(channel), ScalarWebMethod.GETBLUETOOTHSETTINGS); + } else if (PLAYBACKSETTINGS.equalsIgnoreCase(ctgy)) { + refreshGeneralSettings(Collections.singleton(channel), ScalarWebMethod.GETPLAYBACKMODESETTINGS); + } else if (PS_CHANNEL.equalsIgnoreCase(ctgy)) { + refreshPresetChannelStateDescription(Collections.singletonList(channel)); + } else { + logger.debug("Unknown refresh channel: {}", channel); + } + } + + /** + * Refresh content + */ + private void refreshContent() { + final ContentState state = stateContent.get(); + + if (SonyUtil.isEmpty(state.getParentUri())) { + notifyContentListResult(); + } else { + Count ct; + + try { + ct = execute(ScalarWebMethod.GETCONTENTCOUNT, version -> { + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0, ScalarWebMethod.V1_1, + ScalarWebMethod.V1_2)) { + return new ContentCount_1_0(state.getParentUri()); + } + return new ContentCount_1_3(state.getParentUri()); + }).as(Count.class); + + } catch (final IOException e) { + ct = new Count(-1); + } + + // update child count + stateChanged(CN_PARENTURI, SonyUtil.newStringType(state.getParentUri())); + stateChanged(CN_CHILDCOUNT, SonyUtil.newDecimalType(ct.getCount())); + + try { + final ScalarWebResult res = execute(ScalarWebMethod.GETCONTENTLIST, version -> { + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0, ScalarWebMethod.V1_1, + ScalarWebMethod.V1_2, ScalarWebMethod.V1_3)) { + return new ContentListRequest_1_0(state.getParentUri(), state.getIdx(), 1); + } + return new ContentListRequest_1_4(state.getParentUri(), state.getIdx(), 1); + }); + + String childUri = null; + Integer childIdx = null; + + // For USB - if you ask for 1, you'll always get two results + // 1. The actual index you asked for + // 2. A row describing the storage itself (idx = -1) + // so we need to filter for just our result + final String version = getVersion(ScalarWebMethod.GETCONTENTLIST); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0, ScalarWebMethod.V1_1)) { + for (final ContentListResult_1_0 clr : res.asArray(ContentListResult_1_0.class)) { + if (clr.getIndex() == state.getIdx()) { + notifyContentListResult(clr); + childUri = clr.getUri(); + childIdx = clr.getIndex(); + } + } + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_2, ScalarWebMethod.V1_3)) { + for (final ContentListResult_1_2 clr : res.asArray(ContentListResult_1_2.class)) { + if (clr.getIndex() == state.getIdx()) { + notifyContentListResult(clr); + childUri = clr.getUri(); + childIdx = clr.getIndex(); + } + } + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_4)) { + for (final ContentListResult_1_4 clr : res.asArray(ContentListResult_1_4.class)) { + if (clr.getIndex() == state.getIdx()) { + notifyContentListResult(clr); + childUri = clr.getUri(); + childIdx = clr.getIndex(); + } + } + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_5)) { + for (final ContentListResult_1_5 clr : res.asArray(ContentListResult_1_5.class)) { + if (clr.getIndex() == state.getIdx()) { + notifyContentListResult(clr); + childUri = clr.getUri(); + childIdx = clr.getIndex(); + } + } + } + + if (childIdx != null && childUri != null) { + final String finalUri = childUri; + final Integer finalIdx = childIdx; + stateContent.updateAndGet(cs -> new ContentState(cs.getParentUri(), finalUri, finalIdx)); + } + stateChanged(CN_URI, SonyUtil.newStringType(childUri)); + stateChanged(CN_INDEX, SonyUtil.newDecimalType(state.getIdx())); + + } catch (final IOException e) { + notifyContentListResult(); + } + } + } + + /** + * Refresh current external input status for a set of channels + * + * @param channels a non-null, possibly empty set of channels + */ + private void refreshCurrentExternalInputStatus(final Set channels) { + Objects.requireNonNull(channels, "channels cannot be null"); + if (getService().hasMethod(ScalarWebMethod.GETCURRENTEXTERNALINPUTSSTATUS)) { + try { + final ScalarWebResult result = getInputStatus(); + final String version = getService().getVersion(ScalarWebMethod.GETCURRENTEXTERNALINPUTSSTATUS); + + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + for (final CurrentExternalInputsStatus_1_0 inp : result + .asArray(CurrentExternalInputsStatus_1_0.class)) { + final String inpUri = inp.getUri(); + for (final ScalarWebChannel chnl : channels) { + if (inpUri != null && inpUri.equalsIgnoreCase(chnl.getPathPart(0))) { + notifyInputStatus(chnl, inp); + } + } + } + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_1)) { + for (final CurrentExternalInputsStatus_1_1 inp : result + .asArray(CurrentExternalInputsStatus_1_1.class)) { + final String inpUri = inp.getUri(); + for (final ScalarWebChannel chnl : channels) { + if (inpUri != null && inpUri.equalsIgnoreCase(chnl.getPathPart(0))) { + notifyInputStatus(chnl, inp); + } + } + } + } + } catch (final IOException e) { + logger.debug("Error refreshing current external input status {}", e.getMessage()); + } + } + } + + /** + * Refresh current external input status + */ + private void refreshCurrentExternalTerminalsStatus() { + if (getService().hasMethod(ScalarWebMethod.GETCURRENTEXTERNALTERMINALSSTATUS)) { + for (final CurrentExternalTerminalsStatus_1_0 term : getTerminalStatuses(true)) { + notifyCurrentTerminalStatus(term); + } + } + } + + /** + * Refresh the parental rating + */ + private void refreshParentalRating() { + try { + notifyParentalRating(getParentalRating().as(ParentalRatingSetting_1_0.class)); + } catch (final IOException e) { + logger.debug("Exception occurred retrieving the parental rating setting: {}", e.getMessage()); + } + } + + /** + * Refresh the playing content info + */ + private void refreshPlayingContentInfo() { + try { + final ScalarWebResult result = getPlayingContentInfo(); + final String version = getService().getVersion(ScalarWebMethod.GETPLAYINGCONTENTINFO); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0, ScalarWebMethod.V1_1)) { + for (final PlayingContentInfoResult_1_0 res : result.asArray(PlayingContentInfoResult_1_0.class)) { + notifyPlayingContentInfo(res, getIdForOutput(MAINOUTPUT)); + } + } else if (VersionUtilities.equals(version, ScalarWebMethod.V1_2)) { + for (final PlayingContentInfoResult_1_2 res : result.asArray(PlayingContentInfoResult_1_2.class)) { + final String output = res.getOutput(MAINOUTPUT); + notifyPlayingContentInfo(res, getIdForOutput(output)); + } + } + } catch (final IOException e) { + logger.debug("Error refreshing playing content info {}", e.getMessage()); + } + } + + /** + * Refresh the schemes + */ + private void refreshSchemes() { + final String schemes = getSchemes(true).stream().map(s -> s.getScheme()).collect(Collectors.joining(",")); + stateSources.clear(); // clear sources to reretrieve them since schemes changed + stateChanged(SCHEMES, SonyUtil.newStringType(schemes)); + } + + /** + * Refresh the sources + */ + private void refreshSources() { + final List sources = new ArrayList<>(); + for (final Source src : getSources(true)) { + final String source = src.getSource(); + if (source != null && !source.isEmpty()) { + sources.add(source); + } + } + + if (!sources.isEmpty()) { + stateChanged(SOURCES, SonyUtil.newStringType(String.join(",", sources))); + } + } + + @Override + public void refreshState(boolean initial) { + final ScalarWebChannelTracker tracker = getChannelTracker(); + + // always refresh these since they are used in lookups + refreshSchemes(); + refreshSources(); + + if (tracker.isCategoryLinked(ctgy -> ctgy.startsWith(PARENTRATING))) { + refreshParentalRating(); + } + + if (initial || !notificationHelper.isEnabled(ScalarWebEvent.NOTIFYPLAYINGCONTENTINFO)) { + if (tracker.isCategoryLinked( + ctgy -> ctgy.startsWith(PLAYING) || ctgy.startsWith(TERM) || ctgy.startsWith(PRESETS))) { + refreshPlayingContentInfo(); + } + } + + refreshCurrentExternalInputStatus(tracker.getLinkedChannelsForCategory(ctgy -> ctgy.startsWith(INPUT))); + + if (initial || !notificationHelper.isEnabled(ScalarWebEvent.NOTIFYEXTERNALTERMINALSTATUS)) { + refreshCurrentExternalTerminalsStatus(); + } + + if (tracker.isCategoryLinked(ctgy -> ctgy.startsWith(CONTENT))) { + refreshContent(); + } + + if (tracker.isCategoryLinked(BLUETOOTHSETTINGS)) { + refreshGeneralSettings(tracker.getLinkedChannelsForCategory(BLUETOOTHSETTINGS), + ScalarWebMethod.GETBLUETOOTHSETTINGS); + } + if (tracker.isCategoryLinked(PLAYBACKSETTINGS)) { + refreshGeneralSettings(tracker.getLinkedChannelsForCategory(PLAYBACKSETTINGS), + ScalarWebMethod.GETPLAYBACKMODESETTINGS); + } + + // Very heavy call - let's just make them restart binding when a preset changes if they + // want it to show up on a dynamic state for the UI + // if (tracker.isCategoryLinked(PS_CHANNEL)) { + // refreshPresetChannelStateDescription(tracker.getLinkedChannelsForCategory(PS_CHANNEL)); + // } + } + + @Override + public void setChannel(final ScalarWebChannel channel, final Command command) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + if (BLUETOOTHSETTINGS.equalsIgnoreCase(channel.getCategory())) { + setGeneralSetting(ScalarWebMethod.SETBLUETOOTHSETTINGS, channel, command); + } else if (PLAYBACKSETTINGS.equalsIgnoreCase(channel.getCategory())) { + setGeneralSetting(ScalarWebMethod.SETPLAYBACKMODESETTINGS, channel, command); + } else if (PL_CMD.equalsIgnoreCase(channel.getCategory())) { + final String uri = channel.getPathPart(0); + if (uri == null || uri.isEmpty()) { + logger.debug("{} command - channel has no uri: {}", PL_CMD, channel); + return; + } + if (command instanceof StringType) { + setPlayingCommand(uri, channel.getId(), command.toString()); + } else { + logger.debug("{} command not an StringType: {}", PL_CMD, command); + } + } else if (PS_CHANNEL.equalsIgnoreCase(channel.getCategory())) { + /* + * final String srcId = channel.getPathPart(0); + * if (srcId == null || srcId.isEmpty()) { + * logger.debug("{} command - channel has no srcId: {}", PS_CHANNEL, channel); + * return; + * } + */ + if (command instanceof StringType) { + setPlayPresetChannel(channel, command.toString()); + } else { + logger.debug("{} command not an StringType: {}", PS_CHANNEL, command); + } + } else if (PL_PRESET.equalsIgnoreCase(channel.getCategory())) { + final String output = channel.getId(); + if (command instanceof DecimalType) { + final int preset = ((DecimalType) command).intValue(); + statePlaying.compute(output, (k, v) -> { + if (v == null) { + return new PlayingState("", preset); + } else { + return new PlayingState(v.getUri(), preset); + } + }); + } else { + logger.debug("{} command not an DecimalType: {}", PL_PRESET, command); + } + } else if (channel.getCategory().startsWith(TERM)) { + final String uri = channel.getPathPart(0); + if (uri == null || uri.isEmpty()) { + logger.debug("{} command - channel has no uri: {}", TERM, channel); + return; + } + switch (channel.getCategory()) { + case TERM_SOURCE: { + if (command instanceof StringType) { + setTerminalSource(uri, command.toString()); + } else { + logger.debug("{} command not an StringType: {}", TERM_SOURCE, command); + } + break; + } + case TERM_ACTIVE: { + if (command instanceof OnOffType) { + setTerminalStatus(uri, command == OnOffType.ON); + } else { + logger.debug("{} command not an OnOffType: {}", TERM_ACTIVE, command); + } + break; + } + } + } else if (channel.getCategory().startsWith(CONTENT)) { + switch (channel.getCategory()) { + case CN_PARENTURI: { + if (command instanceof StringType) { + stateContent.set(new ContentState(command.toString(), "", 0)); + getContext().getScheduler().execute(() -> refreshContent()); + } else { + logger.debug("{} command not an StringType: {}", CN_PARENTURI, command); + } + break; + } + case CN_INDEX: { + if (command instanceof DecimalType) { + stateContent.updateAndGet(cs -> new ContentState(cs.getParentUri(), cs.getUri(), + ((DecimalType) command).intValue())); + getContext().getScheduler().execute(() -> refreshContent()); + } else { + logger.debug("{} command not an DecimalType: {}", CN_INDEX, command); + } + break; + } + case CN_CMD: { + if (command instanceof StringType) { + final String cmd = command.toString(); + if ("select".equalsIgnoreCase(cmd)) { + final ContentState state = stateContent.get(); + setPlayContent(state.getUri(), null); + } else { + logger.debug("{} command received an unknown command: {}", CN_CMD, cmd); + } + } else { + logger.debug("{} command not an StringType: {}", CN_CMD, command); + } + break; + } + case CN_ISPROTECTED: { + if (command instanceof OnOffType) { + setContentProtection(command == OnOffType.ON); + } else { + logger.debug("{} command not an OnOffType: {}", CN_ISPROTECTED, command); + } + break; + } + case CN_EPGVISIBILITY: { + if (command instanceof StringType) { + setTvContentVisibility(command.toString(), null, null); + } else { + logger.debug("{} command not an StringType: {}", CN_EPGVISIBILITY, command); + } + break; + } + case CN_CHANNELSURFINGVISIBILITY: { + if (command instanceof StringType) { + setTvContentVisibility(null, command.toString(), null); + } else { + logger.debug("{} command not an StringType: {}", CN_CHANNELSURFINGVISIBILITY, command); + } + break; + } + case CN_VISIBILITY: { + if (command instanceof StringType) { + setTvContentVisibility(null, null, command.toString()); + } else { + logger.debug("{} command not an StringType: {}", CN_VISIBILITY, command); + } + break; + } + } + } + } + + /** + * Sets the content protection on or off + * + * @param on true to turn on protection, false otherwise + */ + private void setContentProtection(final boolean on) { + final ContentState cs = stateContent.get(); + handleExecute(ScalarWebMethod.SETDELETEPROTECTION, new DeleteProtection(cs.getUri(), on)); + } + + /** + * Plays (or stops) the specified content - potentially on a specified output + * + * @param uri the non-null, possibly empty URI + * @param output the possibly null, possibly empty output to play on + */ + private void setPlayContent(final String uri, final @Nullable String output) { + Objects.requireNonNull(uri, "uri cannot be null"); + + final String enrichedUri = getEnrichedUri(uri); + final String translatedOutput = getTranslatedOutput(output); + handleExecute(ScalarWebMethod.SETPLAYCONTENT, version -> { + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0, ScalarWebMethod.V1_1)) { + return new PlayContent_1_0(enrichedUri); + } + return new PlayContent_1_2(enrichedUri, translatedOutput); + }); + } + + /** + * Enrich uri to handle situation where uri refers to tv source only , e.g. 'tv:dvbs' or 'tv:' + * This is the case when trying to set a TV source from the terminal source channel. + * In this situation, try to get full tv content uri with channel information + * + * @param uri + * @return + */ + private String getEnrichedUri(final String uri) { + // get enriched uri from last played tv uri if exists (uri is of form 'tv:dvbs' or 'tv:') + final String lastPlayedTVUri = lastPlayedTVUriBySource.get(uri); + if (lastPlayedTVUri != null) + return lastPlayedTVUri; + + // get enriched uri from preset entry if exists (uri is of form 'tv:dvbs' or 'tv:') + if (displayNumberUriMapBySource.containsKey(uri)) { + // return displayNumberUriMapBySource.get(uri).values().stream().findFirst().orElse(uri); + final String presetUri = displayNumberUriMapBySource.get(uri).get("0001"); + if (presetUri != null) { + return presetUri; + } + } + return uri; + } + + /** + * Returns the translated output (ie MAINOUTPUT would be translated to an empty string) + * + * @param output a possibly null, possibly empty output + * @return a non-null, possibly empty translated output + */ + private static String getTranslatedOutput(@Nullable String output) { + return output == null || MAINOUTPUT.equalsIgnoreCase(output) ? "" : output; + } + + /** + * Handles the playing command. A playing command can handle the following: + *
    + *
  1. play - plays the current selection
  2. + *
  3. pause - pauses the currently playing selection
  4. + *
  5. stop - stops the currently playing selection
  6. + *
  7. next - plays the next content (if a radio, scans forward)
  8. + *
  9. prev - plays the previous content (if a radio, scans backward)
  10. + *
  11. fwd - fast-forward content (if a radio, seeks forward manually)
  12. + *
  13. bwd - rewinds content (if a radio, seeks backward manually)
  14. + *
  15. fwdseek - if a radio, seeks forward automatically
  16. + *
  17. bwdseek - if a radio, seeks backward automatically
  18. + *
  19. setpreset - if a radio, sets the current preset
  20. + *
  21. getpreset - if a radio, restores the current preset
  22. + *
+ * + * @param output a non-null, non-empty output to play on + * @param id a non-null, non-empty id + * @param command a non-null non-empty command to execute + */ + private void setPlayingCommand(final String output, final String id, final String command) { + SonyUtil.validateNotEmpty(output, "output cannot be empty"); + SonyUtil.validateNotEmpty(id, "id cannot be empty"); + SonyUtil.validateNotEmpty(command, "id cannot be empty"); + + final PlayingState state = statePlaying.get(id); + final String playingUri = state == null ? "" : state.getUri(); + + final String translatedOutput = getTranslatedOutput(output); + + final Matcher ms = Source.RADIOPATTERN.matcher(playingUri); + final boolean isRadio = ms.matches(); + + switch (command.toLowerCase()) { + case "play": + // don't use the "setPlayContent" method here because it will start playing from beginning + // by just passing an output, it continues play after a pause/fast forward etc + handleExecute(ScalarWebMethod.SETPLAYCONTENT, new Output(translatedOutput)); + break; + + case "pause": + handleExecute(ScalarWebMethod.PAUSEPLAYINGCONTENT, new Output(translatedOutput)); + break; + + case "stop": + if (getService().hasMethod(ScalarWebMethod.STOPPLAYINGCONTENT)) { + handleExecute(ScalarWebMethod.STOPPLAYINGCONTENT, new Output(playingUri)); + } else { + handleExecute(ScalarWebMethod.DELETECOUNT, new DeleteContent(playingUri)); + } + break; + + case "next": + if (isRadio) { + handleExecute(ScalarWebMethod.SCANPLAYINGCONTENT, + new ScanPlayingContent_1_0(true, translatedOutput)); + } else { + handleExecute(ScalarWebMethod.SETPLAYNEXTCONTENT, new Output(translatedOutput)); + } + break; + + case "prev": + if (isRadio) { + handleExecute(ScalarWebMethod.SCANPLAYINGCONTENT, + new ScanPlayingContent_1_0(false, translatedOutput)); + } else { + handleExecute(ScalarWebMethod.SETPLAYPREVIOUSCONTENT, new Output(translatedOutput)); + } + break; + + case "fwd": + if (isRadio) { + handleExecute(ScalarWebMethod.SEEKBROADCASTSTATION, new SeekBroadcastStation_1_0(true, false)); + } else { + handleExecute(ScalarWebMethod.SCANPLAYINGCONTENT, + new ScanPlayingContent_1_0(true, translatedOutput)); + } + break; + + case "bwd": + if (isRadio) { + handleExecute(ScalarWebMethod.SEEKBROADCASTSTATION, new SeekBroadcastStation_1_0(false, false)); + } else { + handleExecute(ScalarWebMethod.SCANPLAYINGCONTENT, + new ScanPlayingContent_1_0(false, translatedOutput)); + } + break; + + case "fwdseek": + if (isRadio) { + handleExecute(ScalarWebMethod.SEEKBROADCASTSTATION, new SeekBroadcastStation_1_0(true, true)); + } else { + logger.debug("Not playing a radio currently: {}", playingUri); + } + break; + + case "bwdseek": + if (isRadio) { + handleExecute(ScalarWebMethod.SEEKBROADCASTSTATION, new SeekBroadcastStation_1_0(false, true)); + } else { + logger.debug("Not playing a radio currently: {}", playingUri); + } + break; + + case "setpreset": + if (isRadio) { + final int preset = state == null ? 1 : state.getPreset(); + final String presetUri = ms.groupCount() == 1 ? ("?contentId=" + preset) + : ms.replaceFirst("$1" + preset); + handleExecute(ScalarWebMethod.PRESETBROADCASTSTATION, new PresetBroadcastStation(presetUri)); + } else { + logger.debug("Not playing a radio currently: {}", playingUri); + } + break; + + case "getpreset": + if (isRadio) { + final int preset = state == null ? 1 : state.getPreset(); + final String presetUri = ms.groupCount() == 1 ? ("?contentId=" + preset) + : ms.replaceFirst("$1" + preset); + setPlayContent(presetUri, translatedOutput); + } else { + logger.debug("Not playing a radio currently: {}", playingUri); + } + break; + + default: + break; + } + } + + /** + * Sets the source for the specified terminal + * + * @param output a non-null, non-empty output + * @param source a non-null, non-empty source + */ + private void setTerminalSource(final String output, final String source) { + SonyUtil.validateNotEmpty(output, "output cannot be empty"); + SonyUtil.validateNotEmpty(source, "source cannot be empty"); + + final Optional src = getSources().stream().filter(s -> s.isMatch(source)).findFirst(); + final String srcUri = src.isPresent() ? src.get().getSource() : null; + setPlayContent(SonyUtil.defaultIfEmpty(srcUri, source), output); + + getContext().getScheduler().execute(() -> { + stateContent.set(new ContentState(source, "", 0)); + refreshContent(); + }); + } + + /** + * Sets the terminal status to active or not + * + * @param uri the non-null, non-empty terminal status + * @param on true if playing, false otherwise + */ + private void setTerminalStatus(final String uri, final boolean on) { + SonyUtil.validateNotEmpty(uri, "uri cannot be empty"); + + handleExecute(ScalarWebMethod.SETACTIVETERMINAL, + new ActiveTerminal(uri, on ? ActiveTerminal.ACTIVE : ActiveTerminal.INACTIVE)); + + // Turn off any other channel status + for (final ScalarWebChannel chnl : getChannelTracker().getLinkedChannelsForCategory(TERM_ACTIVE)) { + if (!uri.equalsIgnoreCase(chnl.getPathPart(0))) { + stateChanged(TERM_ACTIVE, chnl.getId(), OnOffType.OFF); + } + } + } + + /** + * Sets the tv content visibility. + * + * @param epgVisibility the epg visibility (null if not specified) + * @param channelSurfingVisibility the channel surfing visibility (null if not specified) + * @param visibility the visibility (null if not specified) + */ + private void setTvContentVisibility(final @Nullable String epgVisibility, + final @Nullable String channelSurfingVisibility, final @Nullable String visibility) { + final ContentState cs = stateContent.get(); + handleExecute(ScalarWebMethod.SETTVCONTENTVISIBILITY, + new TvContentVisibility(cs.getUri(), epgVisibility, channelSurfingVisibility, visibility)); + } + + /** + * Plays the preset channel specified by it's display name + * + * @param channel a non-null, non-empty channel + * @param dispName a non-null, non-empty display name + */ + private void setPlayPresetChannel(final ScalarWebChannel channel, final String dispName) { + Objects.requireNonNull(channel); + SonyUtil.validateNotEmpty(dispName, "dispName cannot be empty"); + + final String srcId = channel.getPathPart(0); + if (srcId == null || srcId.isEmpty()) { + logger.debug("{} command - channel has no srcId: {}", PS_CHANNEL, channel); + return; + } + + if (PS_REFRESH.equals(dispName)) { + // special command to refresh presets + refreshPresetChannelStateDescription(Collections.singletonList(channel)); + } else { + final String uri = displayNumberUriMapBySource.get(srcId).get(dispName); + if (uri != null && !uri.isEmpty()) { + setPlayContent(uri, null); + } + } + } + + /** + * Refreshs the preset channel state description for the specified channels + * + * @param channels a non-null, possibly empty list of channels + */ + private void refreshPresetChannelStateDescription(final List channels) { + Objects.requireNonNull(channels, "channels cannot be null"); + + for (final ScalarWebChannel chl : channels) { + final String srcId = chl.getPathPart(0); + if (srcId == null || srcId.isEmpty()) { + logger.debug("{} command - channel has no srcId: {}", PS_CHANNEL, chl); + continue; + } + + // clear cache + final ConcurrentMap displayNumberUriMap; + if (displayNumberUriMapBySource.get(srcId) != null) { + displayNumberUriMap = displayNumberUriMapBySource.get(srcId); + } else { + displayNumberUriMap = new ConcurrentHashMap<>(); + displayNumberUriMapBySource.put(srcId, displayNumberUriMap); + } + + final List presets = new ArrayList<>(); + processContentList(srcId, res -> { + presets.add(res); + return true; + }); + + final List stateOptions = new ArrayList<>(); + + final boolean isPresetConfigurable = Boolean.TRUE.equals(getContext().getConfig().isConfigurablePresets()) + && srcId.toLowerCase().startsWith("tv:"); + + if (!presets.isEmpty()) { + if (isPresetConfigurable) { + final HashMap rankMap = new HashMap<>(); + final String thingId = getContext().getThing().getUID().getId(); + final Path path = Paths.get(OpenHAB.getUserDataFolder(), "config", "sony", "presets", + ScalarWebChannel.createChannelId(chl.getCategory(), chl.getId()) + "_" + thingId + ".csv"); + if (Files.exists(path)) { + try { + // regex to parse csv formatted lines (with limitations) + String regexCSV = ",(?=([^\"]*\"[^\"]*\")*[^\"]*$)"; + String regexQuotes = "^\"|\"$"; + final String content = Files.readString(path); + Scanner scanner = new Scanner(content); + // skip header row + scanner.nextLine(); + while (scanner.hasNextLine()) { + try { + final String line = scanner.nextLine(); + final String[] values = line.split(regexCSV); + final String dispNum = values[1].trim().replaceAll(regexQuotes, ""); + final Integer rank = Integer.parseInt(values[4].trim().replaceAll(regexQuotes, "")); + rankMap.put(dispNum, rank); + } catch (final Exception ex) { + // ignore + } + } + scanner.close(); + } catch (final Exception ex) { + logger.debug( + "Exception '{}' while trying to read or process content list for source {} from path {}", + ex.getMessage(), srcId, path); + } + } + List stateOptionsToAdd = presets.stream().map(e -> { + final String title = e.getTitle(); + final String dispNum = e.getDispNum(); + final String uri = e.getUri(); + Optional si = Optional.empty(); + if (dispNum != null && !dispNum.isEmpty() && uri != null && !uri.isEmpty()) { + si = Optional.of(new StateOption(dispNum, SonyUtil.defaultIfEmpty(title, dispNum))); + if (displayNumberUriMap != null) { + displayNumberUriMap.put(dispNum, uri); + } + } + return si; + }).filter(Optional::isPresent).map(Optional::get) + .filter(a -> (rankMap.getOrDefault(a.getValue(), Integer.MAX_VALUE)) >= 0) + .sorted(Comparator. comparingInt(a -> { + Integer r = rankMap.getOrDefault(a.getValue(), 0); + return r == 0 ? Integer.MAX_VALUE : r; + }).thenComparing(a -> SonyUtil.defaultIfEmpty(a.getLabel(), ""))) + .collect(Collectors.toList()); + + stateOptions.addAll(stateOptionsToAdd); + + StringBuilder content = new StringBuilder(); + content.append("Source, DispNum, Title, Uri, Rank\n"); + for (final ContentListResult_1_0 clr : presets) { + content.append(String.format("\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n", srcId, clr.getDispNum(), + clr.getTitle(), clr.getUri(), rankMap.getOrDefault(clr.getDispNum(), 0))); + } + try { + Files.createDirectories(path.getParent()); + Files.writeString(path, content.toString()); + } catch (final IOException e) { + logger.debug("IOException trying to write content list for source {} to path {}", srcId, path); + } + + } else { + List stateOptionsToAdd = presets.stream().map(e -> { + final String title = e.getTitle(); + final String dispNum = e.getDispNum(); + final String uri = e.getUri(); + Optional si = Optional.empty(); + if (dispNum != null && !dispNum.isEmpty() && uri != null && !uri.isEmpty()) { + si = Optional.of(new StateOption(dispNum, SonyUtil.defaultIfEmpty(title, dispNum))); + displayNumberUriMap.put(dispNum, uri); + } + return si; + }).filter(Optional::isPresent).map(Optional::get) + .sorted(Comparator.comparing(a -> SonyUtil.defaultIfEmpty(a.getLabel(), ""))) + .collect(Collectors.toList()); + + stateOptions.addAll(stateOptionsToAdd); + + } + + // add special refresh option at beginning + stateOptions.add(0, new StateOption(PS_REFRESH, PS_REFRESH)); + final StateDescriptionFragmentBuilder bld = StateDescriptionFragmentBuilder.create() + .withOptions(stateOptions); + final StateDescription sd = bld.build().toStateDescription(); + if (sd != null) { + getContext().getStateProvider().addStateOverride(getContext().getThingUID(), chl.getChannelId(), + sd); + } + } + } + } + + /** + * Processes a content list request and calls back the provided callback until either processed or nothing left + * + * @param uriOrSource a non-null, non-empty uri or source + * @param callback a non-null callback to use + */ + private void processContentList(final String uriOrSource, final ContentListCallback callback) { + SonyUtil.validateNotEmpty(uriOrSource, "uriOrSource cannot be empty"); + Objects.requireNonNull(callback, "callback cannot be null"); + + Count ct; + try { + ct = execute(ScalarWebMethod.GETCONTENTCOUNT, version -> { + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0, ScalarWebMethod.V1_1, + ScalarWebMethod.V1_2)) { + return new ContentCount_1_0(uriOrSource); + } + return new ContentCount_1_3(uriOrSource); + }).as(Count.class); + + } catch (final IOException e) { + ct = new Count(0); + } + + final int maxCount = ct.getCount(); + int count = 0; + while (count < maxCount) { + final int stIdx = count; + try { + final ScalarWebResult res = execute(ScalarWebMethod.GETCONTENTLIST, version -> { + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0, ScalarWebMethod.V1_1, + ScalarWebMethod.V1_2, ScalarWebMethod.V1_3)) { + return new ContentListRequest_1_0(uriOrSource, stIdx, MAX_CT); + } + return new ContentListRequest_1_4(uriOrSource, stIdx, MAX_CT); + }); + final List resultList = res.asArray(ContentListResult_1_0.class); + for (final ContentListResult_1_0 clr : resultList) { + if (!callback.processContentListResult(clr)) { + return; + } + } + // request might return fewer items than MAX_CT, therefore get actual number of fetched items from + // result + // list + count += resultList.size(); + } catch (final IOException e) { + logger.debug("IOException getting {} for {} [idx: {}, max: {}]: {}", ScalarWebMethod.GETCONTENTLIST, + uriOrSource, stIdx, MAX_CT, e.getMessage()); + // give-up getting content items + break; + } + } + logger.debug("Received {} content items for source {}", count, uriOrSource); + } + + /** + * Converts a URI to a channel id by parsing the URI and using the port/zone name (host name in the URI) and the + * port/zone number (port/zone query parameter). Please note that we specifically short 'extInput' to 'in' and + * 'extOutput' to 'out' for ease of writing channel names. + * + * Note: CEC schemes are unique by port number/logical address. We will further add a "-{addr}" if a logical address + * exists + * + * Note2: if we have inputs only (not a terminal), then the "in" part will be ignored. + * + * The following demonstrates the various possible inputs and the output of this function: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * @param uri a non-null, non-empty uri + * @param isInput true if we are doing inputs, false otherwise + * @return a non-null channel identifier + */ + private String createChannelId(final String uri, final boolean isInput) { + SonyUtil.validateNotEmpty(uri, "uri cannot be empty"); + + // Return the uri if just a word (not really a uri!) + final int colIdx = uri.indexOf(":"); + if (colIdx < 0) { + return SonyUtil.createValidChannelUId(uri); + } + + String scheme = uri.substring(0, colIdx); + if (isInput) { + scheme = ""; + } else if ("extinput".equalsIgnoreCase(scheme)) { + scheme = "in-"; + } else if ("extoutput".equalsIgnoreCase(scheme)) { + scheme = "out-"; + } else { + scheme = scheme + "-"; + } + final String finalScheme = scheme; // must make final - ugh + + final int portNameIdx = uri.indexOf("?"); + if (portNameIdx < 0) { + return SonyUtil.createValidChannelUId(finalScheme + uri.substring(colIdx + 1)); + } + + final String portName = uri.substring(colIdx + 1, portNameIdx); + final String query = uri.substring(portNameIdx + 1); + + // Handle when there is no query path + if (SonyUtil.isEmpty(query)) { + return SonyUtil.createValidChannelUId(finalScheme + portName); + } + + // Note that this won't handle multiple parms with the same name - we just hope sony won't ever do that + final Map queryParms = Arrays.stream(query.split("&")).map(o -> o.split("=")) + .collect(Collectors.toMap(k -> k[0], v -> v[1])); + + final String portNbr = queryParms.get("port"); + final String zoneNbr = queryParms.get("zone"); + + String channelId = finalScheme + portName; + + if (!SonyUtil.isEmpty(zoneNbr)) { + channelId += zoneNbr; + } else if (!SonyUtil.isEmpty(portNbr)) { + channelId += portNbr; + } + + // Should only be for CEC - but let's let it ride on all + final String logAddr = queryParms.get("logicalAddr"); + if (!SonyUtil.isEmpty(logAddr)) { + channelId += ("-" + logAddr); + } + + return SonyUtil.createValidChannelUId(channelId); + } + + /** + * helper method to conver tthe terminal status to a map of terminal URL to terminal title + * + * @param terms a non-null, possibly empty list of terminal status + * @return a map of terminal title by terminal uri + */ + private static Map getTerminalOutputs(final List terms) { + Objects.requireNonNull(terms, "terms cannot be nul"); + + final Map outputs = new HashMap<>(); + for (final CurrentExternalTerminalsStatus_1_0 term : terms) { + final String uri = term.getUri(); + if (uri != null && term.isOutput()) { + outputs.put(uri, term.getTitle(uri)); + } + } + + return outputs; + } + + /** + * Helper class to track the current content state (uris and index) + */ + @NonNullByDefault + private static class ContentState { + /** The parent uri */ + private final String parentUri; + + /** The content uri */ + private final String uri; + + /** The content index position */ + private final int idx; + + /** Constructs the state with no URIs and a 0 index position */ + public ContentState() { + this("", "", 0); + } + + /** + * Constructs the state with the specified uris and index position + * + * @param parentUri a non null, possibly empty parent uri + * @param uri a non null, possibly empty uri + * @param idx a greater than or equal to 0 index position + */ + public ContentState(final String parentUri, final String uri, final int idx) { + Objects.requireNonNull(parentUri, "parentUri cannot be null"); + Objects.requireNonNull(uri, "uri cannot be null"); + if (idx < 0) { + throw new IllegalArgumentException("idx cannot be below zero: " + idx); + } + this.parentUri = parentUri; + this.uri = uri; + this.idx = idx; + } + + /** + * Gets the parent URI + * + * @return the parentUri + */ + public String getParentUri() { + return parentUri; + } + + /** + * Gets the URI + * + * @return the uri + */ + public String getUri() { + return uri; + } + + /** + * Gets the index position + * + * @return the index position + */ + public int getIdx() { + return idx; + } + } + + /** + * This class represets the current playing state + */ + @NonNullByDefault + private static class PlayingState { + /** The playing URI */ + private final String uri; + + /** The play preset */ + private final int preset; + + /** + * Constructs the playing state from the URI + * + * @param uri a non-null, possibly empty URI + * @param preset a greater than or equal to 0 preset + */ + public PlayingState(final String uri, final int preset) { + Objects.requireNonNull(uri, "uri cannot be null"); + this.uri = uri; + this.preset = preset; + } + + /** + * The URI + * + * @return the uri + */ + public String getUri() { + return uri; + } + + /** + * The preset + * + * @return the preset + */ + public int getPreset() { + return preset; + } + } + + /** + * A helper class to store the input source attributes + */ + @NonNullByDefault + private static class InputSource { + /** The URI of the input */ + private final String uri; + + /** The title of the input */ + private final String title; + + /** The outputs linked to the input */ + private final List outputs; + + /** + * Constructs the input source based on the parms + * + * @param uri a non-null, non-empty input URI + * @param title a possibly null, possibly empty input title + * @param outputs a possibly null, possibly empty list of outputs + */ + public InputSource(final String uri, final @Nullable String title, final @Nullable List outputs) { + SonyUtil.validateNotEmpty(uri, "uri cannot be empty"); + this.uri = uri; + this.title = SonyUtil.defaultIfEmpty(title, uri); + this.outputs = outputs == null ? new ArrayList<>() : outputs; + } + + /** + * The URI + * + * @return the uri + */ + public String getUri() { + return uri; + } + + /** + * The title + * + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * The outputs + * + * @return the outputs + */ + public List getOutputs() { + return outputs; + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebBrowserProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebBrowserProtocol.java new file mode 100644 index 0000000000000..dbd69a317237b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebBrowserProtocol.java @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelTracker; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.BrowserControl; +import org.openhab.binding.sony.internal.scalarweb.models.api.TextUrl; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.Command; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The implementation of the protocol handles the Browser service + * + * @param the generic type for the callback + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +class ScalarWebBrowserProtocol<@NonNull T extends ThingCallback> extends AbstractScalarWebProtocol { + + /** + * The logger + */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebBrowserProtocol.class); + + // Constants used by this protocol + private static final String BROWSERCONTROL = "browsercontrol"; + private static final String TEXTURL = "texturl"; + private static final String TEXTTITLE = "texttitle"; + private static final String TEXTTYPE = "texttype"; + private static final String TEXTFAVICON = "textfavicon"; + + /** + * Instantiates a new scalar web browser protocol. + * + * @param factory the non-null factory + * @param context the non-null context + * @param service the non-null service + * @param callback the non-null callback + */ + ScalarWebBrowserProtocol(final ScalarWebProtocolFactory factory, final ScalarWebContext context, + final ScalarWebService service, final @NonNull T callback) { + super(factory, context, service, callback); + } + + @Override + public Collection getChannelDescriptors(final boolean dynamicOnly) { + final List descriptors = new ArrayList(); + + // no dynamic channels + if (dynamicOnly) { + return descriptors; + } + + if (service.hasMethod(ScalarWebMethod.ACTIVATEBROWSERCONTROL)) { + descriptors.add(createDescriptor(createChannel(BROWSERCONTROL), "String", "scalarbrowseractivate")); + } + + if (service.hasMethod(ScalarWebMethod.GETTEXTURL)) { + descriptors.add(createDescriptor(createChannel(TEXTURL), "String", "scalarbrowsertexturl")); + descriptors.add(createDescriptor(createChannel(TEXTTITLE), "String", "scalarbrowsertexttitle")); + descriptors.add(createDescriptor(createChannel(TEXTTYPE), "String", "scalarbrowsertexttype")); + descriptors.add(createDescriptor(createChannel(TEXTFAVICON), "Image", "scalarbrowsertextfavicon")); + } + + return descriptors; + } + + @Override + public void refreshState(boolean initial) { + final ScalarWebChannelTracker tracker = getChannelTracker(); + if (tracker.isCategoryLinked(BROWSERCONTROL)) { + refreshBrowserControl(); + } + + if (tracker.isCategoryLinked(TEXTURL, TEXTTITLE, TEXTTYPE, TEXTFAVICON)) { + refreshTextUrl(); + } + } + + @Override + public void refreshChannel(final ScalarWebChannel channel) { + Objects.requireNonNull(channel, "channel cannot be null"); + + switch (channel.getCategory()) { + case BROWSERCONTROL: + refreshBrowserControl(); + break; + + case TEXTURL: + case TEXTTITLE: + case TEXTTYPE: + case TEXTFAVICON: + refreshTextUrl(); + break; + + default: + logger.debug("Unknown refresh channel: {}", channel); + break; + } + } + + /** + * Refresh browser control + */ + private void refreshBrowserControl() { + stateChanged(BROWSERCONTROL, StringType.EMPTY); + } + + /** + * Refresh text url information (url, title, type, favicon) + */ + private void refreshTextUrl() { + try { + final TextUrl url = execute(ScalarWebMethod.GETTEXTURL).as(TextUrl.class); + + stateChanged(TEXTURL, SonyUtil.newStringType(url.getUrl())); + stateChanged(TEXTTITLE, SonyUtil.newStringType(url.getTitle())); + stateChanged(TEXTTYPE, SonyUtil.newStringType(url.getType())); + stateChanged(TEXTFAVICON, SonyUtil.newStringType(url.getFavicon())); + + final String iconUrl = url.getFavicon(); + if (iconUrl == null || iconUrl.isEmpty()) { + callback.stateChanged(TEXTFAVICON, UnDefType.UNDEF); + } else { + try (SonyHttpTransport transport = SonyTransportFactory.createHttpTransport( + getService().getTransport().getBaseUri().toString(), getContext().getClientBuilder())) { + final RawType rawType = NetUtil.getRawType(transport, iconUrl); + callback.stateChanged(TEXTFAVICON, rawType == null ? UnDefType.UNDEF : rawType); + } catch (final URISyntaxException e) { + logger.debug("Exception occurred getting application icon: {}", e.getMessage()); + } + } + + } catch (final IOException e) { + logger.debug("Error retrieving text URL information: {}", e.getMessage()); + } + } + + @Override + public void setChannel(final ScalarWebChannel channel, final Command command) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + switch (channel.getCategory()) { + case BROWSERCONTROL: + if (command instanceof StringType) { + setActivateBrowserControl(command.toString()); + } else { + logger.debug("BROWSERCONTROL mode command not an StringType: {}", command); + } + + break; + + case TEXTURL: + if (command instanceof StringType) { + setTextUrl(command.toString()); + } else { + logger.debug("TEXTURL command not an StringType: {}", command); + } + + break; + + default: + logger.debug("Unhandled channel command: {} - {}", channel, command); + break; + } + } + + /** + * Activates the browser control + * + * @param control the new activate browser control + */ + private void setActivateBrowserControl(final @Nullable String control) { + handleExecute(ScalarWebMethod.ACTIVATEBROWSERCONTROL, + new BrowserControl(SonyUtil.defaultIfEmpty(control, null))); + } + + /** + * Sets the URL text + * + * @param url the new URL text + */ + private void setTextUrl(final String url) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + handleExecute(ScalarWebMethod.SETTEXTURL, new TextUrl(url)); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebCecProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebCecProtocol.java new file mode 100644 index 0000000000000..0a8666294b3ce --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebCecProtocol.java @@ -0,0 +1,206 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.Enabled; +import org.openhab.binding.sony.internal.scalarweb.models.api.PowerSyncMode; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The implementation of the protocol handles the CEC service + * + * @author Tim Roberts - Initial contribution + * @param the generic type for the callback + */ +@NonNullByDefault +class ScalarWebCecProtocol<@NonNull T extends ThingCallback> extends AbstractScalarWebProtocol { + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebCecProtocol.class); + + // Constants used by the protocol + private static final String CONTROLMODE = "controlmode"; + private static final String MHLAUTOINPUTCHANGEMODE = "mhlautoinputchangemode"; + private static final String MHLPOWERFEEDMODE = "mhlpowerfeedmode"; + private static final String POWEROFFSYNCMODE = "poweroffsyncmode"; + private static final String POWERONSYNCMODE = "poweronsyncmode"; + + /** + * Instantiates a new scalar web cec protocol. + * + * @param factory the non-null factory + * @param context the non-null context + * @param service the non-null service + * @param callback the non-null callback + */ + ScalarWebCecProtocol(final ScalarWebProtocolFactory factory, final ScalarWebContext context, + final ScalarWebService service, final @NonNull T callback) { + super(factory, context, service, callback); + } + + @Override + public Collection getChannelDescriptors(final boolean dynamicOnly) { + final List descriptors = new ArrayList(); + + // no dynamic channels + if (dynamicOnly) { + return descriptors; + } + + if (service.hasMethod(ScalarWebMethod.SETCECCONTROLMODE)) { + descriptors.add(createDescriptor(createChannel(CONTROLMODE), "Switch", "scalarceccontrolmode")); + } + + if (service.hasMethod(ScalarWebMethod.SETMHLAUTOINPUTCHANGEMODE)) { + descriptors.add(createDescriptor(createChannel(MHLAUTOINPUTCHANGEMODE), "Switch", + "scalarcecmhlautoinputchangemode")); + } + + if (service.hasMethod(ScalarWebMethod.SETMHLPOWERFEEDMODE)) { + descriptors.add(createDescriptor(createChannel(MHLPOWERFEEDMODE), "Switch", "scalarcecmhlpowerfeedmode")); + } + + if (service.hasMethod(ScalarWebMethod.SETPOWERSYNCMODE)) { + descriptors.add(createDescriptor(createChannel(POWEROFFSYNCMODE), "Switch", "scalarcecpoweroffsyncmode")); + descriptors.add(createDescriptor(createChannel(POWERONSYNCMODE), "Switch", "scalarcecpoweronsyncmode")); + } + + return descriptors; + } + + @Override + public void refreshState(boolean initial) { + } + + @Override + public void refreshChannel(final ScalarWebChannel channel) { + } + + @Override + public void setChannel(final ScalarWebChannel channel, final Command command) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + switch (channel.getCategory()) { + case CONTROLMODE: + if (command instanceof OnOffType) { + setControlMode(command == OnOffType.ON); + } else { + logger.debug("Control mode command not an OnOffType: {}", command); + } + + break; + + case MHLAUTOINPUTCHANGEMODE: + if (command instanceof OnOffType) { + setMhlAutoInputChangeMode(command == OnOffType.ON); + } else { + logger.debug("MHLAUTOINPUTCHANGEMODE command not an OnOffType: {}", command); + } + + break; + + case MHLPOWERFEEDMODE: + if (command instanceof OnOffType) { + setMhlPowerFeedMode(command == OnOffType.ON); + } else { + logger.debug("MHLPOWERFEEDMODE command not an OnOffType: {}", command); + } + + break; + + case POWEROFFSYNCMODE: + if (command instanceof OnOffType) { + setPowerOffSyncMode(command == OnOffType.ON); + } else { + logger.debug("POWEROFFSYNCMODE command not an OnOffType: {}", command); + } + + break; + + case POWERONSYNCMODE: + if (command instanceof OnOffType) { + setPowerOnSyncMode(command == OnOffType.ON); + } else { + logger.debug("POWERONSYNCMODE command not an OnOffType: {}", command); + } + + break; + + default: + logger.debug("Unhandled channel command: {} - {}", channel, command); + break; + } + } + + /** + * Sets the control mode + * + * @param on true if on, false otherwise + */ + private void setControlMode(final boolean on) { + handleExecute(ScalarWebMethod.SETCECCONTROLMODE, new Enabled(on)); + } + + /** + * Sets the mhl auto input change mode + * + * @param on true if on, false otherwise + */ + private void setMhlAutoInputChangeMode(final boolean on) { + handleExecute(ScalarWebMethod.SETMHLAUTOINPUTCHANGEMODE, new Enabled(on)); + } + + /** + * Sets the mhl power feed mode + * + * @param on true if on, false otherwise + */ + private void setMhlPowerFeedMode(final boolean on) { + handleExecute(ScalarWebMethod.SETMHLPOWERFEEDMODE, new Enabled(on)); + } + + /** + * Sets the power off sync mode + * + * @param on true if on, false otherwise + */ + private void setPowerOffSyncMode(final boolean on) { + handleExecute(ScalarWebMethod.SETPOWERSYNCMODE, new PowerSyncMode(on, null)); + } + + /** + * Sets the power on sync mode + * + * @param on true if on, false otherwise + */ + private void setPowerOnSyncMode(final boolean on) { + handleExecute(ScalarWebMethod.SETPOWERSYNCMODE, new PowerSyncMode(null, on)); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebGenericProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebGenericProtocol.java new file mode 100644 index 0000000000000..59c3cda422cb4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebGenericProtocol.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.util.Collection; +import java.util.Collections; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.core.types.Command; + +/** + * The implementation of the protocol is a generic placeholder protocol that provides no active channels + * + * @author Tim Roberts - Initial contribution + * @param the generic type for the callback + */ +@NonNullByDefault +class ScalarWebGenericProtocol<@NonNull T extends ThingCallback> extends AbstractScalarWebProtocol { + /** + * Instantiates a new scalar web video protocol. + * + * @param context the non-null context + * @param service the non-null service + * @param callback the non-null callback + */ + ScalarWebGenericProtocol(final ScalarWebProtocolFactory factory, final ScalarWebContext context, + final ScalarWebService service, final @NonNull T callback) { + super(factory, context, service, callback); + } + + @Override + public Collection getChannelDescriptors(final boolean dynamicOnly) { + return Collections.emptyList(); + } + + @Override + public void refreshState(boolean initial) { + } + + @Override + public void refreshChannel(final ScalarWebChannel channel) { + } + + @Override + public void setChannel(final ScalarWebChannel channel, final Command command) { + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebIlluminationProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebIlluminationProtocol.java new file mode 100644 index 0000000000000..72df2d540ca70 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebIlluminationProtocol.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelTracker; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The implementation of the protocol handles the illumination service + * + * @author Tim Roberts - Initial contribution + * @param the generic type for the callback + */ +@NonNullByDefault +class ScalarWebIlluminationProtocol<@NonNull T extends ThingCallback> extends AbstractScalarWebProtocol { + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebIlluminationProtocol.class); + + // Constants used by this protocol + private static final String ILLUMINATIONSETTINGS = "illuminationsettings"; + + /** + * Instantiates a new scalar web audio protocol. + * + * @param factory the non-null factory to use + * @param context the non-null context to use + * @param audioService the non-null service to use + * @param callback the non-null callback to use + */ + ScalarWebIlluminationProtocol(final ScalarWebProtocolFactory factory, final ScalarWebContext context, + final ScalarWebService audioService, final @NonNull T callback) { + super(factory, context, audioService, callback); + } + + @Override + public Collection getChannelDescriptors(final boolean dynamicOnly) { + final List descriptors = new ArrayList(); + + // no dynamic channels + if (dynamicOnly) { + return descriptors; + } + + if (service.hasMethod(ScalarWebMethod.GETILLUMNATIONSETTING)) { + addGeneralSettingsDescriptor(descriptors, ScalarWebMethod.GETILLUMNATIONSETTING, ILLUMINATIONSETTINGS, + "Illumination Setting"); + } + + return descriptors; + } + + @Override + public void refreshState(boolean initial) { + final ScalarWebChannelTracker tracker = getContext().getTracker(); + if (tracker.isCategoryLinked(ILLUMINATIONSETTINGS)) { + refreshGeneralSettings(tracker.getLinkedChannelsForCategory(ILLUMINATIONSETTINGS), + ScalarWebMethod.GETILLUMNATIONSETTING); + } + } + + @Override + public void refreshChannel(final ScalarWebChannel channel) { + Objects.requireNonNull(channel, "channel cannot be null"); + + final String ctgy = channel.getCategory(); + if (ILLUMINATIONSETTINGS.equalsIgnoreCase(ctgy)) { + refreshGeneralSettings(Collections.singleton(channel), ScalarWebMethod.GETILLUMNATIONSETTING); + } + } + + @Override + public void setChannel(final ScalarWebChannel channel, final Command command) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + switch (channel.getCategory()) { + case ILLUMINATIONSETTINGS: + setGeneralSetting(ScalarWebMethod.SETILLUMNATIONSETTING, channel, command); + break; + + default: + logger.debug("Unhandled channel command: {} - {}", channel, command); + break; + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebLoginProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebLoginProtocol.java new file mode 100644 index 0000000000000..4ea1efd7b05a6 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebLoginProtocol.java @@ -0,0 +1,421 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import javax.ws.rs.client.ClientBuilder; +import javax.xml.parsers.ParserConfigurationException; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.AccessResult; +import org.openhab.binding.sony.internal.CheckResult; +import org.openhab.binding.sony.internal.SonyAuth; +import org.openhab.binding.sony.internal.SonyAuthChecker; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.ircc.IrccClientFactory; +import org.openhab.binding.sony.internal.ircc.models.IrccClient; +import org.openhab.binding.sony.internal.ircc.models.IrccRemoteCommand; +import org.openhab.binding.sony.internal.ircc.models.IrccRemoteCommands; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebClient; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebConfig; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebConstants; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebError; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.InterfaceInformation; +import org.openhab.binding.sony.internal.scalarweb.models.api.NetIf; +import org.openhab.binding.sony.internal.scalarweb.models.api.NetworkSetting; +import org.openhab.binding.sony.internal.scalarweb.models.api.RemoteControllerInfo; +import org.openhab.binding.sony.internal.scalarweb.models.api.RemoteControllerInfo.RemoteCommand; +import org.openhab.binding.sony.internal.scalarweb.models.api.SystemInformation; +import org.openhab.binding.sony.internal.simpleip.SimpleIpConfig; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.openhab.binding.sony.internal.transports.TransportOptionAutoAuth; +import org.openhab.core.OpenHAB; +import org.openhab.core.transform.TransformationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.SAXException; + +/** + * This is the login protocol handler for scalar web systems. The login handler will handle both registration and login. + * Additionally, the handler will also perform initial connection logic (like writing scalar/IRCC commands to the file). + * + * @author Tim Roberts - Initial contribution + * @param the generic type callback + */ +@NonNullByDefault +public class ScalarWebLoginProtocol<@NonNull T extends ThingCallback> { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebLoginProtocol.class); + + /** The configuration */ + private final ScalarWebConfig config; + + /** The callback to set state and status. */ + private final T callback; + + /** The scalar state */ + private final ScalarWebClient scalarClient; + + /** The transformation service */ + private final @Nullable TransformationService transformService; + + /** The clientBuilder used in HttpRequest */ + private final ClientBuilder clientBuilder; + + /** The sony authentication */ + private final SonyAuth sonyAuth; + + /** + * Constructs the protocol handler from given parameters. + * + * @param client a non-null {@link ScalarWebClient} + * @param config a non-null {@link SimpleIpConfig} (may be connected or disconnected) + * @param callback a non-null {@link RioHandlerCallback} to callback + * @param transformService a potentially null transformation service + * @param clientBuilder a client builder + * @throws IOException + */ + public ScalarWebLoginProtocol(final ScalarWebClient client, final ScalarWebConfig config, final @NonNull T callback, + final @Nullable TransformationService transformService, final ClientBuilder clientBuilder) + throws IOException { + this.scalarClient = client; + this.config = config; + this.callback = callback; + this.transformService = transformService; + this.clientBuilder = clientBuilder; + + final ScalarWebService accessControlService = scalarClient.getService(ScalarWebService.ACCESSCONTROL); + + sonyAuth = new SonyAuth(() -> { + final String irccUrl = config.getIrccUrl(); + try { + SonyUtil.sendWakeOnLan(logger, config.getDeviceIpAddress(), config.getDeviceMacAddress()); + return irccUrl == null || irccUrl.isEmpty() ? null : IrccClientFactory.get(irccUrl, clientBuilder); + } catch (IOException | URISyntaxException e) { + logger.debug("Cannot create IRCC Client: {}", e.getMessage()); + return null; + } + }, accessControlService); + } + + /** + * Gets the callback. + * + * @return the callback + */ + T getCallback() { + return callback; + } + + /** + * Attempts to log into the system. + * + * @return the access check result + * @throws IOException Signals that an I/O exception has occurred. + * @throws ParserConfigurationException the parser configuration exception + * @throws SAXException the SAX exception + * @throws URISyntaxException the URI syntax exception + */ + public AccessResult login() throws IOException, ParserConfigurationException, SAXException, URISyntaxException { + final ScalarWebService systemService = scalarClient.getService(ScalarWebService.SYSTEM); + if (systemService == null) { + return AccessResult.SERVICEMISSING; + } + + final URL baseUrl = scalarClient.getDevice().getBaseUrl(); + if (baseUrl == null) { + return new AccessResult(AccessResult.OTHER, "missing base url"); + } + + final SonyTransport[] transports = scalarClient.getDevice().getServices().stream() + .map(srv -> srv.getTransport()).toArray(SonyTransport[]::new); + + // turn off auto authorization for all services + Arrays.stream(transports).forEach(t -> t.setOption(TransportOptionAutoAuth.FALSE)); + // Arrays.stream(transports).forEach(t -> t.setOption(TransportOptionAutoAuth.TRUE)); + + // TODO: Check if this is needed + // SonyUtil.sendWakeOnLan(logger, config.getDeviceIpAddress(), config.getDeviceMacAddress()); + + final String accessCode = config.getAccessCode(); + final SonyAuthChecker authChecker = new SonyAuthChecker(systemService.getTransport(), accessCode); + final CheckResult checkResult = authChecker.checkResult(() -> { + + // Default to a bad access result + AccessResult ar = new AccessResult("unknown", "Unknown Result!"); + + // If we have a power status - execute it first + if (systemService.hasMethod(ScalarWebMethod.GETPOWERSTATUS)) { + final ScalarWebResult result = systemService.execute(ScalarWebMethod.GETPOWERSTATUS); + ar = getAccessResult(result); + if (ar.equals(AccessResult.NEEDSPAIRING) || ar.equals(AccessResult.DISPLAYOFF)) { + return ar; + } + } + + // Now try the get system information + if (systemService.hasMethod(ScalarWebMethod.GETSYSTEMINFORMATION)) { + final ScalarWebResult result = systemService.execute(ScalarWebMethod.GETSYSTEMINFORMATION); + ar = getAccessResult(result); + if (ar.equals(AccessResult.NEEDSPAIRING) || ar.equals(AccessResult.DISPLAYOFF)) { + return ar; + } + } + + // and finally the get device mode + if (systemService.hasMethod(ScalarWebMethod.GETDEVICEMODE)) { + final ScalarWebResult result = systemService.execute(ScalarWebMethod.GETDEVICEMODE); + // getDeviceMode takes an unknown "value" argument - if we get back + // illegal arugment - then it executed fine and we are OK + ar = getAccessResult(result); + if (ar.equals(AccessResult.NEEDSPAIRING) || ar.equals(AccessResult.DISPLAYOFF)) { + return ar; + } + if (result.getDeviceErrorCode() == ScalarWebError.ILLEGALARGUMENT) { + ar = AccessResult.OK; + } + } + + // Finally - return our last result (probably either AccessResult.OK or some http error) + return ar; + }); + + if (CheckResult.OK_HEADER.equals(checkResult)) { + if (accessCode == null || accessCode.isEmpty()) { + // This shouldn't happen - if our check result is OK_HEADER, then + // we had a valid (non-null, non-empty) accessCode. Unfortunately + // nullable checking thinks this can be null now. + logger.debug("This shouldn't happen - access code is blank!: {}", accessCode); + return new AccessResult(AccessResult.OTHER, "Access code cannot be blank"); + } else { + SonyAuth.setupHeader(accessCode, transports); + } + } else if (CheckResult.OK_COOKIE.equals(checkResult)) { + SonyAuth.setupCookie(transports); + } else if (AccessResult.DISPLAYOFF.equals(checkResult)) { + return checkResult; + } else if (AccessResult.NEEDSPAIRING.equals(checkResult)) { + if (SonyUtil.isEmpty(accessCode)) { + return new AccessResult(AccessResult.OTHER, "Access code cannot be blank"); + } else { + try (SonyHttpTransport httpTransport = SonyTransportFactory.createHttpTransport(baseUrl, + ScalarWebService.ACCESSCONTROL, clientBuilder)) { + final AccessResult res = sonyAuth.requestAccess(httpTransport, + ScalarWebConstants.ACCESSCODE_RQST.equalsIgnoreCase(accessCode) ? null : accessCode); + if (AccessResult.OK.equals(res)) { + SonyAuth.setupCookie(transports); + } else { + return res; + } + } + } + } + + postLogin(); + return AccessResult.OK; + } + + /** + * Get's the access result for the result + * + * @param result a non-null result + * @return a non-null if there is a bad result, null if okay + */ + private static AccessResult getAccessResult(ScalarWebResult result) { + Objects.requireNonNull(result, "result cannot be null"); + + if (result.getDeviceErrorCode() == ScalarWebError.DISPLAYISOFF) { + return AccessResult.DISPLAYOFF; + } + + final int deviceErrorCode = result.getDeviceErrorCode(); + final int httpCode = result.getHttpResponse().getHttpCode(); + + if (deviceErrorCode == ScalarWebError.NOTIMPLEMENTED || deviceErrorCode == ScalarWebError.FORBIDDEN + || httpCode == HttpStatus.UNAUTHORIZED_401 || httpCode == HttpStatus.FORBIDDEN_403) { + return AccessResult.NEEDSPAIRING; + } + + if (httpCode == HttpStatus.OK_200) { + return AccessResult.OK; + } + + return new AccessResult(result.getHttpResponse()); + } + + /** + * Post successful login stuff - set the properties and write out the commands. + * + * @throws ParserConfigurationException the parser configuration exception + * @throws SAXException the SAX exception + * @throws IOException Signals that an I/O exception has occurred. + */ + private void postLogin() throws ParserConfigurationException, SAXException, IOException { + logger.debug("WebScalar System now connected"); + + final ScalarWebService sysService = scalarClient.getService(ScalarWebService.SYSTEM); + Objects.requireNonNull(sysService, "sysService is null - shouldn't happen since it's checked in login()"); + + if (sysService.hasMethod(ScalarWebMethod.GETSYSTEMINFORMATION)) { + final ScalarWebResult sysResult = sysService.execute(ScalarWebMethod.GETSYSTEMINFORMATION); + if (!sysResult.isError() && sysResult.hasResults()) { + final SystemInformation sysInfo = sysResult.as(SystemInformation.class); + callback.setProperty(ScalarWebConstants.PROP_PRODUCT, sysInfo.getProduct()); + callback.setProperty(ScalarWebConstants.PROP_NAME, sysInfo.getName()); + + final String modelName = sysInfo.getModel(); + if (modelName != null && SonyUtil.isValidModelName(modelName)) { + callback.setProperty(ScalarWebConstants.PROP_MODEL, modelName); + } + + callback.setProperty(ScalarWebConstants.PROP_GENERATION, sysInfo.getGeneration()); + callback.setProperty(ScalarWebConstants.PROP_SERIAL, sysInfo.getSerial()); + callback.setProperty(ScalarWebConstants.PROP_MACADDR, sysInfo.getMacAddr()); + callback.setProperty(ScalarWebConstants.PROP_AREA, sysInfo.getArea()); + callback.setProperty(ScalarWebConstants.PROP_REGION, sysInfo.getRegion()); + } + } + + if (sysService.hasMethod(ScalarWebMethod.GETINTERFACEINFORMATION)) { + final ScalarWebResult intResult = sysService.execute(ScalarWebMethod.GETINTERFACEINFORMATION); + if (!intResult.isError() && intResult.hasResults()) { + final InterfaceInformation intInfo = intResult.as(InterfaceInformation.class); + callback.setProperty(ScalarWebConstants.PROP_INTERFACEVERSION, intInfo.getInterfaceVersion()); + callback.setProperty(ScalarWebConstants.PROP_PRODUCTCATEGORY, intInfo.getProductCategory()); + callback.setProperty(ScalarWebConstants.PROP_SERVERNAME, intInfo.getServerName()); + } + } + + if (sysService.hasMethod(ScalarWebMethod.GETNETWORKSETTINGS)) { + for (final String netIf : new String[] { "eth0", "wlan0", "eth1", "wlan1" }) { + final ScalarWebResult swr = sysService.execute(ScalarWebMethod.GETNETWORKSETTINGS, new NetIf(netIf)); + if (!swr.isError() && swr.hasResults()) { + final NetworkSetting netSetting = swr.as(NetworkSetting.class); + callback.setProperty(ScalarWebConstants.PROP_NETIF, netSetting.getNetif()); + callback.setProperty(ScalarWebConstants.PROP_HWADDRESS, netSetting.getHwAddr()); + callback.setProperty(ScalarWebConstants.PROP_IPV4, netSetting.getIpAddrV4()); + callback.setProperty(ScalarWebConstants.PROP_IPV6, netSetting.getIpAddrV6()); + callback.setProperty(ScalarWebConstants.PROP_NETMASK, netSetting.getNetmask()); + callback.setProperty(ScalarWebConstants.PROP_GATEWAY, netSetting.getGateway()); + break; + } + } + } + + writeCommands(sysService); + } + + /** + * Write commands + * + * @param service the non-null service + * @throws ParserConfigurationException the parser configuration exception + * @throws SAXException the SAX exception + * @throws IOException Signals that an I/O exception has occurred. + */ + private void writeCommands(final ScalarWebService service) + throws ParserConfigurationException, SAXException, IOException { + Objects.requireNonNull(service, "service cannot be null"); + + if (transformService == null) { + logger.debug("No MAP transformation service - skipping writing a map file"); + } else { + final String cmdMap = config.getCommandsMapFile(); + if (SonyUtil.isEmpty(cmdMap)) { + logger.debug("No command map defined - ignoring"); + return; + } + + final String filePath = OpenHAB.getConfigFolder() + File.separator + + TransformationService.TRANSFORM_FOLDER_NAME + File.separator + cmdMap; + final Path file = Paths.get(filePath); + if (file.toFile().exists()) { + logger.debug("Command map already defined - ignoring: {}", file); + return; + } + + try { + final Set cmds = new HashSet(); + final List lines = new ArrayList(); + if (service.hasMethod(ScalarWebMethod.GETREMOTECONTROLLERINFO)) { + final RemoteControllerInfo rci = service.execute(ScalarWebMethod.GETREMOTECONTROLLERINFO) + .as(RemoteControllerInfo.class); + + for (final RemoteCommand v : rci.getCommands()) { + // Note: encode value in case it's a URL type + final String name = v.getName(); + final String value = v.getValue(); + if (name != null && value != null && !cmds.contains(name)) { + cmds.add(name); + lines.add(name + "=" + URLEncoder.encode(value, "UTF-8")); + } + } + } else { + logger.debug("No {} method found", ScalarWebMethod.GETREMOTECONTROLLERINFO); + } + + // add any ircc extended commands + final String irccUrl = config.getIrccUrl(); + if (irccUrl != null && !irccUrl.isEmpty()) { + try { + final IrccClient irccClient = IrccClientFactory.get(irccUrl, clientBuilder); + final IrccRemoteCommands remoteCmds = irccClient.getRemoteCommands(); + for (final IrccRemoteCommand v : remoteCmds.getRemoteCommands().values()) { + // Note: encode value in case it's a URL type + final String name = v.getName(); + if (!cmds.contains(name)) { + cmds.add(name); + lines.add(v.getName() + "=" + URLEncoder.encode(v.getCmd(), "UTF-8")); + } + } + } catch (IOException | URISyntaxException e) { + logger.debug("Exception creating IRCC client: {}", e.getMessage(), e); + } + } + Collections.sort(lines, String.CASE_INSENSITIVE_ORDER); + + if (!lines.isEmpty()) { + logger.debug("Writing remote commands to {}", file); + Files.write(file, lines, StandardCharsets.UTF_8); + } + } catch (final IOException e) { + logger.debug("Remote commands are undefined: {}", e.getMessage()); + } + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebProtocol.java new file mode 100644 index 0000000000000..3b3b02e12ab51 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebProtocol.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.util.Collection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.NotifySettingUpdate; +import org.openhab.core.types.Command; + +/** + * The interface definition for all protocols + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface ScalarWebProtocol { + /** + * Gets the channel descriptors + * + * @param dynamicOnly true to retrieve channel descriptors that are defined as dynamic + * @return the channel descriptors + */ + Collection getChannelDescriptors(final boolean dynamicOnly); + + /** + * Refresh state + * + * @param initial true if this is the initial refresh state after going online, false otherwise + */ + void refreshState(final boolean initial); + + /** + * Refresh channel + * + * @param channel the non-null channel + */ + void refreshChannel(final ScalarWebChannel channel); + + /** + * Sets the channel + * + * @param channel the non-null channel + * @param command the non-null command + */ + void setChannel(final ScalarWebChannel channel, final Command command); + + /** + * Get the underlying service + * + * @return a non-null underlying serivce + */ + ScalarWebService getService(); + + /** + * Called when notifying the protocol of a settings update + * + * @param setting a non-null setting + */ + void notifySettingUpdate(final NotifySettingUpdate setting); + + /** + * Defines a close method to release resources. We do NOT implement AutoCloseable since that forces an exception + * onto this method (which we don't need) + */ + void close(); +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebProtocolFactory.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebProtocolFactory.java new file mode 100644 index 0000000000000..067773be547ed --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebProtocolFactory.java @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebClient; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory for creating and managing ScalarWebProtocol objects + * + * @author Tim Roberts - Initial contribution + * @param the generic type of callback + */ +@NonNullByDefault +public class ScalarWebProtocolFactory<@NonNull T extends ThingCallback> implements AutoCloseable { + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebProtocolFactory.class); + + /** The protocols by service name (key is case insensitive) */ + private final Map protocols = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + /** + * Instantiates a new scalar web protocol factory. + * + * @param context the non-null context + * @param client the non-null state + * @param callback the non-null callback + */ + public ScalarWebProtocolFactory(final ScalarWebContext context, final ScalarWebClient client, + final @NonNull T callback) { + for (final ScalarWebService service : client.getDevice().getServices()) { + final String serviceName = service.getServiceName(); + switch (serviceName) { + case ScalarWebService.APPCONTROL: + protocols.put(ScalarWebService.APPCONTROL, + new ScalarWebAppControlProtocol(this, context, service, callback)); + break; + case ScalarWebService.AUDIO: + protocols.put(ScalarWebService.AUDIO, + new ScalarWebAudioProtocol(this, context, service, callback)); + break; + + case ScalarWebService.AVCONTENT: + protocols.put(ScalarWebService.AVCONTENT, + new ScalarWebAvContentProtocol(this, context, service, callback)); + break; + + case ScalarWebService.BROWSER: + protocols.put(ScalarWebService.BROWSER, + new ScalarWebBrowserProtocol(this, context, service, callback)); + break; + + case ScalarWebService.CEC: + protocols.put(ScalarWebService.CEC, new ScalarWebCecProtocol(this, context, service, callback)); + break; + + case ScalarWebService.ILLUMINATION: + protocols.put(ScalarWebService.ILLUMINATION, + new ScalarWebIlluminationProtocol(this, context, service, callback)); + break; + + case ScalarWebService.SYSTEM: + protocols.put(ScalarWebService.SYSTEM, new ScalarWebSystemProtocol(this, context, service, + callback, context.getConfig().getIrccUrl())); + break; + + case ScalarWebService.VIDEO: + protocols.put(ScalarWebService.VIDEO, + new ScalarWebVideoProtocol(this, context, service, callback)); + break; + + case ScalarWebService.VIDEOSCREEN: + protocols.put(ScalarWebService.VIDEOSCREEN, + new ScalarWebVideoScreenProtocol(this, context, service, callback)); + break; + + case ScalarWebService.ACCESSCONTROL: + case ScalarWebService.GUIDE: + case ScalarWebService.ENCRYPTION: + case ScalarWebService.NOTIFICATION: + case ScalarWebService.RECORDING: + protocols.put(serviceName, new ScalarWebGenericProtocol(this, context, service, callback)); + logger.trace("Generic protocol for {} service (no specific protocol written)", serviceName); + break; + + default: + logger.debug("No protocol found for service {}", serviceName); + break; + } + } + } + + /** + * Gets the protocol for the given name + * + * @param name the service name + * @return the protocol or null if not found + */ + public @Nullable ScalarWebProtocol getProtocol(final @Nullable String name) { + if (name == null || name.isEmpty()) { + return null; + } + return protocols.get(name); + } + + /** + * Gets the channel descriptors for all protocols + * + * @return the non-null, possibly empty channel descriptors + */ + public Collection getChannelDescriptors() { + return getChannelDescriptors(false); + } + + /** + * Gets the channel descriptors for all or only dynamic channels + * + * @param dynamicOnly whether to get dynamic only channels + * @return the non-null, possibly empty channel descriptors + */ + public Collection getChannelDescriptors(final boolean dynamicOnly) { + return protocols.values().stream().flatMap(p -> p.getChannelDescriptors(dynamicOnly).stream()) + .collect(Collectors.toList()); + } + + /** + * Refresh all state in all services + * + * @param scheduler the non-null scheduler to schedule refresh on each service + * @param initial true if this is the initial refresh state after going online, false otherwise + */ + public void refreshAllState(final ScheduledExecutorService scheduler, boolean initial) { + Objects.requireNonNull(scheduler, "scheduler cannot be null"); + logger.debug("Scheduling refreshState on all protocols"); + protocols.values().stream().forEach(p -> { + final ScalarWebProtocol myProtocol = p; + scheduler.execute(() -> { + logger.debug("Executing refreshState on {}", myProtocol); + myProtocol.refreshState(initial); + }); + }); + } + + @Override + public void close() { + protocols.values().stream().forEach(p -> p.close()); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebSystemProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebSystemProtocol.java new file mode 100644 index 0000000000000..b9fee7afb392b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebSystemProtocol.java @@ -0,0 +1,1126 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.ircc.IrccClientFactory; +import org.openhab.binding.sony.internal.ircc.models.IrccClient; +import org.openhab.binding.sony.internal.net.HttpResponse; +import org.openhab.binding.sony.internal.net.HttpResponse.SOAPError; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelTracker; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.VersionUtilities; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebEvent; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.CurrentTime; +import org.openhab.binding.sony.internal.scalarweb.models.api.Language; +import org.openhab.binding.sony.internal.scalarweb.models.api.LedIndicatorStatus; +import org.openhab.binding.sony.internal.scalarweb.models.api.NotifySettingUpdate; +import org.openhab.binding.sony.internal.scalarweb.models.api.PostalCode; +import org.openhab.binding.sony.internal.scalarweb.models.api.PowerSavingMode; +import org.openhab.binding.sony.internal.scalarweb.models.api.PowerStatusRequest_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.PowerStatusRequest_1_1; +import org.openhab.binding.sony.internal.scalarweb.models.api.PowerStatusResult_1_0; +import org.openhab.binding.sony.internal.scalarweb.models.api.PowerStatusResult_1_1; +import org.openhab.binding.sony.internal.scalarweb.models.api.Scheme; +import org.openhab.binding.sony.internal.scalarweb.models.api.SoftwareUpdate; +import org.openhab.binding.sony.internal.scalarweb.models.api.Source; +import org.openhab.binding.sony.internal.scalarweb.models.api.StorageListItem_1_1; +import org.openhab.binding.sony.internal.scalarweb.models.api.StorageListItem_1_2; +import org.openhab.binding.sony.internal.scalarweb.models.api.StorageListRequest_1_1; +import org.openhab.binding.sony.internal.scalarweb.models.api.StorageListRequest_1_2; +import org.openhab.binding.sony.internal.scalarweb.models.api.SystemInformation; +import org.openhab.binding.sony.internal.scalarweb.models.api.WolMode; +import org.openhab.binding.sony.internal.transports.SonyHttpTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.MetricPrefix; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.transform.TransformationException; +import org.openhab.core.transform.TransformationService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The implementation of the protocol handles the System service + * + * @author Tim Roberts - Initial contribution + * @param the generic type for the callback + */ +@NonNullByDefault +public class ScalarWebSystemProtocol<@NonNull T extends ThingCallback> extends AbstractScalarWebProtocol { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebSystemProtocol.class); + + // Constants used by protocol + private static final String CURRENTTIME = "currenttime"; + private static final String LEDINDICATORSTATUS = "ledindicatorstatus"; + private static final String POWERSAVINGMODE = "powersavingmode"; + public static final String POWERSTATUS = "powerstatus"; + private static final String WOLMODE = "wolmode"; + private static final String LANGUAGE = "language"; + private static final String REBOOT = "reboot"; + private static final String SYSCMD = "sysCmd"; + private static final String POSTALCODE = "postalcode"; + private static final String DEVICEMISCSETTING = "devicemiscsettings"; + private static final String POWERSETTINGS = "powersettings"; + private static final String SLEEPSETTINGS = "sleepsettings"; + private static final String WUTANGSETTINGS = "wutangsettings"; + + private static final String STORAGE = "st_"; + private static final String ST_DEVICENAME = STORAGE + "deviceName"; + private static final String ST_ERROR = STORAGE + "error"; + private static final String ST_FILESYSTEM = STORAGE + "fileSystem"; + private static final String ST_FINALIZESTATUS = STORAGE + "finalizeStatus"; + private static final String ST_FORMAT = STORAGE + "format"; + private static final String ST_FORMATSTATUS = STORAGE + "formatStatus"; + private static final String ST_FORMATTABLE = STORAGE + "formattable"; + private static final String ST_FORMATTING = STORAGE + "formatting"; + private static final String ST_FREECAPACITYMB = STORAGE + "freeCapacityMB"; + private static final String ST_HASNONSTANDARDDATA = STORAGE + "hasNonStandardData"; + private static final String ST_HASUNSUPPORTEDCONTENTS = STORAGE + "hasUnsupportedContents"; + private static final String ST_ISAVAILABLE = STORAGE + "isAvailable"; + private static final String ST_ISLOCKED = STORAGE + "isLocked"; + private static final String ST_ISMANAGEMENTINFOFULL = STORAGE + "isManagementInfoFull"; + private static final String ST_ISPROTECTED = STORAGE + "isProtected"; + private static final String ST_ISREGISTERED = STORAGE + "isRegistered"; + private static final String ST_ISSELFRECORDED = STORAGE + "isSelfRecorded"; + private static final String ST_ISSQVSUPPORTED = STORAGE + "isSqvSupported"; + private static final String ST_LUN = STORAGE + "lun"; + private static final String ST_MOUNTED = STORAGE + "mounted"; + private static final String ST_PERMISSION = STORAGE + "permission"; + private static final String ST_POSITION = STORAGE + "position"; + private static final String ST_PROTOCOL = STORAGE + "protocol"; + private static final String ST_REGISTRATIONDATE = STORAGE + "registrationDate"; + private static final String ST_SYSTEMAREACAPACITYMB = STORAGE + "systemAreaCapacityMB"; + private static final String ST_TIMESECTOFINALIZE = STORAGE + "timeSecToFinalize"; + private static final String ST_TIMESECTOGETCONTENTS = STORAGE + "timeSecToGetContents"; + private static final String ST_TYPE = STORAGE + "type"; + private static final String ST_URI = STORAGE + "uri"; + private static final String ST_USBDEVICETYPE = STORAGE + "usbDeviceType"; + private static final String ST_VOLUMELABEL = STORAGE + "volumeLabel"; + private static final String ST_WHOLECAPACITYMB = STORAGE + "wholeCapacityMB"; + + /** The url for the IRCC service */ + private final @Nullable String irccUrl; + + /** The notifications that are enabled */ + private final NotificationHelper notificationHelper; + + /** + * Instantiates a new scalar web system protocol. + * + * @param factory the non-null factory + * @param context the non-null context + * @param service the non-null service + * @param callback the non-null callback + * @param irccUrl the possibly null, possibly empty ircc url + */ + ScalarWebSystemProtocol(final ScalarWebProtocolFactory factory, final ScalarWebContext context, + final ScalarWebService service, final @NonNull T callback, final @Nullable String irccUrl) { + super(factory, context, service, callback); + + this.irccUrl = irccUrl; + + notificationHelper = new NotificationHelper( + enableNotifications(ScalarWebEvent.NOTIFYPOWERSTATUS, ScalarWebEvent.NOTIFYSTORAGESTATUS, + ScalarWebEvent.NOTIFYSETTINGSUPDATE, ScalarWebEvent.NOTIFYSWUPDATEINFO)); + } + + @Override + public Collection getChannelDescriptors(final boolean dynamicOnly) { + final List descriptors = new ArrayList(); + + // no dynamic channels + if (dynamicOnly) { + return descriptors; + } + + if (getService().hasMethod(ScalarWebMethod.GETCURRENTTIME)) { + try { + execute(ScalarWebMethod.GETCURRENTTIME); + descriptors.add(createDescriptor(createChannel(CURRENTTIME), "DateTime", "scalarsystemcurrenttime")); + } catch (final IOException e) { + logger.debug("Exception getting current time: {}", e.getMessage()); + } + } + + if (getService().hasMethod(ScalarWebMethod.GETSYSTEMINFORMATION)) { + try { + execute(ScalarWebMethod.GETSYSTEMINFORMATION); + descriptors.add(createDescriptor(createChannel(LANGUAGE), "String", "scalarsystemlanguage")); + } catch (final IOException e) { + logger.debug("Exception getting system information: {}", e.getMessage()); + } + } + + if (getService().hasMethod(ScalarWebMethod.GETLEDINDICATORSTATUS)) { + try { + execute(ScalarWebMethod.GETLEDINDICATORSTATUS); + descriptors.add(createDescriptor(createChannel(LEDINDICATORSTATUS), "String", + "scalarsystemledindicatorstatus")); + } catch (final IOException e) { + logger.debug("Exception getting led indicator status: {}", e.getMessage()); + } + } + + if (getService().hasMethod(ScalarWebMethod.GETPOWERSAVINGMODE)) { + try { + execute(ScalarWebMethod.GETPOWERSAVINGMODE); + descriptors + .add(createDescriptor(createChannel(POWERSAVINGMODE), "String", "scalarsystempowersavingmode")); + } catch (final IOException e) { + logger.debug("Exception getting power savings mode: {}", e.getMessage()); + } + } + + if (getService().hasMethod(ScalarWebMethod.GETPOWERSTATUS)) { + try { + execute(ScalarWebMethod.GETPOWERSTATUS); + descriptors.add(createDescriptor(createChannel(POWERSTATUS), "Switch", "scalarsystempowerstatus")); + } catch (final IOException e) { + logger.debug("Exception getting power status: {}", e.getMessage()); + } + } + + if (getService().hasMethod(ScalarWebMethod.GETWOLMODE)) { + try { + execute(ScalarWebMethod.GETWOLMODE); + descriptors.add(createDescriptor(createChannel(WOLMODE), "Switch", "scalarsystemwolmode")); + } catch (final IOException e) { + logger.debug("Exception getting wol mode: {}", e.getMessage()); + } + } + + if (getService().hasMethod(ScalarWebMethod.GETPOSTALCODE)) { + try { + execute(ScalarWebMethod.GETPOSTALCODE); + descriptors.add(createDescriptor(createChannel(POSTALCODE), "String", "scalarsystempostalcode")); + } catch (final IOException e) { + logger.debug("Exception getting postal code: {}", e.getMessage()); + } + } + + if (service.hasMethod(ScalarWebMethod.REQUESTREBOOT)) { + descriptors.add(createDescriptor(createChannel(REBOOT), "Switch", "scalarsystemreboot")); + } + + // IRCC should be available by default + descriptors.add(createDescriptor(createChannel(SYSCMD), "String", "scalarsystemircc")); + + if (service.hasMethod(ScalarWebMethod.GETDEVICEMISCSETTINGS)) { + addGeneralSettingsDescriptor(descriptors, ScalarWebMethod.GETDEVICEMISCSETTINGS, DEVICEMISCSETTING, + "Device Misc Setting"); + } + + if (service.hasMethod(ScalarWebMethod.GETPOWERSETTINGS)) { + addGeneralSettingsDescriptor(descriptors, ScalarWebMethod.GETPOWERSETTINGS, POWERSETTINGS, "Power Setting"); + } + + if (service.hasMethod(ScalarWebMethod.GETSLEEPTIMERSETTINGS)) { + addGeneralSettingsDescriptor(descriptors, ScalarWebMethod.GETSLEEPTIMERSETTINGS, SLEEPSETTINGS, + "Sleep Timer Settings"); + } + + if (service.hasMethod(ScalarWebMethod.GETWUTANGINFO)) { + addGeneralSettingsDescriptor(descriptors, ScalarWebMethod.GETWUTANGINFO, WUTANGSETTINGS, "WuTang Settings"); + } + + if (service.hasMethod(ScalarWebMethod.GETSTORAGELIST)) { + try { + addStorageListDescriptors(descriptors); + } catch (final IOException e) { + logger.debug("Exception getting storage list descriptors: {}", e.getMessage(), e); + } + } + + return descriptors; + } + + /** + * Add the storage list descriptors + * + * @param descriptors a non-null, possibly empty list of descriptors to add too + * @throws IOException if an IO exception occurs + */ + private void addStorageListDescriptors(final List descriptors) throws IOException { + Objects.requireNonNull(descriptors, "descriptors cannot be null"); + + final String version = getService().getVersion(ScalarWebMethod.GETSTORAGELIST); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_1)) { + for (final StorageListItem_1_1 sl : execute(ScalarWebMethod.GETSTORAGELIST, new StorageListRequest_1_1()) + .asArray(StorageListItem_1_1.class)) { + final String uri = sl.getUri(); + if (uri == null || uri.isEmpty()) { + logger.debug("Storage List had no URI (which is required): {}", sl); + continue; + } + + final String sourcePart = Source.getSourcePart(uri).toUpperCase(); + final String id = getStorageChannelId(uri); + + descriptors.add(createDescriptor(createChannel(ST_DEVICENAME, id), "String", + "scalarsystemstoragedevicename", "Storage Name (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_ERROR, id), "String", "scalarsystemstorageerror", + "Storage Error (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_FORMAT, id), "String", "scalarsystemstorageformat", + "Storage Format (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_FORMATTABLE, id), "String", + "scalarsystemstorageformattable", "Storage Formattable Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_FORMATTING, id), "String", + "scalarsystemstorageformatting", "Storage Formatting Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_FREECAPACITYMB, id), "Number:DataAmount", + "scalarsystemstoragefreecapacitymb", "Storage Free Capacity (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_ISAVAILABLE, id), "String", + "scalarsystemstorageisavailable", "Storage Available Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_LUN, id), "Number", "scalarsystemstoragelun", + "Storage LUN (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_MOUNTED, id), "String", "scalarsystemstoragemounted", + "Storage Mount Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_PERMISSION, id), "String", + "scalarsystemstoragepermission", "Storage Permission Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_POSITION, id), "String", + "scalarsystemstorageposition", "Storage Position (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_SYSTEMAREACAPACITYMB, id), "Number:DataAmount", + "scalarsystemstoragesystemareacapacitymb", "Storage System Capacity (" + sourcePart + ")", + null)); + descriptors.add(createDescriptor(createChannel(ST_TYPE, id), "String", "scalarsystemstoragetype", + "Storage Type (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_URI, id), "String", "scalarsystemstorageuri", + "Storage URI (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_VOLUMELABEL, id), "String", + "scalarsystemstoragevolumelabel", "Storage Label (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_WHOLECAPACITYMB, id), "Number:DataAmount", + "scalarsystemstoragewholecapacitymb", "Storage Whole Capacity (" + sourcePart + ")", null)); + } + } else { + for (final StorageListItem_1_2 sl : execute(ScalarWebMethod.GETSTORAGELIST, new StorageListRequest_1_2()) + .asArray(StorageListItem_1_2.class)) { + final String uri = sl.getUri(); + if (uri == null || uri.isEmpty()) { + logger.debug("Storage List had no URI (which is required): {}", sl); + continue; + } + + final String sourcePart = Source.getSourcePart(uri).toUpperCase(); + final String id = getStorageChannelId(uri); + + descriptors.add(createDescriptor(createChannel(ST_DEVICENAME, id), "String", + "scalarsystemstoragedevicename", "Storage Name (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_ERROR, id), "String", "scalarsystemstorageerror", + "Storage Error (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_FILESYSTEM, id), "String", + "scalarsystemstoragefilesystem", "Storage File Size (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_FINALIZESTATUS, id), "String", + "scalarsystemstoragefinalizestatus", "Storage Finalize Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_FORMAT, id), "String", "scalarsystemstorageformat", + "Storage Format (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_FORMATSTATUS, id), "String", + "scalarsystemstorageformatstatus", "Storage Format Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_FORMATTABLE, id), "String", + "scalarsystemstorageformattable", "Storage Formattable Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_FREECAPACITYMB, id), "Number:DataAmount", + "scalarsystemstoragefreecapacitymb", "Storage Free Capacity (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_HASNONSTANDARDDATA, id), "String", + "scalarsystemstoragehasnonstandarddata", "Storage Non-standard Data (" + sourcePart + ")", + null)); + descriptors.add(createDescriptor(createChannel(ST_HASUNSUPPORTEDCONTENTS, id), "String", + "scalarsystemstoragehasunsupportedcontents", + "Storage Unsupported Contents (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_ISAVAILABLE, id), "String", + "scalarsystemstorageisavailable", "Storage Available Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_ISLOCKED, id), "String", + "scalarsystemstorageislocked", "Storage Locked Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_ISMANAGEMENTINFOFULL, id), "String", + "scalarsystemstorageismanagementinfofull", + "Storage Management Info Full Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_ISPROTECTED, id), "String", + "scalarsystemstorageisprotected", "Storage Protection Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_ISREGISTERED, id), "String", + "scalarsystemstorageisregistered", "Storage Registered Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_ISSELFRECORDED, id), "String", + "scalarsystemstorageisselfrecorded", "Storage Self Recorded Status (" + sourcePart + ")", + null)); + descriptors.add(createDescriptor(createChannel(ST_ISSQVSUPPORTED, id), "String", + "scalarsystemstorageissqvsupported", "Storage SQV Supported Status (" + sourcePart + ")", + null)); + descriptors.add(createDescriptor(createChannel(ST_LUN, id), "Number", "scalarsystemstoragelun", + "Storage LUN (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_MOUNTED, id), "String", "scalarsystemstoragemounted", + "Storage Mount Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_PERMISSION, id), "String", + "scalarsystemstoragepermission", "Storage Permission Status (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_POSITION, id), "String", + "scalarsystemstorageposition", "Storage Position (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_PROTOCOL, id), "String", + "scalarsystemstorageprotocol", "Storage Protocol (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_REGISTRATIONDATE, id), "String", + "scalarsystemstorageregistrationdate", "Storage Registration Date (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_SYSTEMAREACAPACITYMB, id), "Number:DataAmount", + "scalarsystemstoragesystemareacapacitymb", "Storage System Capacity (" + sourcePart + ")", + null)); + descriptors.add(createDescriptor(createChannel(ST_TIMESECTOFINALIZE, id), "Number:Time", + "scalarsystemstoragetimesectofinalize", "Storage Finalization Time (" + sourcePart + ")", + null)); + descriptors.add(createDescriptor(createChannel(ST_TIMESECTOGETCONTENTS, id), "Number:Time", + "scalarsystemstoragetimesectogetcontents", "Storage Get Contents Time (" + sourcePart + ")", + null)); + descriptors.add(createDescriptor(createChannel(ST_TYPE, id), "String", "scalarsystemstoragetype", + "Storage Type (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_URI, id), "String", "scalarsystemstorageuri", + "Storage URI (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_USBDEVICETYPE, id), "String", + "scalarsystemstorageusbdevicetype", "Storage USB Type (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_VOLUMELABEL, id), "String", + "scalarsystemstoragevolumelabel", "Storage Label (" + sourcePart + ")", null)); + descriptors.add(createDescriptor(createChannel(ST_WHOLECAPACITYMB, id), "Number:DataAmount", + "scalarsystemstoragewholecapacitymb", "Storage Whole Capacity (" + sourcePart + ")", null)); + } + } + } + + /** + * Gets the channel ID from a given storage URI (commonly 'storage:xxx'). If the scheme is "storage", simply use the + * source part. If the scheme is not storage - then reformat to 'scheme-src' + * + * @param uri a non-null, non-empty URI + * @return a channel id for the uri + */ + private String getStorageChannelId(final String uri) { + SonyUtil.validateNotEmpty(uri, "uri cannot be empty"); + final String scheme = Source.getSchemePart(uri); + final String src = Source.getSourcePart(uri); + + return SonyUtil.createValidChannelUId((Scheme.STORAGE.equalsIgnoreCase(scheme) ? "" : (scheme + "-")) + src); + } + + @Override + public void refreshState(boolean initial) { + final ScalarWebChannelTracker tracker = getChannelTracker(); + if (tracker.isCategoryLinked(CURRENTTIME)) { + refreshCurrentTime(); + } + if (tracker.isCategoryLinked(LEDINDICATORSTATUS)) { + refreshLedIndicator(); + } + if (tracker.isCategoryLinked(LANGUAGE)) { + refreshLanguage(); + } + if (tracker.isCategoryLinked(POWERSAVINGMODE)) { + refreshPowerSavingsMode(); + } + + if (initial || !notificationHelper.isEnabled(ScalarWebEvent.NOTIFYPOWERSTATUS)) { + if (tracker.isCategoryLinked(POWERSTATUS)) { + refreshPowerStatus(); + } + } + if (tracker.isCategoryLinked(WOLMODE)) { + refreshWolMode(); + } + if (tracker.isCategoryLinked(REBOOT)) { + refreshReboot(); + } + if (tracker.isCategoryLinked(POSTALCODE)) { + refreshPostalCode(); + } + if (tracker.isCategoryLinked(SYSCMD)) { + refreshSysCmd(); + } + + if (initial || !notificationHelper.isEnabled(ScalarWebEvent.NOTIFYSETTINGSUPDATE)) { + if (tracker.isCategoryLinked(DEVICEMISCSETTING)) { + refreshGeneralSettings(tracker.getLinkedChannelsForCategory(DEVICEMISCSETTING), + ScalarWebMethod.GETDEVICEMISCSETTINGS); + } + if (tracker.isCategoryLinked(POWERSETTINGS)) { + refreshGeneralSettings(tracker.getLinkedChannelsForCategory(POWERSETTINGS), + ScalarWebMethod.GETPOWERSETTINGS); + } + if (tracker.isCategoryLinked(SLEEPSETTINGS)) { + refreshGeneralSettings(tracker.getLinkedChannelsForCategory(SLEEPSETTINGS), + ScalarWebMethod.GETSLEEPTIMERSETTINGS); + } + if (tracker.isCategoryLinked(WUTANGSETTINGS)) { + refreshGeneralSettings(tracker.getLinkedChannelsForCategory(WUTANGSETTINGS), + ScalarWebMethod.GETWUTANGINFO); + } + } + + if (initial || !notificationHelper.isEnabled(ScalarWebEvent.NOTIFYSTORAGESTATUS)) { + if (tracker.isCategoryLinked(ctgy -> ctgy.startsWith(STORAGE))) { + refreshStorage(); + } + } + } + + @Override + public void refreshChannel(final ScalarWebChannel channel) { + Objects.requireNonNull(channel, "channel cannot be null"); + + switch (channel.getCategory()) { + case CURRENTTIME: + refreshCurrentTime(); + break; + + case LANGUAGE: + refreshLanguage(); + break; + + case LEDINDICATORSTATUS: + refreshLedIndicator(); + break; + + case POWERSAVINGMODE: + refreshPowerSavingsMode(); + break; + + case POWERSTATUS: + refreshPowerStatus(); + break; + + case WOLMODE: + refreshWolMode(); + break; + + case REBOOT: + refreshReboot(); + break; + + case POSTALCODE: + refreshPostalCode(); + break; + + case SYSCMD: + refreshSysCmd(); + break; + + case DEVICEMISCSETTING: + refreshGeneralSettings(Collections.singleton(channel), ScalarWebMethod.GETDEVICEMISCSETTINGS); + break; + + case POWERSETTINGS: + refreshGeneralSettings(Collections.singleton(channel), ScalarWebMethod.GETPOWERSETTINGS); + break; + + case SLEEPSETTINGS: + refreshGeneralSettings(Collections.singleton(channel), ScalarWebMethod.GETSLEEPTIMERSETTINGS); + break; + + case WUTANGSETTINGS: + refreshGeneralSettings(Collections.singleton(channel), ScalarWebMethod.GETWUTANGINFO); + break; + + default: + final String ctgy = channel.getCategory(); + if (ctgy.startsWith(STORAGE)) { + refreshStorage(); + } else { + logger.debug("Unknown refresh channel: {}", channel); + } + break; + } + } + + /** + * Refresh current time + */ + private void refreshCurrentTime() { + try { + final CurrentTime ct = execute(ScalarWebMethod.GETCURRENTTIME).as(CurrentTime.class); + stateChanged(CURRENTTIME, new DateTimeType(ct.getDateTime())); + } catch (final IOException e) { + logger.debug("Cannot get the current time: {}", e.getMessage()); + } + } + + /** + * Refresh the language + */ + private void refreshLanguage() { + try { + final SystemInformation sysInfo = execute(ScalarWebMethod.GETSYSTEMINFORMATION).as(SystemInformation.class); + stateChanged(LANGUAGE, SonyUtil.newStringType(sysInfo.getLanguage())); + } catch (final IOException e) { + logger.debug("Cannot get the get system information for refresh langauge: {}", e.getMessage()); + } + } + + /** + * Refresh led indicator + */ + private void refreshLedIndicator() { + try { + final LedIndicatorStatus ledStatus = execute(ScalarWebMethod.GETLEDINDICATORSTATUS) + .as(LedIndicatorStatus.class); + stateChanged(LEDINDICATORSTATUS, SonyUtil.newStringType(ledStatus.getMode())); + } catch (final IOException e) { + logger.debug("Cannot get the get led indicator status: {}", e.getMessage()); + } + } + + /** + * Refresh power savings mode + */ + private void refreshPowerSavingsMode() { + try { + final PowerSavingMode mode = execute(ScalarWebMethod.GETPOWERSAVINGMODE).as(PowerSavingMode.class); + stateChanged(POWERSAVINGMODE, SonyUtil.newStringType(mode.getMode())); + } catch (final IOException e) { + logger.debug("Cannot get the get power savings mode: {}", e.getMessage()); + } + } + + /** + * Refresh postal code + */ + private void refreshPostalCode() { + try { + final PostalCode postalCode = execute(ScalarWebMethod.GETPOSTALCODE).as(PostalCode.class); + stateChanged(POSTALCODE, SonyUtil.newStringType(postalCode.getPostalCode())); + } catch (final IOException e) { + logger.debug("Cannot get the get postal code: {}", e.getMessage()); + } + } + + /** + * Refresh power status + */ + private void refreshPowerStatus() { + try { + if (VersionUtilities.equals(getVersion(ScalarWebMethod.GETPOWERSTATUS), ScalarWebMethod.V1_0)) { + notifyPowerStatus(execute(ScalarWebMethod.GETPOWERSTATUS).as(PowerStatusResult_1_0.class)); + } else { + notifyPowerStatus(execute(ScalarWebMethod.GETPOWERSTATUS).as(PowerStatusResult_1_1.class)); + } + } catch (final IOException e) { + logger.debug("Cannot refresh the power status: {}", e.getMessage()); + } + } + + /** + * Refresh wol mode + */ + private void refreshWolMode() { + try { + final WolMode mode = execute(ScalarWebMethod.GETWOLMODE).as(WolMode.class); + stateChanged(WOLMODE, mode.isEnabled() ? OnOffType.ON : OnOffType.OFF); + } catch (final IOException e) { + logger.debug("Cannot get the get WOL mode: {}", e.getMessage()); + } + } + + /** + * Refresh reboot + */ + private void refreshReboot() { + callback.stateChanged(REBOOT, OnOffType.OFF); + } + + /** + * Refresh system command + */ + private void refreshSysCmd() { + callback.stateChanged(SYSCMD, StringType.EMPTY); + } + + /** + * Refresh the playing content info + */ + private void refreshStorage() { + try { + final String version = getService().getVersion(ScalarWebMethod.GETSTORAGELIST); + if (VersionUtilities.equals(version, ScalarWebMethod.V1_1)) { + execute(ScalarWebMethod.GETSTORAGELIST, new StorageListRequest_1_1()).asArray(StorageListItem_1_1.class) + .forEach(sl -> notifyStorageStatus(sl)); + } else { + execute(ScalarWebMethod.GETSTORAGELIST, new StorageListRequest_1_2()).asArray(StorageListItem_1_2.class) + .forEach(sl -> notifyStorageStatus(sl)); + } + } catch (final IOException e) { + logger.debug("Error refreshing playing content info {}", e.getMessage()); + } + } + + @Override + public void setChannel(final ScalarWebChannel channel, final Command command) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + switch (channel.getCategory()) { + case LEDINDICATORSTATUS: + if (command instanceof StringType) { + setLedIndicatorStatus(command.toString()); + } else { + logger.debug("LEDINDICATORSTATUS command not an StringType: {}", command); + } + + break; + + case LANGUAGE: + if (command instanceof StringType) { + setLanguage(command.toString()); + } else { + logger.debug("LANGUAGE command not an StringType: {}", command); + } + + break; + + case POWERSAVINGMODE: + if (command instanceof StringType) { + setPowerSavingMode(command.toString()); + } else { + logger.debug("POWERSAVINGMODE command not an StringType: {}", command); + } + + break; + + case POWERSTATUS: + if (command instanceof OnOffType) { + setPowerStatus(command == OnOffType.ON); + } else { + logger.debug("POWERSTATUS command not an OnOffType: {}", command); + } + + break; + + case WOLMODE: + if (command instanceof OnOffType) { + setWolMode(command == OnOffType.ON); + } else { + logger.debug("WOLMODE command not an OnOffType: {}", command); + } + + break; + + case REBOOT: + if (command instanceof OnOffType && command == OnOffType.ON) { + requestReboot(); + } else { + logger.debug("REBOOT command not an OnOffType: {}", command); + } + + break; + + case POSTALCODE: + if (command instanceof StringType) { + setPostalCode(command.toString()); + } else { + logger.debug("POSTALCODE command not an StringType: {}", command); + } + + break; + + case SYSCMD: + if (command instanceof StringType) { + sendIrccCommand(command.toString()); + } else { + logger.debug("SYSCMD command not an StringType: {}", command); + } + + break; + + case DEVICEMISCSETTING: + setGeneralSetting(ScalarWebMethod.SETDEVICEMISSETTINGS, channel, command); + break; + + case POWERSETTINGS: + setGeneralSetting(ScalarWebMethod.SETPOWERSETTINGS, channel, command); + break; + + case SLEEPSETTINGS: + setGeneralSetting(ScalarWebMethod.SETSLEEPTIMERSETTINGS, channel, command); + break; + + case WUTANGSETTINGS: + setGeneralSetting(ScalarWebMethod.SETWUTANGINFO, channel, command); + break; + + default: + logger.debug("Unhandled channel command: {} - {}", channel, command); + break; + } + } + + /** + * Sets the led indicator status + * + * @param mode the non-null, non-empty new led indicator status + */ + private void setLedIndicatorStatus(final String mode) { + SonyUtil.validateNotEmpty(mode, "mode cannot be empty"); + handleExecute(ScalarWebMethod.SETLEDINDICATORSTATUS, new LedIndicatorStatus(mode, "")); + } + + /** + * Sets the language + * + * @param language the non-null, non-empty new language + */ + private void setLanguage(final String language) { + SonyUtil.validateNotEmpty(language, "language cannot be empty"); + handleExecute(ScalarWebMethod.SETLANGUAGE, new Language(language)); + } + + /** + * Sets the power saving mode + * + * @param mode the non-null, non-empty new power saving mode + */ + private void setPowerSavingMode(final String mode) { + SonyUtil.validateNotEmpty(mode, "mode cannot be empty"); + handleExecute(ScalarWebMethod.SETPOWERSAVINGMODE, new PowerSavingMode(mode)); + } + + /** + * Sets the power status + * + * @param status true for on, false otherwise + */ + private void setPowerStatus(final boolean status) { + if (status) { + SonyUtil.sendWakeOnLan(logger, getContext().getConfig().getDeviceIpAddress(), + getContext().getConfig().getDeviceMacAddress()); + } + + handleExecute(ScalarWebMethod.SETPOWERSTATUS, version -> { + if (VersionUtilities.equals(version, ScalarWebMethod.V1_0)) { + return new PowerStatusRequest_1_0(status); + } + return new PowerStatusRequest_1_1(status); + }); + } + + /** + * Sets the wol mode + * + * @param enabled true to enable WOL, false otherwise + */ + private void setWolMode(final boolean enabled) { + handleExecute(ScalarWebMethod.SETWOLMODE, new WolMode(enabled)); + } + + /** + * Sets the postal code + * + * @param postalCode the non-null, non-empty new postal code + */ + private void setPostalCode(final String postalCode) { + SonyUtil.validateNotEmpty(postalCode, "postalCode cannot be empty"); + handleExecute(ScalarWebMethod.SETPOSTALCODE, new PostalCode(postalCode)); + } + + /** + * Request reboot + */ + private void requestReboot() { + handleExecute(ScalarWebMethod.REQUESTREBOOT); + } + + /** + * Send an IRCC command + * + * @param cmd a possibly null, possibly empty IRCC command to send + */ + public void sendIrccCommand(final String cmd) { + if (SonyUtil.isEmpty(cmd)) { + return; + } + + final String localIrccUrl = irccUrl; + if (localIrccUrl == null || localIrccUrl.isEmpty()) { + logger.debug("IRCC URL was not specified in configuration"); + } else { + try { + final IrccClient irccClient = IrccClientFactory.get(localIrccUrl, getContext().getClientBuilder()); + final ScalarWebContext context = getContext(); + String localCmd = cmd; + + final String cmdMap = context.getConfig().getCommandsMapFile(); + + final TransformationService localTransformService = context.getTransformService(); + if (localTransformService != null && cmdMap != null) { + String code; + try { + code = localTransformService.transform(cmdMap, cmd); + + if (code != null && !code.isBlank()) { + logger.debug("Transformed {} with map file '{}' to {}", cmd, cmdMap, code); + + try { + localCmd = URLDecoder.decode(code, "UTF-8"); + } catch (final UnsupportedEncodingException e) { + localCmd = code; + } + } + } catch (final TransformationException e) { + logger.debug("Failed to transform {} using map file '{}', exception={}", cmd, cmdMap, + e.getMessage()); + return; + } + } + + if (localCmd == null || localCmd.isEmpty()) { + logger.debug("IRCC command was empty or null - ignoring"); + return; + } + + // Always use an http transport to execute soap + HttpResponse httpResponse; + try (final SonyHttpTransport httpTransport = SonyTransportFactory.createHttpTransport( + irccClient.getBaseUrl().toExternalForm(), getContext().getClientBuilder())) { + // copy all the options from the parent one (authentication options) + getService().getTransport().getOptions().stream().forEach(o -> httpTransport.setOption(o)); + httpResponse = irccClient.executeSoap(httpTransport, localCmd); + } catch (final URISyntaxException e) { + logger.debug("URI syntax exception: {}", e.getMessage()); + return; + } + + switch (httpResponse.getHttpCode()) { + case HttpStatus.OK_200: + // everything is great! + break; + + case HttpStatus.SERVICE_UNAVAILABLE_503: + logger.debug("IRCC service is unavailable (power off?)"); + break; + + case HttpStatus.FORBIDDEN_403: + logger.debug("IRCC methods have been forbidden on service {} ({}): {}", + service.getServiceName(), irccClient.getBaseUrl(), httpResponse); + break; + + case HttpStatus.INTERNAL_SERVER_ERROR_500: + final SOAPError soapError = httpResponse.getSOAPError(); + if (soapError == null) { + final IOException e = httpResponse.createException(); + logger.debug("Communication error for IRCC method on service {} ({}): {}", + service.getServiceName(), irccClient.getBaseUrl(), e.getMessage(), e); + callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + e.getMessage()); + break; + } else { + logger.debug("SOAP Error: ({}) {}", soapError.getSoapCode(), + soapError.getSoapDescription()); + } + break; + + default: + final IOException e = httpResponse.createException(); + logger.debug("Communication error for IRCC method on service {} ({}): {}", + service.getServiceName(), irccClient.getBaseUrl(), e.getMessage(), e); + callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + e.getMessage()); + break; + } + + } catch (IOException | URISyntaxException e) { + logger.debug("Cannot create IRCC client: {}", e.getMessage(), e); + return; + } + } + } + + @Override + protected void eventReceived(final ScalarWebEvent event) throws IOException { + Objects.requireNonNull(event, "event cannot be null"); + + final @Nullable String mtd = event.getMethod(); + if (mtd == null || mtd.isEmpty()) { + logger.debug("Unhandled event received (no method): {}", event); + } else { + switch (mtd) { + case ScalarWebEvent.NOTIFYPOWERSTATUS: + final String powerVersion = getVersion(ScalarWebMethod.GETPOWERSTATUS); + if (VersionUtilities.equals(powerVersion, ScalarWebMethod.V1_0)) { + notifyPowerStatus(event.as(PowerStatusResult_1_0.class)); + } else { + notifyPowerStatus(event.as(PowerStatusResult_1_1.class)); + } + + break; + + case ScalarWebEvent.NOTIFYSWUPDATEINFO: + notifySoftwareUpdate(event.as(SoftwareUpdate.class)); + break; + + case ScalarWebEvent.NOTIFYSETTINGSUPDATE: + notifySettingUpdate(event.as(NotifySettingUpdate.class)); + break; + + case ScalarWebEvent.NOTIFYSTORAGESTATUS: + final String storVersion = getVersion(ScalarWebMethod.GETSTORAGELIST); + if (VersionUtilities.equals(storVersion, ScalarWebMethod.V1_1)) { + notifyStorageStatus(event.as(StorageListItem_1_1.class)); + } else { + notifyStorageStatus(event.as(StorageListItem_1_2.class)); + } + + break; + + default: + logger.debug("Unhandled event received: {}", event); + break; + } + } + } + + /** + * Called when a power state notification (v1.0) is received + * + * @param status a non-null power status notification + */ + private void notifyPowerStatus(final PowerStatusResult_1_0 status) { + Objects.requireNonNull(status, "status cannot be null"); + stateChanged(POWERSTATUS, status.isActive() ? OnOffType.ON : OnOffType.OFF); + } + + /** + * Called when a power state notification (v1.1) is received + * + * @param status a non-null power status notification + */ + private void notifyPowerStatus(final PowerStatusResult_1_1 status) { + Objects.requireNonNull(status, "status cannot be null"); + stateChanged(POWERSTATUS, status.isActive() ? OnOffType.ON : OnOffType.OFF); + } + + /** + * Called when a storage status notification (v1.1) is received + * + * @param item a non-null item + */ + private void notifyStorageStatus(final StorageListItem_1_1 item) { + Objects.requireNonNull(item, "item cannot be null"); + + final String uri = item.getUri(); + if (uri == null || uri.isEmpty()) { + logger.debug("Storage URI is empty - ignoring notification: {}", item); + return; + } + + final String id = getStorageChannelId(uri); + + stateChanged(ST_DEVICENAME, id, SonyUtil.newStringType(item.getDeviceName())); + stateChanged(ST_ERROR, id, SonyUtil.newStringType(item.getError())); + stateChanged(ST_FORMAT, id, SonyUtil.newStringType(item.getFormat())); + stateChanged(ST_FORMATTABLE, id, SonyUtil.newStringType(item.getFormattable())); + stateChanged(ST_FORMATTING, id, SonyUtil.newStringType(item.getFormatting())); + stateChanged(ST_FREECAPACITYMB, id, + SonyUtil.newQuantityType(item.getFreeCapacityMB(), MetricPrefix.MEGA(Units.BYTE))); + stateChanged(ST_ISAVAILABLE, id, SonyUtil.newStringType(item.getIsAvailable())); + stateChanged(ST_LUN, id, SonyUtil.newDecimalType(item.getLun())); + stateChanged(ST_MOUNTED, id, SonyUtil.newStringType(item.getMounted())); + stateChanged(ST_PERMISSION, id, SonyUtil.newStringType(item.getPermission())); + stateChanged(ST_POSITION, id, SonyUtil.newStringType(item.getPosition())); + stateChanged(ST_SYSTEMAREACAPACITYMB, + SonyUtil.newQuantityType(item.getSystemAreaCapacityMB(), MetricPrefix.MEGA(Units.BYTE))); + stateChanged(ST_TYPE, id, SonyUtil.newStringType(item.getType())); + stateChanged(ST_URI, id, SonyUtil.newStringType(uri)); + stateChanged(ST_VOLUMELABEL, id, SonyUtil.newStringType(item.getVolumeLabel())); + stateChanged(ST_WHOLECAPACITYMB, id, + SonyUtil.newQuantityType(item.getWholeCapacityMB(), MetricPrefix.MEGA(Units.BYTE))); + } + + /** + * Called when a storage status notification (v1.2) is received + * + * @param item a non-null item + */ + private void notifyStorageStatus(final StorageListItem_1_2 item) { + Objects.requireNonNull(item, "item cannot be null"); + + final String uri = item.getUri(); + if (uri == null || uri.isEmpty()) { + logger.debug("Storage URI is empty - ignoring notification: {}", item); + return; + } + + final String id = getStorageChannelId(uri); + + stateChanged(ST_DEVICENAME, id, SonyUtil.newStringType(item.getDeviceName())); + stateChanged(ST_ERROR, id, SonyUtil.newStringType(item.getError())); + stateChanged(ST_FILESYSTEM, id, SonyUtil.newStringType(item.getFileSystem())); + stateChanged(ST_FINALIZESTATUS, id, SonyUtil.newStringType(item.getFinalizeStatus())); + stateChanged(ST_FORMAT, id, SonyUtil.newStringType(item.getFormat())); + stateChanged(ST_FORMATSTATUS, id, SonyUtil.newStringType(item.getFormatStatus())); + stateChanged(ST_FORMATTABLE, id, SonyUtil.newStringType(item.getFormattable())); + stateChanged(ST_FREECAPACITYMB, id, + SonyUtil.newQuantityType(item.getFreeCapacityMB(), MetricPrefix.MEGA(Units.BYTE))); + stateChanged(ST_HASNONSTANDARDDATA, id, SonyUtil.newStringType(item.getHasNonStandardData())); + stateChanged(ST_HASUNSUPPORTEDCONTENTS, id, SonyUtil.newStringType(item.getHasUnsupportedContents())); + stateChanged(ST_ISAVAILABLE, id, SonyUtil.newStringType(item.getIsAvailable())); + stateChanged(ST_ISLOCKED, SonyUtil.newStringType(item.getIsLocked())); + stateChanged(ST_ISMANAGEMENTINFOFULL, id, SonyUtil.newStringType(item.getIsManagementInfoFull())); + stateChanged(ST_ISPROTECTED, id, SonyUtil.newStringType(item.getIsProtected())); + stateChanged(ST_ISREGISTERED, id, SonyUtil.newStringType(item.getIsRegistered())); + stateChanged(ST_ISSELFRECORDED, id, SonyUtil.newStringType(item.getIsSelfRecorded())); + stateChanged(ST_ISSQVSUPPORTED, id, SonyUtil.newStringType(item.getIsSqvSupported())); + stateChanged(ST_LUN, id, SonyUtil.newDecimalType(item.getLun())); + stateChanged(ST_MOUNTED, id, SonyUtil.newStringType(item.getMounted())); + stateChanged(ST_PERMISSION, id, SonyUtil.newStringType(item.getPermission())); + stateChanged(ST_POSITION, id, SonyUtil.newStringType(item.getPosition())); + stateChanged(ST_PROTOCOL, id, SonyUtil.newStringType(item.getProtocol())); + stateChanged(ST_REGISTRATIONDATE, id, SonyUtil.newStringType(item.getRegistrationDate())); + stateChanged(ST_SYSTEMAREACAPACITYMB, id, + SonyUtil.newQuantityType(item.getSystemAreaCapacityMB(), MetricPrefix.MEGA(Units.BYTE))); + stateChanged(ST_TIMESECTOFINALIZE, id, SonyUtil.newQuantityType(item.getTimeSecToFinalize(), Units.SECOND)); + stateChanged(ST_TIMESECTOGETCONTENTS, id, + SonyUtil.newQuantityType(item.getTimeSecToGetContents(), Units.SECOND)); + stateChanged(ST_TYPE, id, SonyUtil.newStringType(item.getType())); + stateChanged(ST_URI, id, SonyUtil.newStringType(uri)); + stateChanged(ST_USBDEVICETYPE, id, SonyUtil.newStringType(item.getUsbDeviceType())); + stateChanged(ST_VOLUMELABEL, id, SonyUtil.newStringType(item.getVolumeLabel())); + stateChanged(ST_WHOLECAPACITYMB, id, + SonyUtil.newQuantityType(item.getWholeCapacityMB(), MetricPrefix.MEGA(Units.BYTE))); + } + + /** + * Called when a software update notification has been done + * + * @param update a non-null software update + */ + private void notifySoftwareUpdate(final SoftwareUpdate update) { + Objects.requireNonNull(update, "update cannot be null"); + // TODO - do firmware update stuff + } + + @Override + public void close() { + super.close(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebVideoProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebVideoProtocol.java new file mode 100644 index 0000000000000..fe4492f5f09d1 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebVideoProtocol.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelTracker; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The implementation of the protocol handles the Video service + * + * @author Tim Roberts - Initial contribution + * @param the generic type for the callback + */ +@NonNullByDefault +class ScalarWebVideoProtocol<@NonNull T extends ThingCallback> extends AbstractScalarWebProtocol { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebVideoProtocol.class); + + // Constants used by the protocol + private static final String PICTUREQUALITYSETTINGS = "picturequalitysettings"; + + /** + * Instantiates a new scalar web video protocol. + * + * @param context the non-null context + * @param service the non-null service + * @param callback the non-null callback + */ + ScalarWebVideoProtocol(final ScalarWebProtocolFactory factory, final ScalarWebContext context, + final ScalarWebService service, final @NonNull T callback) { + super(factory, context, service, callback); + } + + @Override + public Collection getChannelDescriptors(final boolean dynamicOnly) { + final List descriptors = new ArrayList(); + + // no dynamic channels + if (dynamicOnly) { + return descriptors; + } + + if (service.hasMethod(ScalarWebMethod.GETPICTUREQUALITYSETTINGS)) { + addGeneralSettingsDescriptor(descriptors, ScalarWebMethod.GETPICTUREQUALITYSETTINGS, PICTUREQUALITYSETTINGS, + "Picture Quality Setting"); + } + + return descriptors; + } + + @Override + public void refreshState(boolean initial) { + final ScalarWebChannelTracker tracker = getChannelTracker(); + + if (tracker.isCategoryLinked(PICTUREQUALITYSETTINGS)) { + refreshGeneralSettings(tracker.getLinkedChannelsForCategory(PICTUREQUALITYSETTINGS), + ScalarWebMethod.GETPICTUREQUALITYSETTINGS); + } + } + + @Override + public void refreshChannel(final ScalarWebChannel channel) { + Objects.requireNonNull(channel, "channel cannot be null"); + + switch (channel.getCategory()) { + case PICTUREQUALITYSETTINGS: + refreshGeneralSettings(Collections.singleton(channel), ScalarWebMethod.GETPICTUREQUALITYSETTINGS); + break; + + default: + logger.debug("Unknown refresh channel: {}", channel); + break; + } + } + + @Override + public void setChannel(final ScalarWebChannel channel, final Command command) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + switch (channel.getCategory()) { + case PICTUREQUALITYSETTINGS: + setGeneralSetting(ScalarWebMethod.SETPICTUREQUALITYSETTINGS, channel, command); + break; + + default: + logger.debug("Unhandled channel command: {} - {}", channel, command); + break; + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebVideoScreenProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebVideoScreenProtocol.java new file mode 100644 index 0000000000000..14777bf0af87e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/protocols/ScalarWebVideoScreenProtocol.java @@ -0,0 +1,311 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.protocols; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannel; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelDescriptor; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebChannelTracker; +import org.openhab.binding.sony.internal.scalarweb.ScalarWebContext; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.CurrentValue; +import org.openhab.binding.sony.internal.scalarweb.models.api.Mode; +import org.openhab.binding.sony.internal.scalarweb.models.api.Position; +import org.openhab.binding.sony.internal.scalarweb.models.api.Screen; +import org.openhab.binding.sony.internal.scalarweb.models.api.Value; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The implementation of the protocol handles the Video Screen service + * + * @author Tim Roberts - Initial contribution + * @param the generic type for the callback + */ +@NonNullByDefault +class ScalarWebVideoScreenProtocol<@NonNull T extends ThingCallback> extends AbstractScalarWebProtocol { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(ScalarWebVideoScreenProtocol.class); + + // Constants used by the protocol + private static final String AUDIOSOURCE = "audiosource"; + private static final String BANNERMODE = "bannermode"; + private static final String MULTISCREENMODE = "multiscreenmode"; + private static final String PIPSUBSCREENPOSITION = "pipsubscreenposition"; + private static final String SCENESETTING = "scenesetting"; + + /** + * Instantiates a new scalar web video screen protocol. + * + * @param context the non-null context + * @param service the non-null service + * @param callback the non-null callback + */ + ScalarWebVideoScreenProtocol(final ScalarWebProtocolFactory factory, final ScalarWebContext context, + final ScalarWebService service, final @NonNull T callback) { + super(factory, context, service, callback); + } + + @Override + public Collection getChannelDescriptors(final boolean dynamicOnly) { + final List descriptors = new ArrayList(); + + // no dynamic channels + if (dynamicOnly) { + return descriptors; + } + + try { + execute(ScalarWebMethod.GETAUDIOSOURCESCREEN); + descriptors.add(createDescriptor(createChannel(AUDIOSOURCE), "String", "scalarvideoscreenaudiosource")); + } catch (final IOException e) { + logger.debug("Exception getting audio source screen: {}", e.getMessage()); + } + + try { + execute(ScalarWebMethod.GETBANNERMODE); + descriptors.add(createDescriptor(createChannel(BANNERMODE), "String", "scalarvideoscreenbannermode")); + } catch (final IOException e) { + logger.debug("Exception getting banner mode: {}", e.getMessage()); + } + + try { + execute(ScalarWebMethod.GETMULTISCREENMODE); + descriptors.add( + createDescriptor(createChannel(MULTISCREENMODE), "String", "scalarvideoscreenmultiscreenmode")); + } catch (final IOException e) { + logger.debug("Exception getting multiscreen mode: {}", e.getMessage()); + } + + try { + execute(ScalarWebMethod.GETPIPSUBSCREENPOSITION); + descriptors.add(createDescriptor(createChannel(PIPSUBSCREENPOSITION), "String", + "scalarvideoscreenpipsubscreenposition")); + } catch (final IOException e) { + logger.debug("Exception getting pip subscreen position: {}", e.getMessage()); + } + + // scenesetting shows notimplemented when not in TWIN view mode - so just assume it has it. + descriptors.add(createDescriptor(createChannel(SCENESETTING), "String", "scalarvideoscreenscenesetting")); + + return descriptors; + } + + @Override + public void refreshState(boolean initial) { + final ScalarWebChannelTracker tracker = getChannelTracker(); + if (tracker.isCategoryLinked(AUDIOSOURCE)) { + refreshAudioSource(); + } + + if (tracker.isCategoryLinked(BANNERMODE)) { + refreshBannerMode(); + } + + if (tracker.isCategoryLinked(MULTISCREENMODE)) { + refreshMultiScreenMode(); + } + + if (tracker.isCategoryLinked(PIPSUBSCREENPOSITION)) { + refreshPipPosition(); + } + + if (tracker.isCategoryLinked(SCENESETTING)) { + refreshSceneSetting(); + } + } + + @Override + public void refreshChannel(final ScalarWebChannel channel) { + Objects.requireNonNull(channel, "channel cannot be null"); + + switch (channel.getCategory()) { + case AUDIOSOURCE: + refreshAudioSource(); + break; + + case BANNERMODE: + refreshBannerMode(); + break; + + case MULTISCREENMODE: + refreshMultiScreenMode(); + break; + + case PIPSUBSCREENPOSITION: + refreshPipPosition(); + break; + + case SCENESETTING: + refreshSceneSetting(); + break; + + default: + logger.debug("Unknown refresh channel: {}", channel); + break; + } + } + + /** + * Refresh audio source + */ + private void refreshAudioSource() { + try { + final Screen screen = execute(ScalarWebMethod.GETAUDIOSOURCESCREEN).as(Screen.class); + stateChanged(AUDIOSOURCE, SonyUtil.newStringType(screen.getScreen())); + } catch (final IOException e) { + logger.debug("Exception getting the audio source screen: {}", e.getMessage()); + } + } + + /** + * Refresh banner mode + */ + private void refreshBannerMode() { + try { + final CurrentValue cv = execute(ScalarWebMethod.GETBANNERMODE).as(CurrentValue.class); + stateChanged(BANNERMODE, SonyUtil.newStringType(cv.getCurrentValue())); + } catch (final IOException e) { + logger.debug("Exception getting the banner mode: {}", e.getMessage()); + } + } + + /** + * Refresh multi screen mode + */ + private void refreshMultiScreenMode() { + try { + final Mode mode = execute(ScalarWebMethod.GETMULTISCREENMODE).as(Mode.class); + stateChanged(MULTISCREENMODE, SonyUtil.newStringType(mode.getMode())); + } catch (final IOException e) { + logger.debug("Exception getting the multi screen mode: {}", e.getMessage()); + } + } + + /** + * Refresh pip position + */ + private void refreshPipPosition() { + try { + final Position position = execute(ScalarWebMethod.GETPIPSUBSCREENPOSITION).as(Position.class); + stateChanged(PIPSUBSCREENPOSITION, SonyUtil.newStringType(position.getPosition())); + } catch (final IOException e) { + logger.debug("Exception getting the PIP position: {}", e.getMessage()); + } + } + + /** + * Refresh scene setting. + */ + private void refreshSceneSetting() { + try { + final CurrentValue cv = execute(ScalarWebMethod.GETSCENESETTING).as(CurrentValue.class); + stateChanged(SCENESETTING, SonyUtil.newStringType(cv.getCurrentValue())); + } catch (final IOException e) { + logger.debug("Exception getting the scene screen: {}", e.getMessage()); + } + } + + @Override + public void setChannel(final ScalarWebChannel channel, final Command command) { + Objects.requireNonNull(channel, "channel cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + switch (channel.getCategory()) { + case AUDIOSOURCE: + setAudioSource(command.toString()); + break; + + case BANNERMODE: + setBannerMode(command.toString()); + break; + + case MULTISCREENMODE: + setMultiScreenMode(command.toString()); + break; + + case PIPSUBSCREENPOSITION: + setPipSubScreenPosition(command.toString()); + break; + + case SCENESETTING: + setSceneSetting(command.toString()); + break; + + default: + logger.debug("Unhandled channel command: {} - {}", channel, command); + break; + } + } + + /** + * Sets the audio source. + * + * @param audioSource the new non-null, non-empty audio source + */ + private void setAudioSource(final String audioSource) { + SonyUtil.validateNotEmpty(audioSource, "audioSource cannot be empty"); + handleExecute(ScalarWebMethod.SETAUDIOSOURCESCREEN, new Screen(audioSource)); + } + + /** + * Sets the banner mode + * + * @param bannerMode the new non-null, non-empty banner mode + */ + private void setBannerMode(final String bannerMode) { + SonyUtil.validateNotEmpty(bannerMode, "bannerMode cannot be empty"); + handleExecute(ScalarWebMethod.SETBANNERMODE, new Value(bannerMode)); + } + + /** + * Sets the multi screen mode + * + * @param multiScreenMode the new non-null, non-empty multi screen mode + */ + private void setMultiScreenMode(final String multiScreenMode) { + SonyUtil.validateNotEmpty(multiScreenMode, "multiScreenMode cannot be empty"); + handleExecute(ScalarWebMethod.SETMULTISCREENMODE, new Mode(multiScreenMode)); + } + + /** + * Sets the pip sub screen position + * + * @param pipPosition the new non-null, non-empty pip sub screen position + */ + private void setPipSubScreenPosition(final String pipPosition) { + SonyUtil.validateNotEmpty(pipPosition, "pipPosition cannot be empty"); + handleExecute(ScalarWebMethod.SETPIPSUBSCREENPOSITION, new Position(pipPosition)); + } + + /** + * Sets the scene setting + * + * @param sceneSetting the new scene setting + */ + private void setSceneSetting(final String sceneSetting) { + SonyUtil.validateNotEmpty(sceneSetting, "sceneSetting cannot be empty"); + handleExecute(ScalarWebMethod.SETSCENESETTING, new Value(sceneSetting)); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/service/CommandRequest.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/service/CommandRequest.java new file mode 100644 index 0000000000000..a325151b9b86d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/service/CommandRequest.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.service; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The class will represent a command request deserialized from a webpage. A command request is basically a + * ScalarWebRequest to run against a sony device + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class CommandRequest { + /** The base URL to run the command against */ + private @Nullable String baseUrl; + + /** The service to run against */ + private @Nullable String serviceName; + + /** The transport name to use */ + private @Nullable String transport; + + /** The command to run */ + private @Nullable String command; + + /** The version of the command to use */ + private @Nullable String version; + + /** Any parameters to use */ + private @Nullable String parms; + + /** + * Empty constructor used for deserialization + */ + public CommandRequest() { + } + + /** + * Gets the base URL + * + * @return the base URL + */ + public @Nullable String getBaseUrl() { + return baseUrl; + } + + /** + * Gets the service name + * + * @return the service name + */ + public @Nullable String getServiceName() { + return serviceName; + } + + /** + * Gets the transport + * + * @return the transport + */ + public @Nullable String getTransport() { + return transport; + } + + /** + * Gets the command + * + * @return the command + */ + public @Nullable String getCommand() { + return command; + } + + /** + * Gets the command version + * + * @return the command version + */ + public @Nullable String getVersion() { + return version; + } + + /** + * Gets any parameters + * + * @return the parameters + */ + public @Nullable String getParms() { + return parms; + } + + @Override + public String toString() { + return "CommandRequest [baseUrl=" + baseUrl + ", serviceName=" + serviceName + ", transport=" + transport + + ", command=" + command + ", version=" + version + ", parms=" + parms + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/service/CommandResponse.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/service/CommandResponse.java new file mode 100644 index 0000000000000..801e1cb7e0a8b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/service/CommandResponse.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.service; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * The class represents the results of the requested command and will be serialized back to the webpage + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class CommandResponse { + /** True if the call was successful, false otherwise */ + private final boolean success; + + /** The optional message if not successful */ + private final @Nullable String message; + + /** The optional message if not successful */ + private final @Nullable String results; + + /** + * Constructs a successful result with the results + * + * @param results a non-null, non-empty results string + */ + public CommandResponse(String results) { + SonyUtil.validateNotEmpty(results, "results cannot be empty"); + this.success = true; + this.message = null; + this.results = results; + } + + /** + * Constructs a (generally unsuccessful) result with the message + * + * @param success true if success, false otherwise + * @param message a non-null, non-empty message + */ + public CommandResponse(boolean success, String message) { + SonyUtil.validateNotEmpty(message, "message cannot be empty"); + this.success = success; + this.message = message; + this.results = null; + } + + @Override + public String toString() { + return "CommandResponse [success=" + success + ", message=" + message + ", results=" + results + "]"; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/service/SonyServlet.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/service/SonyServlet.java new file mode 100644 index 0000000000000..92885508e7937 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/scalarweb/service/SonyServlet.java @@ -0,0 +1,214 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.scalarweb.service; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URL; +import java.util.Hashtable; +import java.util.Objects; +import java.util.concurrent.ScheduledExecutorService; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.gson.GsonUtilities; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebRequest; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; +import org.openhab.binding.sony.internal.transports.SonyTransport; +import org.openhab.binding.sony.internal.transports.SonyTransportFactory; +import org.openhab.binding.sony.internal.transports.TransportOptionAutoAuth; +import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.io.net.http.WebSocketFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; + +/** + * This servlet will handle the interactions between the sony system and the webpages + * + * @author Tim Roberts - Initial contribution + * @param the generic type for the callback + */ +@NonNullByDefault +@Component() +public class SonyServlet extends HttpServlet { + + /** Stupid */ + private static final long serialVersionUID = -8873654812522111922L; + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(SonyServlet.class); + + /** The path to the main sony page */ + private static final String SONY_PATH = "/sony"; + + /** The path to the main sony application */ + private static final String SONYAPP_PATH = "/sony/app"; + + /** The http service */ + private final HttpService httpService; + + /** The websocket client to use */ + private final WebSocketClient webSocketClient; + + /** The clientBuilder used in HttpRequest */ + private final ClientBuilder clientBuilder; + + /** The GSON to use for serialization */ + private final Gson gson = GsonUtilities.getApiGson(); + + /** The scheduler to use to schedule tasks */ + private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("sony"); + + /** + * Constructs the sony servlet + * + * @param webSocketFactory a non-null websocket factory + * @param httpService a non-null http service + */ + @Activate + public SonyServlet(final @Reference WebSocketFactory webSocketFactory, final @Reference HttpService httpService, + final @Reference ClientBuilder clientBuilder) { + Objects.requireNonNull(webSocketFactory, "webSocketFactory cannot be null"); + Objects.requireNonNull(httpService, "httpService cannot be null"); + + this.webSocketClient = webSocketFactory.getCommonWebSocketClient(); + this.httpService = httpService; + this.clientBuilder = clientBuilder; + } + + @Activate + public void activate() { + try { + httpService.registerServlet(SONYAPP_PATH, this, new Hashtable<>(), httpService.createDefaultHttpContext()); + httpService.registerResources(SONY_PATH, "web/sonyapp", httpService.createDefaultHttpContext()); + logger.debug("Started Sony Web service at {}", SONY_PATH); + } catch (ServletException | NamespaceException e) { + logger.debug("Exception starting status servlet: {}", e.getMessage(), e); + } + } + + @Deactivate + public void deactivate() { + } + + @Override + protected void doPost(final @Nullable HttpServletRequest req, final @Nullable HttpServletResponse resp) + throws ServletException, IOException { + Objects.requireNonNull(req, "req cannot be null"); + Objects.requireNonNull(resp, "resp cannot be null"); + final CommandRequest cmdRqst = gson.fromJson(req.getReader(), CommandRequest.class); + + final String baseUrl = cmdRqst.getBaseUrl(); + if (baseUrl == null || baseUrl.isEmpty()) { + write(resp, gson.toJson(new CommandResponse(false, "baseUrl is required"))); + return; + } + + final String serviceName = cmdRqst.getServiceName(); + if (serviceName == null || serviceName.isEmpty()) { + write(resp, gson.toJson(new CommandResponse(false, "serviceName is required"))); + return; + } + + final String transportName = cmdRqst.getTransport(); + if (transportName == null || transportName.isEmpty()) { + write(resp, gson.toJson(new CommandResponse(false, "transport is required"))); + return; + } + + final String command = cmdRqst.getCommand(); + if (command == null || command.isEmpty()) { + write(resp, gson.toJson(new CommandResponse(false, "command is required"))); + return; + } + + final String version = cmdRqst.getVersion(); + if (version == null || version.isEmpty()) { + write(resp, gson.toJson(new CommandResponse(false, "version is required"))); + return; + } + + final String parms = cmdRqst.getParms(); + // cannot be null but can be empty + if (parms == null) { + write(resp, gson.toJson(new CommandResponse(false, "parms is required"))); + return; + } + + final SonyTransportFactory factory = new SonyTransportFactory(new URL(baseUrl), gson, webSocketClient, + scheduler, clientBuilder); + + try (final SonyTransport transport = factory.getSonyTransport(serviceName, transportName)) { + if (transport == null) { + write(resp, gson.toJson(new CommandResponse(false, "No transport of type: " + transportName))); + return; + } else { + // Use 1 to not conflict with ScalarWebRequest IDs + final String cmd = "{\"id\":1,\"method\":\"" + command + "\",\"version\":\"" + version + + "\",\"params\":[" + parms + "]}"; + + final ScalarWebRequest rqst = Objects.requireNonNull(gson.fromJson(cmd, ScalarWebRequest.class)); + + final ScalarWebResult result = transport.execute(rqst, TransportOptionAutoAuth.TRUE); + if (result.isError()) { + write(resp, gson.toJson(new CommandResponse(false, + SonyUtil.defaultIfEmpty(result.getDeviceErrorDesc(), "failure")))); + } else { + final JsonArray ja = result.getResults(); + final String resString = ja == null ? null : gson.toJson(ja); + write(resp, gson.toJson(new CommandResponse(SonyUtil.defaultIfEmpty(resString, "Success")))); + } + } + } + } + + /** + * Write a response out to the {@link HttpServletResponse} + * + * @param resp the non-null {@link HttpServletResponse} + * @param str the possibly null, possibly empty string content to write + * @throws IOException Signals that an I/O exception has occurred. + */ + public void write(final HttpServletResponse resp, final String str) throws IOException { + Objects.requireNonNull(resp, "resp cannot be null"); + + resp.setContentType("application/json"); + resp.setCharacterEncoding("UTF-8"); + final PrintWriter pw = resp.getWriter(); + if (SonyUtil.isEmpty(str)) { + pw.print("{}"); + } else { + pw.print(str); + } + + logger.trace("Sending: {}", str); + pw.flush(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpConfig.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpConfig.java new file mode 100644 index 0000000000000..64a3440c64bc4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpConfig.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.simpleip; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.AbstractConfig; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * Configuration class for the {@link SimpleIpHandler}. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SimpleIpConfig extends AbstractConfig { + /** The commands map file */ + private @Nullable String commandsMapFile; + + /** The network interface the sony system listens on (eth0 or wlan0) */ + private @Nullable String netInterface; + + // ---- the following properties are not part of the config.xml (and are properties) ---- + + /** The commands map file */ + private @Nullable String discoveredCommandsMapFile; + + @Override + public @Nullable String getDeviceIpAddress() { + return super.getDeviceAddress(); + } + + /** + * Gets the network interface being used + * + * @return the network interface + */ + public String getNetInterface() { + return SonyUtil.defaultIfEmpty(netInterface, "eth0"); + } + + /** + * Sets the network interface being used + * + * @param netInterface the network interface + */ + public void setNetInterface(final String netInterface) { + this.netInterface = netInterface; + } + + /** + * Gets the commands map file name + * + * @return the commands map file name + */ + + public @Nullable String getCommandsMapFile() { + return SonyUtil.defaultIfEmpty(commandsMapFile, discoveredCommandsMapFile); + } + + /** + * Sets the command map file name + * + * @param commandsMapFile the command map file name + */ + public void setCommandsMapFile(final String commandsMapFile) { + this.commandsMapFile = commandsMapFile; + } + + /** + * Sets the discovered command map file name + * + * @param discoveredCommandsMapFile the command map file name + */ + public void setDiscoveredCommandsMapFile(final String discoveredCommandsMapFile) { + this.discoveredCommandsMapFile = discoveredCommandsMapFile; + } + + @Override + public Map asProperties() { + final Map props = super.asProperties(); + + props.put("discoveredCommandsMapFile", SonyUtil.defaultIfEmpty(discoveredCommandsMapFile, "")); + conditionallyAddProperty(props, "commandsMapFile", commandsMapFile); + conditionallyAddProperty(props, "netInterface", netInterface); + + return props; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpConstants.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpConstants.java new file mode 100644 index 0000000000000..dc49dbcddf237 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpConstants.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.simpleip; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The class provides all the constants specific to the Simple IP system. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SimpleIpConstants { + // The thing constants + public static final ThingTypeUID THING_TYPE_SIMPLEIP = new ThingTypeUID(SonyBindingConstants.BINDING_ID, + SonyBindingConstants.SIMPLEIP_THING_TYPE_PREFIX); + + // Default port for simple IP + public static final int PORT = 20060; + + // All the channel constants + static final String CHANNEL_IR = "ir"; + static final String CHANNEL_POWER = "power"; + static final String CHANNEL_TOGGLEPOWER = "togglepower"; + static final String CHANNEL_VOLUME = "volume"; + static final String CHANNEL_AUDIOMUTE = "audiomute"; + static final String CHANNEL_CHANNEL = "channel"; + static final String CHANNEL_TRIPLETCHANNEL = "tripletchannel"; + static final String CHANNEL_INPUTSOURCE = "inputsource"; + static final String CHANNEL_INPUT = "input"; + static final String CHANNEL_SCENE = "scene"; + static final String CHANNEL_PICTUREMUTE = "picturemute"; + static final String CHANNEL_TOGGLEPICTUREMUTE = "togglepicturemute"; + static final String CHANNEL_PICTUREINPICTURE = "pip"; + static final String CHANNEL_TOGGLEPICTUREINPICTURE = "togglepip"; + static final String CHANNEL_TOGGLEPIPPOSITION = "togglepipposition"; + + // All the custom property keys + static final String PROP_BROADCASTADDRESS = "broadcastaddress"; + static final String PROP_MACADDRESS = "macaddress"; +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpDiscoveryParticipant.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpDiscoveryParticipant.java new file mode 100644 index 0000000000000..3dd0635c6698c --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpDiscoveryParticipant.java @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.simpleip; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.model.meta.RemoteDevice; +import org.jupnp.model.meta.RemoteDeviceIdentity; +import org.jupnp.model.types.UDN; +import org.openhab.binding.sony.internal.AbstractDiscoveryParticipant; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.UidUtils; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.net.SocketSessionListener; +import org.openhab.binding.sony.internal.providers.SonyDefinitionProvider; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * This implementation of the {@link UpnpDiscoveryParticipant} provides discovery of Sony SIMPLE IP protocol + * devices. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "discovery.sony-simpleip") +public class SimpleIpDiscoveryParticipant extends AbstractDiscoveryParticipant implements UpnpDiscoveryParticipant { + /** + * Constructs the participant + * + * @param sonyDefinitionProvider a non-null sony definition provider + */ + @Activate + public SimpleIpDiscoveryParticipant(final @Reference SonyDefinitionProvider sonyDefinitionProvider) { + super(SonyBindingConstants.SIMPLEIP_THING_TYPE_PREFIX, sonyDefinitionProvider); + } + + @Override + protected boolean getDiscoveryEnableDefault() { + return false; + } + + @Override + public @Nullable DiscoveryResult createResult(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + + if (!isDiscoveryEnabled()) { + return null; + } + + final ThingUID uid = getThingUID(device); + if (uid == null) { + return null; + } + + final String ipAddress = device.getIdentity().getDescriptorURL().getHost(); + + final CountDownLatch l = new CountDownLatch(1); + final AtomicBoolean valid = new AtomicBoolean(false); + + // Test to see if it's an actual simpleip service + try { + NetUtil.sendSocketRequest(ipAddress, SimpleIpConstants.PORT, "*SEPOWR################\n", + new SocketSessionListener() { + @Override + public boolean responseReceived(final String response) { + valid.set(response.startsWith("*SAPOWR")); + l.countDown(); + return true; + } + + @Override + public void responseException(final IOException e) { + l.countDown(); + } + }); + l.await(10, TimeUnit.SECONDS); + if (!valid.get()) { + logger.debug("SimpleIP device didn't implement the power command - ignoring: {}", device.getIdentity()); + return null; + } + } catch (IOException | InterruptedException e) { + // Normal to get an IO exception if device doesn't support - log as a trace + logger.trace("SimpleIP device exception {}: {}", device.getIdentity(), e.getMessage(), e); + return null; + } + + final SimpleIpConfig config = new SimpleIpConfig(); + config.setDeviceAddress(ipAddress); + + final RemoteDeviceIdentity identity = device.getIdentity(); + config.setDiscoveredMacAddress(getMacAddress(identity, uid)); + config.setDiscoveredCommandsMapFile("simpleip-" + uid.getId() + ".map"); + + final String thingId = UidUtils.getThingId(identity.getUdn()); + return DiscoveryResultBuilder.create(uid).withProperties(config.asProperties()) + .withProperty("SimpleUDN", SonyUtil.defaultIfEmpty(thingId, uid.getId())) + .withRepresentationProperty("SimpleUDN").withLabel(getLabel(device, "Simple IP")).build(); + } + + @Override + public @Nullable ThingUID getThingUID(final RemoteDevice device) { + Objects.requireNonNull(device, "device cannot be null"); + + if (!isDiscoveryEnabled()) { + return null; + } + + final String modelDescription = getModelDescription(device); + + // Simple IP == bravia + if (isSonyDevice(device) && modelDescription != null && modelDescription.toLowerCase().contains("bravia")) { + if (isScalarThingType(device)) { + logger.debug("Found a SCALAR thing type for this SIMPLEIP thing - ignoring SIMPLEIP"); + return null; + } + + final String modelName = getModelName(device); + if (modelName == null || modelName.isEmpty()) { + logger.debug("Found Sony device but it has no model name - ignoring"); + return null; + } + + final RemoteDeviceIdentity identity = device.getIdentity(); + if (identity != null) { + final UDN udn = device.getIdentity().getUdn(); + logger.debug("Found Sony SimpleIP service: {}", udn); + final ThingTypeUID modelUID = getThingTypeUID(modelName); + return UidUtils.createThingUID(modelUID == null ? SimpleIpConstants.THING_TYPE_SIMPLEIP : modelUID, + udn); + } else { + logger.debug("Found Sony SimpleIP service but it had no identity!"); + } + } + return null; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpHandler.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpHandler.java new file mode 100644 index 0000000000000..884f95cae258d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpHandler.java @@ -0,0 +1,342 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.simpleip; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.AbstractThingHandler; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.transform.TransformationService; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The thing handler for a Sony Simple IP device. This is the entry point provides a full two interaction between + * openhab and the simple IP system. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SimpleIpHandler extends AbstractThingHandler { + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(SimpleIpHandler.class); + + /** The protocol handler being used - will be null if not initialized. */ + private final AtomicReference<@Nullable SimpleIpProtocol> protocolHandler = new AtomicReference<>(); + + /** The transformation service to use to transform the MAP file */ + private final @Nullable TransformationService transformationService; + + /** + * Constructs the handler from the {@link Thing} and {@link TransformationService} + * + * @param thing a non-null {@link Thing} the handler is for + * @param transformationService a possibly null {@link TransformationService} to use to transform MAP file + */ + public SimpleIpHandler(final Thing thing, final @Nullable TransformationService transformationService) { + super(thing, SimpleIpConfig.class); + + Objects.requireNonNull(thing, "thing cannot be null"); + this.transformationService = transformationService; + } + + @Override + protected void handleRefreshCommand(final ChannelUID channelUID) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + + final String id = channelUID.getId(); + + SonyUtil.validateNotEmpty(id, "id cannot be empty"); + + if (getThing().getStatus() != ThingStatus.ONLINE) { + return; + } + + final SimpleIpProtocol handler = protocolHandler.get(); + if (handler == null) { + logger.debug("Protocol handler wasn't set for handleRefresh - ignoring '{}'", id); + return; + } + + switch (id) { + case SimpleIpConstants.CHANNEL_POWER: + handler.refreshPower(); + break; + case SimpleIpConstants.CHANNEL_VOLUME: + handler.refreshVolume(); + break; + case SimpleIpConstants.CHANNEL_AUDIOMUTE: + handler.refreshAudioMute(); + break; + case SimpleIpConstants.CHANNEL_CHANNEL: + handler.refreshChannel(); + break; + case SimpleIpConstants.CHANNEL_TRIPLETCHANNEL: + handler.refreshTripletChannel(); + break; + case SimpleIpConstants.CHANNEL_INPUTSOURCE: + handler.refreshInputSource(); + break; + case SimpleIpConstants.CHANNEL_INPUT: + handler.refreshInput(); + break; + case SimpleIpConstants.CHANNEL_PICTUREMUTE: + handler.refreshPictureMute(); + break; + case SimpleIpConstants.CHANNEL_PICTUREINPICTURE: + handler.refreshPictureInPicture(); + break; + + } + } + + @Override + protected void handleSetCommand(final ChannelUID channelUID, final Command command) { + Objects.requireNonNull(channelUID, "channelUID cannot be null"); + Objects.requireNonNull(command, "command cannot be null"); + + final String id = channelUID.getId(); + + final SimpleIpProtocol handler = protocolHandler.get(); + if (handler == null) { + logger.debug("Protocol handler wasn't set for handleCommand - ignoring '{}': '{}'", channelUID, command); + return; + } + + switch (id) { + case SimpleIpConstants.CHANNEL_IR: + if (command instanceof StringType) { + handler.setIR(command.toString()); + } else { + logger.debug("Received a IR channel command with a non StringType: {}", command); + } + + break; + + case SimpleIpConstants.CHANNEL_POWER: + if (command instanceof OnOffType) { + handler.setPower(command == OnOffType.ON); + } else { + logger.debug("Received a POWER channel command with a non OnOffType: {}", command); + } + + break; + + case SimpleIpConstants.CHANNEL_TOGGLEPOWER: + handler.togglePower(); + break; + + case SimpleIpConstants.CHANNEL_VOLUME: + if (command instanceof OnOffType) { + handler.setAudioMute(command == OnOffType.ON); + } else if (command instanceof IncreaseDecreaseType) { + handler.setIR(command == IncreaseDecreaseType.INCREASE ? "Volume Up" : "Volume Down"); + } else if (command instanceof PercentType) { + handler.setAudioVolume(((PercentType) command).intValue()); + } else { + logger.debug( + "Received a AUDIO VOLUME channel command with a non OnOffType/IncreaseDecreaseType/PercentType: {}", + command); + } + + break; + + case SimpleIpConstants.CHANNEL_AUDIOMUTE: + if (command instanceof OnOffType) { + handler.setAudioMute(command == OnOffType.ON); + } else { + logger.debug("Received a AUDIO MUTE channel command with a non OnOffType: {}", command); + } + + break; + + case SimpleIpConstants.CHANNEL_CHANNEL: + if (command instanceof StringType) { + handler.setChannel(command.toString()); + } else { + logger.debug("Received a CHANNEL channel command with a non StringType: {}", command); + } + + break; + + case SimpleIpConstants.CHANNEL_TRIPLETCHANNEL: + if (command instanceof StringType) { + handler.setTripletChannel(command.toString()); + } else { + logger.debug("Received a TRIPLET CHANNEL channel command with a non StringType: {}", command); + } + + break; + case SimpleIpConstants.CHANNEL_INPUTSOURCE: + if (command instanceof StringType) { + handler.setInputSource(command.toString()); + } else { + logger.debug("Received a INPUT SOURCE channel command with a non StringType: {}", command); + } + break; + case SimpleIpConstants.CHANNEL_INPUT: + if (command instanceof StringType) { + handler.setInput(command.toString()); + } else { + logger.debug("Received a INPUT channel command with a non StringType: {}", command); + } + break; + case SimpleIpConstants.CHANNEL_SCENE: + if (command instanceof StringType) { + handler.setScene(command.toString()); + } else { + logger.debug("Received a SCENE channel command with a non StringType: {}", command); + } + break; + case SimpleIpConstants.CHANNEL_PICTUREMUTE: + if (command instanceof OnOffType) { + handler.setPictureMute(command == OnOffType.ON); + } else { + logger.debug("Received a PICTURE MUTE channel command with a non OnOffType: {}", command); + } + break; + case SimpleIpConstants.CHANNEL_TOGGLEPICTUREMUTE: + handler.togglePictureMute(); + break; + case SimpleIpConstants.CHANNEL_PICTUREINPICTURE: + if (command instanceof OnOffType) { + handler.setPictureInPicture(command == OnOffType.ON); + } else { + logger.debug("Received a PICTURE IN PICTURE channel command with a non OnOffType: {}", command); + } + break; + case SimpleIpConstants.CHANNEL_TOGGLEPICTUREINPICTURE: + handler.togglePictureInPicture(); + break; + case SimpleIpConstants.CHANNEL_TOGGLEPIPPOSITION: + handler.togglePipPosition(); + break; + + default: + logger.debug("Unknown/Unsupported Channel id: {}", id); + break; + } + } + + @Override + protected PowerCommand handlePotentialPowerOnCommand(final ChannelUID channelUID, final Command command) { + final String id = channelUID.getId(); + if (SimpleIpConstants.CHANNEL_POWER.equals(id)) { + if (command instanceof OnOffType) { + if (command == OnOffType.ON) { + SonyUtil.sendWakeOnLan(logger, getSonyConfig().getDeviceIpAddress(), + getSonyConfig().getDeviceMacAddress()); + return PowerCommand.ON; + } else { + return PowerCommand.OFF; + } + } + } + return PowerCommand.NON; + } + + @Override + public void refreshState(boolean initial) { + final SimpleIpProtocol protocol = protocolHandler.get(); + if (protocol != null) { + protocol.refreshState(true); + } + } + + /** + * Attempts to connect to the system via {@link SimpleIpProtocol#login()}. Once completed, a ping job will be + * created + * to keep the connection + * alive and a refresh job to refresh state (although the broadcast address and mac address is refreshed immediately + * and only once). If a connection cannot be established (or login failed), the connection attempt will be retried + * later + */ + @Override + protected void connect() { + final SimpleIpConfig config = getSonyConfig(); + + if (SonyUtil.isEmpty(config.getDeviceIpAddress())) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "IP Address of Sony is missing from configuration"); + return; + } + + try { + SonyUtil.checkInterrupt(); + + final SimpleIpProtocol localProtocolHandler = new SimpleIpProtocol(config, transformationService, + new ThingCallback() { + @Override + public void statusChanged(final ThingStatus status, final ThingStatusDetail detail, + final @Nullable String msg) { + updateStatus(status, detail, msg); + } + + @Override + public void stateChanged(final String channelId, final State state) { + updateState(channelId, state); + } + + @Override + public void setProperty(final String propertyName, final @Nullable String propertyValue) { + getThing().setProperty(propertyName, propertyValue); + } + }); + + SonyUtil.checkInterrupt(); + final String response = localProtocolHandler.login(); + if (response == null) { + SonyUtil.close(protocolHandler.getAndSet(localProtocolHandler)); + + logger.debug("Simple IP TV System now connected"); + updateStatus(ThingStatus.ONLINE); + + SonyUtil.checkInterrupt(); + localProtocolHandler.postLogin(); + + SonyUtil.checkInterrupt(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, response); + } + + } catch (final IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Error connecting to simple IP tv"); + } catch (final InterruptedException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "Initialization was interrupted"); + } + } + + @Override + public void dispose() { + super.dispose(); + SonyUtil.close(protocolHandler.getAndSet(null)); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpProtocol.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpProtocol.java new file mode 100644 index 0000000000000..ba00e31a3f0e3 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/simpleip/SimpleIpProtocol.java @@ -0,0 +1,1384 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.simpleip; + +import java.io.File; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +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.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.ThingCallback; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.net.SocketChannelSession; +import org.openhab.binding.sony.internal.net.SocketSession; +import org.openhab.binding.sony.internal.net.SocketSessionListener; +import org.openhab.core.OpenHAB; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.transform.TransformationException; +import org.openhab.core.transform.TransformationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the protocol handler for the Simple IP System. This handler will issue the protocol commands and will + * process the responses from the Simple IP system. The Simple IP system is a little flacky and doesn't seem to handle + * multiple commands in a single session. For this reason, we create a single {@link SocketSession} to listen for any + * notifications (whose lifetime matches that of this handler) and then create separate {@link SocketSession} for each + * request. Special care must be taken to differentiate between a Control request result and the Inquiry/Notification + * results to avoid misinterpreting the result (the control "success" message will have all zeroes - which has a form + * that matches some inquiry/notification results (like volume could be interpreted as 0!). + * + * Additional documentation: https://pro-bravia.sony.net/develop/integrate/ssip/overview/ + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +class SimpleIpProtocol implements SocketSessionListener, AutoCloseable { + + // Logger + private final Logger logger = LoggerFactory.getLogger(SimpleIpProtocol.class); + + // Protocol Constants + private static final char TYPE_CONTROL = 'C'; + private static final char TYPE_QUERY = 'E'; + private static final char TYPE_ANSWER = 'A'; + private static final char TYPE_NOTIFY = 'N'; + + private static final String IRCC = "IRCC"; + private static final String POWER = "POWR"; + private static final String TOGGLE_POWER = "TPOW"; + private static final String VOLUME = "VOLU"; + private static final String AUDIO_MUTE = "AMUT"; + private static final String INPUT = "INPT"; + private static final String PICTURE_MUTE = "PMUT"; + private static final String TOGGLE_PICTURE_MUTE = "TPMU"; + private static final String BROADCAST_ADDRESS = "BADR"; + private static final String MACADDRESS = "MADR"; + private static final String SCENE = "SCEN"; + + private static final String CHANNEL = "CHNN"; + private static final String TRIPLET_CHANNEL = "TCHN"; + private static final String INPUT_SOURCE = "ISRC"; + private static final String PICTURE_IN_PICTURE = "PIPI"; + private static final String TOGGLE_PICTURE_IN_PICTURE = "TPIP"; + private static final String TOGGLE_PIP_POSITION = "TPPP"; + + private static final String NO_PARM = "################"; + + // Size of the parameter area as defined by the spec + private static final int PARMSIZE = 16; + + // Response strings/patterns + private static final String RSP_SUCCESS = "0000000000000000"; + private static final String RSP_ERROR = "FFFFFFFFFFFFFFFF"; + private static final String RSP_NOSUCHTHING = "NNNNNNNNNNNNNNNN"; + private static final Pattern RSP_NOTIFICATION = Pattern + .compile("^\\*S([" + TYPE_ANSWER + TYPE_NOTIFY + "])(\\w{4})(.*{16})"); + + /** The {@link SocketSession} that will listen for notifications. */ + private final SocketSession listeningSession; + + /** The {@link SimpleIpConfig} for creating new {@link SocketSession}. */ + private final SimpleIpConfig config; + + /** The {@link ThingCallback} that we can callback to set state and status. */ + private final ThingCallback callback; + + /** The {@link TransformationService} to use */ + private final @Nullable TransformationService transformService; + + /** The constant for the TV Input in {@link #INPUT_TYPES} */ + private static final int INPUT_TV = 0; + + /** Represents a lookup between the known input source types and their corresponding simple IP identifier. */ + private static final Map INPUT_TYPES = new HashMap(); + static { + INPUT_TYPES.put(INPUT_TV, "TV"); + INPUT_TYPES.put(10000, "HDMI"); + INPUT_TYPES.put(20000, "SCART"); + INPUT_TYPES.put(30000, "Composite"); + INPUT_TYPES.put(40000, "Component"); + INPUT_TYPES.put(50000, "Screen Mirroring"); + INPUT_TYPES.put(60000, "PC RGB Input"); + } + + /** The default commands */ + private static final List DEFAULT_CMDS = Arrays.asList("Input=1", "Guide=2", "EPG=3", "Favorites=4", + "Display=5", "Home=6", "Options=7", "Return=8", "Up=9", "Down=10", "Right=11", "Left=12", "Confirm=13", + "Red=14", "Green=15", "Yellow=16", "Blue=17", "Num1=18", "Num2=19", "Num3=20", "Num4=21", "Num5=22", + "Num6=23", "Num7=24", "Num8=25", "Num9=26", "Num0=27", "Num11=28", "Num12=29", "Volume-Up=30", + "Volume-Down=31", "Mute=32", "Channel-Up=33", "Channel-Down=34", "Subtitle=35", "Closed-Caption=36", + "Enter=37", "DOT=38", "Analog=39", "Teletext=40", "Exit=41", "Analog2=42", "*AD=43", "Digital=44", + "Analog?=45", "BS=46", "CS=47", "BS/CS=48", "Ddata=49", "Pic-Off=50", "Tv_Radio=51", "Theater=52", "SEN=53", + "Internet-Widgets=54", "Internet-Video=55", "Netflix=56", "Scene-Select=57", "Model3D=58", "iManual=59", + "Audio=60", "Wide=61", "Jump=62", "PAP=63", "MyEPG=64", "Program-Description=65", "Write-Chapter=66", + "TrackID=67", "Ten-Key=68", "AppliCast=69", "acTVila=70", "Delete-Video=71", "Photo-Frame=72", + "TV-Pause=73", "Key-Pad=74", "Media=75", "Sync-Menu=76", "Forward=77", "Play=78", "Rewind=79", "Prev=80", + "Stop=81", "Next=82", "Rec=83", "Pause=84", "Eject=85", "Flash-Plus=86", "Flash-Minus=87", "Top-Menus=88", + "Popup-Menu=89", "Rakuraku-Start=90", "One-Touch-Time-Rec=91", "One-Touch-View=92", "One-Touch-Rec=93", + "One-Touch-Stop=94", "DUX=95", "Football-Mode=96", "Social=97", "Power=98", "Power-On=103", "Sleep=104", + "Sleep-Timer=105", "Composite1=107", "Video2=108", "Picture-Mode=110", "Demo-Surround=121", "Hdmi1=124", + "Hdmi2=125", "Hdmi3=126", "Hdmi4=127", "Action-Menu=129", "Help=130"); + + /** + * Constructs the protocol handler from given parameters. This constructor will create the + * {@link #listeningSession} to listen to notifications sent by the Simple IP device (adding ourselfs as the + * listener). + * + * @param config a non-null {@link SimpleIpConfig} (may be connected or disconnected) + * @param transformService a possibly null {@link TransformationService} + * @param callback a non-null {@link ThingCallback} to callback + * @throws IOException Signals that an I/O exception has occurred. + */ + SimpleIpProtocol(final SimpleIpConfig config, final @Nullable TransformationService transformService, + final ThingCallback callback) throws IOException { + this.config = config; + this.transformService = transformService; + this.callback = callback; + + final String ipAddress = config.getDeviceIpAddress(); + Objects.requireNonNull(ipAddress, "ipAddress cannot be null"); + + listeningSession = new SocketChannelSession(ipAddress, SimpleIpConstants.PORT); + listeningSession.addListener(this); + } + + /** + * Attempts to log into the system. The login will connect the {@link #listeningSession} and immediately call + * {@link #postLogin()} since there is no authentication mechanisms + * + * @return always null to indicate a successful login + * @throws IOException if an exception occurs trying to connect our {@link #listeningSession} or writing the + * command file + */ + @Nullable + String login() throws IOException { + listeningSession.connect(); + return null; + } + + /** + * Post successful login stuff - mark us online!. + * + * @throws IOException if an IO exception occurs writing the map file + */ + void postLogin() throws IOException { + writeCommands(); + + final String netIf = config.getNetInterface(); + refreshBroadcastAddress(netIf); + refreshMacAddress(netIf); + } + + /** + * Writes the commands to the commands map file if it doesn't exist. + * + * @throws IOException if an IO exception occurs writing the map file + */ + private void writeCommands() throws IOException { + if (transformService == null) { + logger.debug( + "No MAP transformation service (please install service to use this functionality) - skipping writing a map file"); + } else { + final String cmdMap = config.getCommandsMapFile(); + if (SonyUtil.isEmpty(cmdMap)) { + logger.debug("No command map defined - ignoring"); + return; + } + + final String filePath = OpenHAB.getConfigFolder() + File.separator + + TransformationService.TRANSFORM_FOLDER_NAME + File.separator + cmdMap; + final Path file = Paths.get(filePath); + if (file.toFile().exists()) { + logger.debug("Command map already defined - ignoring: {}", file); + return; + } + + logger.debug("Writing remote commands to {}", file); + Files.write(file, getDefaultCommands(), StandardCharsets.UTF_8); + } + } + + /** + * Called to refresh some of the state of the simple IP system (mainly state that either we won't get notifications + * for whose state may commonly change due to remote actions. + * + * @param includePower true to refresh power status, false otherwise + */ + void refreshState(boolean includePower) { + if (includePower) { + refreshPower(); + } + refreshVolume(); + refreshChannel(); + refreshTripletChannel(); + refreshInputSource(); + refreshPictureInPicture(); + } + + /** + * Sends the command and puts the thing into {@link ThingStatus#OFFLINE} if an IOException occurs. This method will + * create a new {@link SocketSession} for the command and a anonymous {@link SocketSessionListener} to listen for + * the result. The connection will then be closed/disposed of when a valid response is received (if the listener is + * more than 10 seconds old, it will be disposed of to avoid memory leaks). + * + * @param type the type of command ({@link #TYPE_CONTROL} or {@link #TYPE_QUERY}) + * @param command a non-null, non-empty command to send + * @param parm the non-null, non-empty parameters for the command. Must be exactly {@link #PARMSIZE} in length + */ + private void sendCommand(final char type, final String command, final String parm) { + SonyUtil.validateNotEmpty(command, "command cannot be empty"); + SonyUtil.validateNotEmpty(parm, "parm cannot be empty"); + + if (parm.length() != PARMSIZE) { + throw new IllegalArgumentException("parm must be exactly " + PARMSIZE + " in length: " + parm); + } + + final String ipAddress = config.getDeviceIpAddress(); + if (ipAddress == null || ipAddress.isEmpty()) { + throw new IllegalArgumentException("ipAddress cannot be empty"); + } + + // Create our command + final String cmd = "*S" + type + command + parm; + + // SimpleIP seems to need each request on it's own socket - so provide that here. + try { + logger.debug("Sending '{}'", cmd); + NetUtil.sendSocketRequest(ipAddress, SimpleIpConstants.PORT, cmd, new SocketSessionListener() { + @Override + public boolean responseReceived(final String response) { + final String rsp = response.trim(); // remove whitespace + // See if the response is valid + final Matcher m = RSP_NOTIFICATION.matcher(rsp); + if (m.matches() && m.groupCount() == 3) { + // make sure we only process responses for our command + if (m.group(1).equals(Character.toString(TYPE_ANSWER)) && m.group(2).equals(command)) { + logger.debug("Send '{}' result: '{}'", cmd, rsp); + if (type == TYPE_CONTROL) { + handleResponse(m, rsp, cmd); + } else if (type == TYPE_QUERY) { + handleNotification(m, rsp); + } else { + logger.debug("Unknown command type: {}", cmd); + } + return true; + } + } else if (SonyUtil.isEmpty(rsp)) { + logger.debug("Empty reponse (or unsupported command): '{}'", cmd); + } else { + logger.debug("Unparsable response '{}' to command '{}'", rsp, cmd); + } + return false; + } + + @Override + public void responseException(final IOException e) { + if (e instanceof SocketTimeoutException) { + logger.debug("(SocketTimeoutException) Response took too long - ignoring"); + } else { + SimpleIpProtocol.this.responseException(e); + } + } + }); + } catch (final IOException e) { + callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Exception occurred sending command: " + e); + } + } + + /** + * Refreshes the power status. + */ + void refreshPower() { + sendCommand(TYPE_QUERY, POWER, NO_PARM); + } + + /** + * Refreshes the volume status. + */ + void refreshVolume() { + sendCommand(TYPE_QUERY, VOLUME, NO_PARM); + } + + /** + * Refreshes the audio mute status. + */ + void refreshAudioMute() { + sendCommand(TYPE_QUERY, AUDIO_MUTE, NO_PARM); + } + + /** + * Refreshes the channel. + */ + void refreshChannel() { + sendCommand(TYPE_QUERY, CHANNEL, NO_PARM); + } + + /** + * Refreshes the channel triplet. + */ + void refreshTripletChannel() { + sendCommand(TYPE_QUERY, TRIPLET_CHANNEL, NO_PARM); + } + + /** + * Refreshes the input source. + */ + void refreshInputSource() { + sendCommand(TYPE_QUERY, INPUT_SOURCE, NO_PARM); + } + + /** + * Refreshes the input. + */ + void refreshInput() { + sendCommand(TYPE_QUERY, INPUT, NO_PARM); + } + + /** + * Refreshes the input. + */ + void refreshScene() { + sendCommand(TYPE_QUERY, SCENE, NO_PARM); + } + + /** + * Refreshes the picture mute. + */ + void refreshPictureMute() { + sendCommand(TYPE_QUERY, PICTURE_MUTE, NO_PARM); + } + + /** + * Refreshes the PIP. + */ + void refreshPictureInPicture() { + sendCommand(TYPE_QUERY, PICTURE_IN_PICTURE, NO_PARM); + } + + /** + * Refreshes the broadcast address. + * + * @param netInterface the non-null, non-empty network interface to inquire + */ + private void refreshBroadcastAddress(final String netInterface) { + SonyUtil.validateNotEmpty(netInterface, "netInterface cannot be empty"); + sendCommand(TYPE_QUERY, BROADCAST_ADDRESS, SonyUtil.rightPad(netInterface, PARMSIZE, '#')); + } + + /** + * Refreshes the mac address of the given network interface. + * + * @param netInterface the non-null, non-empty interface + */ + private void refreshMacAddress(final String netInterface) { + SonyUtil.validateNotEmpty(netInterface, "netInterface cannot be empty"); + sendCommand(TYPE_QUERY, MACADDRESS, SonyUtil.rightPad(netInterface, PARMSIZE, '#')); + } + + /** + * Sends the specified IR command to the device + * + * @param irCmd the non-null, non-empty IR command to send + */ + void setIR(final String irCmd) { + SonyUtil.validateNotEmpty(irCmd, "irCmd cannot be empty"); + + final String cmdMap = config.getCommandsMapFile(); + + String code = irCmd; + try { + final TransformationService localTransformationService = transformService; + if (localTransformationService == null) { + logger.debug( + "No map transformation service found (please install service) - code will not be transformed"); + } else if (cmdMap == null || cmdMap.isEmpty()) { + logger.debug("Commmand map file was not specified in configuration - code will not be transformed"); + } else { + final String newCode = localTransformationService.transform(cmdMap, irCmd); + if (!SonyUtil.isEmpty(newCode) && !newCode.equalsIgnoreCase(irCmd)) { + logger.debug("Transformed {} with map file '{}' to {}", irCmd, cmdMap, newCode); + code = newCode; + } + } + } catch (final TransformationException e) { + logger.debug("Failed to transform {} using map file '{}', exception={}", irCmd, cmdMap, e.getMessage()); + return; + } + + if (code == null || code.isEmpty()) { + logger.debug("Code is empty - cannot send"); + return; + } + + try { + Integer.parseInt(code); + } catch (final NumberFormatException e) { + logger.debug("The resulting code {} was not an integer", code); + } + + logger.debug("Sending code {}", code); + + sendCommand(TYPE_CONTROL, IRCC, SonyUtil.leftPad(code, PARMSIZE, '0')); + refreshState(true); + } + + /** + * Sets the power on/off to the device + * + * @param on true if on, false off + */ + void setPower(final boolean on) { + if (on) { + SonyUtil.sendWakeOnLan(logger, config.getDeviceIpAddress(), config.getDeviceMacAddress()); + } + sendCommand(TYPE_CONTROL, POWER, SonyUtil.leftPad(on ? "1" : "0", PARMSIZE, '0')); + } + + /** + * Toggles the power + */ + void togglePower() { + sendCommand(TYPE_CONTROL, TOGGLE_POWER, NO_PARM); + } + + /** + * Sets the volume level + * + * @param volume a volume between 0-100 + * @throws IllegalArgumentException if < 0 or > 100 + */ + void setAudioVolume(final int volume) { + if (volume < 0 || volume > 100) { + throw new IllegalArgumentException("volume must be between 0-100"); + } + sendCommand(TYPE_CONTROL, VOLUME, SonyUtil.leftPad(Integer.toString(volume), PARMSIZE, '0')); + } + + /** + * Sets the audio mute + * + * @param on true for muted, false otherwise + */ + void setAudioMute(final boolean on) { + sendCommand(TYPE_CONTROL, AUDIO_MUTE, SonyUtil.leftPad(on ? "1" : "0", PARMSIZE, '0')); + } + + /** + * Sets the channel. Channel must be in the form of "x.x" or simply "x" where x must be numeric + * + * @param channel the non-null, non-empty channel in the form of "x.x" or "x" (such as 50.1 or 6) + * @throws IllegalArgumentException if channel is not in the form of "x.x" or "x" where x is numeric + */ + void setChannel(final String channel) { + SonyUtil.validateNotEmpty(channel, "channel cannot be empty"); + + final int period = channel.indexOf('.'); + final String pre = SonyUtil.trimToEmpty(period < 0 ? channel : channel.substring(0, period)); + final String post = SonyUtil.trimToEmpty(period < 0 ? "0" : channel.substring(period + 1)); + try { + final int preNum = pre.isEmpty() ? 0 : Integer.parseInt(pre); + final int postNum = post.isEmpty() ? 0 : Integer.parseInt(post); + final String cmd = SonyUtil.leftPad(Integer.toString(preNum), 8, '0') + "." + + SonyUtil.rightPad(Integer.toString(postNum), 7, '0'); + sendCommand(TYPE_CONTROL, CHANNEL, cmd); + + } catch (final NumberFormatException e) { + throw new IllegalArgumentException("channel could not be parsed: " + channel); + } + } + + /** + * Sets the triplet channel. Channel must be in the form of "x.x.x" where x must be numeric + * + * @param channel the non-null, non-empty channel in the form of "x.x.x" (such as 32736.32736.1024) + * @throws IllegalArgumentException if channel is not in the form of "x.x.x" + */ + void setTripletChannel(final String channel) { + SonyUtil.validateNotEmpty(channel, "channel cannot be empty"); + + final int firstPeriod = channel.indexOf('.'); + if (firstPeriod < 0) { + throw new IllegalArgumentException( + "Could not find the number of the first part of the triplet channel: " + channel); + } + + final int secondPeriod = channel.indexOf(',', firstPeriod + 1); + if (firstPeriod < 0) { + throw new IllegalArgumentException( + "Could not find the number of the second part of the triplet channel: " + channel); + } + + final String first = SonyUtil.trimToEmpty(channel.substring(0, firstPeriod)); + final String second = SonyUtil.trimToEmpty(channel.substring(firstPeriod + 1, secondPeriod)); + final String third = SonyUtil.trimToEmpty(channel.substring(secondPeriod + 1)); + try { + final int firstNum = first.isEmpty() ? 0 : Integer.parseInt(first); + final int secondNum = second.isEmpty() ? 0 : Integer.parseInt(second); + final int thirdNum = third.isEmpty() ? 0 : Integer.parseInt(third); + + final String firstHex = SonyUtil.leftPad(Integer.toHexString(firstNum), 4, '0'); + final String secondHex = SonyUtil.leftPad(Integer.toHexString(secondNum), 4, '0'); + final String thirdHex = SonyUtil.leftPad(Integer.toHexString(thirdNum), 4, '0'); + + sendCommand(TYPE_CONTROL, CHANNEL, firstHex + secondHex + thirdHex + "####"); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException("channel could not be parsed: " + channel); + } + } + + /** + * Sets the input source and will refresh channel and triplet channel afterwards. This must be a valid string + * recognized by the simple IP device. + * + * @param source a non-null, non-empty input source + */ + void setInputSource(final String source) { + SonyUtil.validateNotEmpty(source, "source cannot be empty"); + + sendCommand(TYPE_CONTROL, INPUT_SOURCE, SonyUtil.rightPad(source, PARMSIZE, '#')); + } + + /** + * Sets the scene. This must be a valid string recognized by the simple IP device. + * + * @param scene a non-null, non-empty scene name + */ + void setScene(final String scene) { + SonyUtil.validateNotEmpty(scene, "scene cannot be empty"); + + sendCommand(TYPE_CONTROL, SCENE, SonyUtil.rightPad(scene, PARMSIZE, '#')); + } + + /** + * Sets the input port and will refresh input source, channel and triplet channel afterwards. This must be a valid + * in the form of "xxxxyyyy" where xxxx is the name of the input and yyyy is the port number (like "hdmi1"). The + * valid input names are "TV", "HDMI", "SCART", "Composite", "Component", "Screen Mirroring", and "PC RGB Input" + * (case doesn't matter). The port number does NOT apply to "TV". + * + * @param input a non-null, non-empty input port + */ + void setInput(final String input) { + SonyUtil.validateNotEmpty(input, "input cannot be empty"); + + int typeCode = -1; + int portNbr = -1; + final String lowerInput = input.toLowerCase(); + for (final Entry entry : INPUT_TYPES.entrySet()) { + if (lowerInput.startsWith(entry.getValue().toLowerCase())) { + typeCode = entry.getKey(); + if (typeCode != INPUT_TV) { + try { + final String portS = SonyUtil.trimToEmpty(input.substring(entry.getValue().length())); + portNbr = portS.isEmpty() ? 0 : Integer.parseInt(portS); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException( + "The port number on the input is invalid (not an integer): " + input); + } + } else { + portNbr = 0; + } + break; + } + } + if (typeCode == -1) { + throw new IllegalArgumentException("Unknown input: " + input); + } + sendCommand(TYPE_CONTROL, INPUT, SonyUtil.leftPad(Integer.toString(typeCode), 12, '0') + + SonyUtil.leftPad(Integer.toString(portNbr), 4, '0')); + } + + /** + * Sets the picture mute + * + * @param on true for muted, false otherwise + */ + void setPictureMute(final boolean on) { + sendCommand(TYPE_CONTROL, PICTURE_MUTE, SonyUtil.leftPad(on ? "1" : "0", PARMSIZE, '0')); + } + + /** + * Toggles the picture mute + */ + void togglePictureMute() { + sendCommand(TYPE_CONTROL, TOGGLE_PICTURE_MUTE, NO_PARM); + } + + /** + * Sets the PIP enabling + * + * @param on true to enable, false otherwise + */ + void setPictureInPicture(final boolean on) { + sendCommand(TYPE_CONTROL, PICTURE_IN_PICTURE, SonyUtil.leftPad(on ? "1" : "0", PARMSIZE, '0')); + } + + /** + * Toggles PIP enabling + */ + void togglePictureInPicture() { + sendCommand(TYPE_CONTROL, TOGGLE_PICTURE_IN_PICTURE, NO_PARM); + } + + /** + * Toggles the PIP position + */ + void togglePipPosition() { + sendCommand(TYPE_CONTROL, TOGGLE_PIP_POSITION, NO_PARM); + } + + /** + * Handles control responses from commands (*SC->*SA). + * + * @param m a non-null matcher + * @param response the non-null, possibly empty response + * @param command the possibly null, possibly empty command that triggered this response + */ + private void handleResponse(final Matcher m, final String response, final @Nullable String command) { + Objects.requireNonNull(m, "m cannot be null"); + Objects.requireNonNull(response, "response cannot be null"); + + if (m.groupCount() == 3) { + final String cmd = m.group(2); + final String parms = m.group(3); + + if (IRCC.equalsIgnoreCase(cmd)) { + handleIRResponse(parms); + } else if (POWER.equalsIgnoreCase(cmd)) { + handlePowerResponse(parms); + } else if (TOGGLE_POWER.equalsIgnoreCase(cmd)) { + handleTogglePowerResponse(parms); + } else if (VOLUME.equalsIgnoreCase(cmd)) { + handleAudioVolumeResponse(parms); + } else if (AUDIO_MUTE.equalsIgnoreCase(cmd)) { + handleAudioMuteResponse(parms); + } else if (CHANNEL.equalsIgnoreCase(cmd)) { + handleChannelResponse(parms); + } else if (TRIPLET_CHANNEL.equalsIgnoreCase(cmd)) { + handleTripletChannelResponse(parms); + } else if (INPUT_SOURCE.equalsIgnoreCase(cmd)) { + handleInputSourceResponse(parms); + } else if (INPUT.equalsIgnoreCase(cmd)) { + handleInputResponse(parms); + } else if (SCENE.equalsIgnoreCase(cmd)) { + handleSceneResponse(parms); + } else if (PICTURE_MUTE.equalsIgnoreCase(cmd)) { + handlePictureMuteResponse(parms); + } else if (TOGGLE_PICTURE_MUTE.equalsIgnoreCase(cmd)) { + handleTogglePictureMuteResponse(parms); + } else if (PICTURE_IN_PICTURE.equalsIgnoreCase(cmd)) { + handlePictureInPictureResponse(parms); + } else if (TOGGLE_PICTURE_IN_PICTURE.equalsIgnoreCase(cmd)) { + handleTogglePictureInPictureResponse(parms); + } else if (TOGGLE_PIP_POSITION.equalsIgnoreCase(cmd)) { + handleTogglePIPPosition(parms); + } else { + logger.debug("Unknown command response '{}' to command '{}' ", response, command); + } + } + } + + /** + * Handles notification messages (*SN) and query responses (*SE->*SA). + * + * @param m a non-null matcher + * @param response the non-null, possibly empty response + */ + private void handleNotification(final Matcher m, final String response) { + Objects.requireNonNull(m, "m cannot be null"); + Objects.requireNonNull(response, "response cannot be null"); + + if (m.groupCount() == 3) { + final String cmd = m.group(2); + final String parms = m.group(3); + + if (POWER.equalsIgnoreCase(cmd)) { + handlePowerNotification(parms); + } else if (VOLUME.equalsIgnoreCase(cmd)) { + handleAudioVolumeNotification(parms); + } else if (AUDIO_MUTE.equalsIgnoreCase(cmd)) { + handleAudioMuteNotification(parms); + } else if (CHANNEL.equalsIgnoreCase(cmd)) { + handleChannelNotification(parms); + } else if (TRIPLET_CHANNEL.equalsIgnoreCase(cmd)) { + handleTripletChannelNotification(parms); + } else if (INPUT_SOURCE.equalsIgnoreCase(cmd)) { + handleInputSourceNotification(parms); + } else if (INPUT.equalsIgnoreCase(cmd)) { + handleInputNotification(parms); + } else if (SCENE.equalsIgnoreCase(cmd)) { + handleSceneNotification(parms); + } else if (PICTURE_MUTE.equalsIgnoreCase(cmd)) { + handlePictureMuteNotification(parms); + } else if (PICTURE_IN_PICTURE.equalsIgnoreCase(cmd)) { + handlePictureInPictureNotification(parms); + } else if (BROADCAST_ADDRESS.equalsIgnoreCase(cmd)) { + handleBroadcastAddressResponse(parms); + } else if (MACADDRESS.equalsIgnoreCase(cmd)) { + handleMacAddressResponse(parms); + } else { + logger.debug("Unknown notification: {}", response); + } + } + } + + /** + * Handles the IRCC command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleIRResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", IRCC, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", IRCC, parms); + } else { + logger.debug("Unknown {} response: {}", IRCC, parms); + } + } + + /** + * Handles the POWR command response. + * + * @param parms a non-null, possibly empty response + */ + private void handlePowerResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", POWER, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", POWER, parms); + } else { + logger.debug("Unknown {} response: {}", POWER, parms); + } + } + + /** + * Handles the TPOW command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleTogglePowerResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", TOGGLE_POWER, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", TOGGLE_POWER, parms); + } else { + logger.debug("Unknown {} response: {}", TOGGLE_POWER, parms); + } + } + + /** + * Handles the power notification/query response. + * + * @param parms a non-null, possibly empty response + */ + private void handlePowerNotification(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", POWER, parms); + } else { + try { + final int power = SonyUtil.isEmpty(parms) ? 0 : Integer.parseInt(parms); + if (power == 0) { + callback.stateChanged(SimpleIpConstants.CHANNEL_POWER, OnOffType.OFF); + } else if (power == 1) { + callback.stateChanged(SimpleIpConstants.CHANNEL_POWER, OnOffType.ON); + } else { + logger.debug("Unknown {} response: {}", POWER, parms); + } + } catch (final NumberFormatException e) { + logger.debug("Unparsable {} response: {}", POWER, parms); + } + + refreshState(false); + } + } + + /** + * Handles the audio volume command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleAudioVolumeResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", VOLUME, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", VOLUME, parms); + } else { + logger.debug("Unknown {} response: {}", VOLUME, parms); + } + } + + /** + * Handles the audio volume notification/query response. + * + * @param parms a non-null, possibly empty response + */ + private void handleAudioVolumeNotification(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", VOLUME, parms); + // you'll get error when tv is off/muted + callback.stateChanged(SimpleIpConstants.CHANNEL_VOLUME, new PercentType(0)); + } else { + try { + final int volume = SonyUtil.isEmpty(parms) ? 0 : Integer.parseInt(parms); + callback.stateChanged(SimpleIpConstants.CHANNEL_VOLUME, new PercentType(volume)); + } catch (final NumberFormatException e) { + logger.debug("Unparsable {} response: {}", VOLUME, parms); + } + } + } + + /** + * Handles the audio mute command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleAudioMuteResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", AUDIO_MUTE, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", AUDIO_MUTE, parms); + } else { + logger.debug("Unknown {} response: {}", AUDIO_MUTE, parms); + } + } + + /** + * Handles the audio mute notification/query response. + * + * @param parms a non-null, possibly empty response + */ + private void handleAudioMuteNotification(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", AUDIO_MUTE, parms); + } else { + try { + final int mute = SonyUtil.isEmpty(parms) ? 0 : Integer.parseInt(parms); + if (mute == 0) { + callback.stateChanged(SimpleIpConstants.CHANNEL_AUDIOMUTE, OnOffType.OFF); + } else if (mute == 1) { + callback.stateChanged(SimpleIpConstants.CHANNEL_AUDIOMUTE, OnOffType.ON); + } else { + logger.debug("Unknown {} response: {}", AUDIO_MUTE, parms); + } + } catch (final NumberFormatException e) { + logger.debug("Unparsable {} response: {}", AUDIO_MUTE, parms); + } + } + } + + /** + * Handles the channel command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleChannelResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", CHANNEL, parms); + callback.stateChanged(SimpleIpConstants.CHANNEL_CHANNEL, StringType.EMPTY); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", CHANNEL, parms); + } else if (RSP_NOSUCHTHING.equals(parms)) { + logger.debug("{} command invalid: {}", CHANNEL, parms); + callback.stateChanged(SimpleIpConstants.CHANNEL_CHANNEL, StringType.EMPTY); + } else { + logger.debug("Unknown {} response: {}", CHANNEL, parms); + } + } + + /** + * Handles the channel notification/query response. + * + * @param parms a non-null, possibly empty response + */ + private void handleChannelNotification(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", CHANNEL, parms); + callback.stateChanged(SimpleIpConstants.CHANNEL_CHANNEL, StringType.EMPTY); + } else { + try { + final int idx = parms.indexOf('.'); + if (idx >= 0) { + final String preS = SonyUtil.trimToEmpty(SonyUtil.stripStart(parms.substring(0, idx), "0")); + final String postS = SonyUtil.trimToEmpty(SonyUtil.stripEnd(parms.substring(idx + 1), "0")); + final int pre = preS.isEmpty() ? 0 : Integer.parseInt(preS); + final int post = postS.isEmpty() ? 0 : Integer.parseInt(postS); + callback.stateChanged(SimpleIpConstants.CHANNEL_CHANNEL, new StringType(pre + "." + post)); + } else { + logger.debug("Unparsable {} response: {}", CHANNEL, parms); + } + } catch (final NumberFormatException e) { + logger.debug("Unparsable {} response: {}", CHANNEL, parms); + } + } + } + + /** + * Handles the triplet channel command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleTripletChannelResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", TRIPLET_CHANNEL, parms); + callback.stateChanged(SimpleIpConstants.CHANNEL_TRIPLETCHANNEL, StringType.EMPTY); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", TRIPLET_CHANNEL, parms); + } else if (RSP_NOSUCHTHING.equals(parms)) { + logger.debug("{} command invalid: {}", TRIPLET_CHANNEL, parms); + callback.stateChanged(SimpleIpConstants.CHANNEL_TRIPLETCHANNEL, StringType.EMPTY); + } else { + logger.debug("Unknown {} response: {}", TRIPLET_CHANNEL, parms); + } + } + + /** + * Handles the triplet channel command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleTripletChannelNotification(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + callback.stateChanged(SimpleIpConstants.CHANNEL_TRIPLETCHANNEL, StringType.EMPTY); + } else { + if (parms.length() >= 12) { + try { + final String firstS = SonyUtil.trimToEmpty(parms.substring(0, 4)); + final String secondS = SonyUtil.trimToEmpty(parms.substring(4, 8)); + final String thirdS = SonyUtil.trimToEmpty(SonyUtil.stripEnd(parms.substring(9, 13), "#")); + final int first = firstS.isEmpty() ? 0 : Integer.parseInt(firstS, 16); + final int second = secondS.isEmpty() ? 0 : Integer.parseInt(secondS, 16); + final int third = thirdS.isEmpty() ? 0 : Integer.parseInt(thirdS, 16); + + callback.stateChanged(SimpleIpConstants.CHANNEL_TRIPLETCHANNEL, + new StringType(first + "." + second + "." + third)); + + } catch (final NumberFormatException e) { + logger.debug("Unparsable triplet channel response: {}", parms); + } + } else { + logger.debug("Unparsable triplet channel response: {}", parms); + } + } + } + + /** + * Handles the input command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleInputSourceResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", INPUT_SOURCE, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", INPUT_SOURCE, parms); + } else if (RSP_NOSUCHTHING.equals(parms)) { + logger.debug("{} response is no such input: {}", INPUT_SOURCE, parms); + } else { + logger.debug("Unknown {} response: {}", INPUT_SOURCE, parms); + } + } + + /** + * Handles the input source command response/notification. + * + * @param parms a non-null, possibly empty response + */ + private void handleInputSourceNotification(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", INPUT_SOURCE, parms); + callback.stateChanged(SimpleIpConstants.CHANNEL_INPUTSOURCE, StringType.EMPTY); + } else { + final int del = parms.indexOf('#'); + if (del >= 0) { + callback.stateChanged(SimpleIpConstants.CHANNEL_INPUTSOURCE, new StringType(parms.substring(0, del))); + } else { + callback.stateChanged(SimpleIpConstants.CHANNEL_INPUTSOURCE, new StringType(parms)); + } + + refreshChannel(); + refreshTripletChannel(); + } + } + + /** + * Handles the input command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleInputResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", INPUT, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", INPUT, parms); + } else if (RSP_NOSUCHTHING.equals(parms)) { + logger.debug("{} response is no such input: {}", INPUT, parms); + } else { + logger.debug("Unknown {} response: {}", INPUT, parms); + } + } + + /** + * Handles the input notification/inquiry response. + * + * @param parms a non-null, possibly empty response + */ + private void handleInputNotification(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", INPUT, parms); + } else { + if (parms.length() >= 13) { + try { + final String inputS = SonyUtil.trimToEmpty(parms.substring(0, 12)); + final String portS = SonyUtil.trimToEmpty(parms.substring(13)); + final int inputType = inputS.isEmpty() ? 0 : Integer.parseInt(inputS); + final int portNbr = portS.isEmpty() ? 0 : Integer.parseInt(portS); + + // workaround to @NonNullByDefault and maps.get issue + final String inputName = INPUT_TYPES.containsKey(inputType) ? INPUT_TYPES.get(inputType) : null; + if (inputName == null) { + logger.debug("Unknown {} name for code: {}", INPUT, parms); + } else { + callback.stateChanged(SimpleIpConstants.CHANNEL_INPUT, + new StringType(inputName + (inputType != INPUT_TV ? portNbr : ""))); + + refreshChannel(); + refreshTripletChannel(); + refreshInputSource(); + } + } catch (final NumberFormatException e) { + logger.debug("Unparsable {} response: {}", INPUT, parms); + } + } else { + logger.debug("Unparsable {} response: {}", INPUT, parms); + } + } + } + + /** + * Handles the scene command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleSceneResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", SCENE, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", SCENE, parms); + } else if (RSP_NOSUCHTHING.equals(parms)) { + logger.debug("{} response is no such input: {}", SCENE, parms); + } else { + logger.debug("Unknown {} response: {}", SCENE, parms); + } + } + + /** + * Handles the scene notification/inquiry response. + * + * @param parms a non-null, possibly empty response + */ + private void handleSceneNotification(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", SCENE, parms); + } else { + final int del = parms.indexOf('#'); + if (del >= 0) { + callback.stateChanged(SimpleIpConstants.CHANNEL_SCENE, new StringType(parms.substring(0, del))); + } else { + callback.stateChanged(SimpleIpConstants.CHANNEL_SCENE, new StringType(parms)); + } + } + } + + /** + * Handles the picture mute command response. + * + * @param parms a non-null, possibly empty response + */ + private void handlePictureMuteResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", PICTURE_MUTE, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", PICTURE_MUTE, parms); + } else { + logger.debug("Unknown {} response: {}", PICTURE_MUTE, parms); + } + } + + /** + * Handles the picture mute notification/query response. + * + * @param parms a non-null, possibly empty response + */ + private void handlePictureMuteNotification(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", PICTURE_MUTE, parms); + } else { + try { + final int mute = SonyUtil.isEmpty(parms) ? 0 : Integer.parseInt(parms); + if (mute == 0) { + callback.stateChanged(SimpleIpConstants.CHANNEL_PICTUREMUTE, OnOffType.OFF); + } else if (mute == 1) { + callback.stateChanged(SimpleIpConstants.CHANNEL_PICTUREMUTE, OnOffType.ON); + } else { + logger.debug("Unknown {} response: {}", PICTURE_MUTE, parms); + } + } catch (final NumberFormatException e) { + logger.debug("Unparsable {} response: {}", PICTURE_MUTE, parms); + } + } + } + + /** + * Handles the toggle picture mute command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleTogglePictureMuteResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", TOGGLE_PICTURE_MUTE, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", TOGGLE_PICTURE_MUTE, parms); + } else { + logger.debug("Unknown {} response: {}", TOGGLE_PICTURE_MUTE, parms); + } + } + + /** + * Handles the PIP command response. + * + * @param parms a non-null, possibly empty response + */ + private void handlePictureInPictureResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", PICTURE_IN_PICTURE, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", PICTURE_IN_PICTURE, parms); + } else { + logger.debug("Unknown {} response: {}", PICTURE_IN_PICTURE, parms); + } + } + + /** + * Handles the PIP notification/query response. + * + * @param parms a non-null, possibly empty response + */ + private void handlePictureInPictureNotification(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", PICTURE_IN_PICTURE, parms); + } else { + try { + final int enabled = SonyUtil.isEmpty(parms) ? 0 : Integer.parseInt(parms); + if (enabled == 0) { + callback.stateChanged(SimpleIpConstants.CHANNEL_PICTUREINPICTURE, OnOffType.OFF); + } else if (enabled == 1) { + callback.stateChanged(SimpleIpConstants.CHANNEL_PICTUREINPICTURE, OnOffType.ON); + } else { + logger.debug("Unknown {} response: {}", PICTURE_IN_PICTURE, parms); + } + } catch (final NumberFormatException e) { + logger.debug("Unparsable {} response: {}", PICTURE_IN_PICTURE, parms); + } + } + } + + /** + * Handles the toggle PIP command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleTogglePictureInPictureResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", TOGGLE_PICTURE_IN_PICTURE, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", TOGGLE_PICTURE_IN_PICTURE, parms); + } else { + logger.debug("Unknown {} response: {}", TOGGLE_PICTURE_IN_PICTURE, parms); + } + } + + /** + * Handles the toggle PIP position command response. + * + * @param parms a non-null, possibly empty response + */ + private void handleTogglePIPPosition(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", TOGGLE_PIP_POSITION, parms); + } else if (RSP_SUCCESS.equals(parms)) { + logger.trace("{} command succeeded: {}", TOGGLE_PIP_POSITION, parms); + } else { + logger.debug("Unknown {} response: {}", TOGGLE_PIP_POSITION, parms); + } + } + + /** + * Handles the broadcast query response. + * + * @param parms a non-null, possibly empty response + */ + private void handleBroadcastAddressResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", BROADCAST_ADDRESS, parms); + } else { + final int del = parms.indexOf('#'); + if (del >= 0) { + callback.setProperty(SimpleIpConstants.PROP_BROADCASTADDRESS, parms.substring(0, del)); + } else { + callback.setProperty(SimpleIpConstants.PROP_BROADCASTADDRESS, parms); + } + } + } + + /** + * Handles the mac address query response. + * + * @param parms a non-null, possibly empty response + */ + private void handleMacAddressResponse(final String parms) { + Objects.requireNonNull(parms, "parms cannot be null"); + + if (RSP_ERROR.equals(parms)) { + logger.debug("{} command failed: {}", MACADDRESS, parms); + } else { + final StringBuffer sb = new StringBuffer(); + + final int max = parms.length(); + for (int x = 0; x < max; x++) { + final char myChar = parms.charAt(x); + if (myChar == '#') { + break; + } + if (x > 0 && x % 2 == 0) { + sb.append(':'); + } + sb.append(myChar); + } + callback.setProperty(SimpleIpConstants.PROP_MACADDRESS, sb.toString()); + } + } + + @Override + public boolean responseReceived(final String response) { + Objects.requireNonNull(response, "response cannot be null"); + + if (SonyUtil.isEmpty(response)) { + return true; + } + + final Matcher m = RSP_NOTIFICATION.matcher(response); + if (m.matches()) { + handleNotification(m, response); + return true; + } + + logger.debug("Unparsable notification: {}", response); + return true; + } + + @Override + public void responseException(final IOException e) { + Objects.requireNonNull(e, "e cannot be null"); + + logger.debug("Exception occurred reading from the socket: {}", e.getMessage(), e); + callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Exception occurred reading from the socket: " + e.getMessage()); + } + + /** + * Helper method to simply return the default commands for Simple IP control. + * + * @return a non-null, non-empty list of commands + */ + private List getDefaultCommands() { + return DEFAULT_CMDS; + } + + @Override + public void close() throws IOException { + listeningSession.removeListener(this); + listeningSession.disconnect(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/AbstractSonyTransport.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/AbstractSonyTransport.java new file mode 100644 index 0000000000000..0ba0b6c4a7670 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/AbstractSonyTransport.java @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.net.Header; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebEvent; + +/** + * This class provides an abstract base to all sony transports and provides the following: + *
    + *
  1. The base URI to be used
  2. + *
  3. Management of listeners
  4. + *
  5. Global transport options
  6. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractSonyTransport implements SonyTransport { + + /** The base URI used by the transport */ + private final URI baseUri; + + /** The listeners of the transport */ + private final List listeners = new CopyOnWriteArrayList<>(); + + /** Global transport options */ + private final List options = new CopyOnWriteArrayList<>(); + + /** + * Constructs the transport using the base URI + * + * @param baseUri a non-null base URI + */ + protected AbstractSonyTransport(final URI baseUri) { + Objects.requireNonNull(baseUri, "baseUri cannot be null"); + this.baseUri = baseUri; + } + + @Override + public URI getBaseUri() { + return baseUri; + } + + @Override + public void addListener(final SonyTransportListener listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + listeners.add(listener); + } + + @Override + public boolean removeListener(final SonyTransportListener listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + return listeners.remove(listener); + } + + /** + * Fires an onError message to all listeners + * + * @param error a non-null thrown error + */ + protected void fireOnError(final Throwable error) { + Objects.requireNonNull(error, "error cannot be null"); + for (final SonyTransportListener listener : listeners) { + listener.onError(error); + } + } + + /** + * First a onEvent message to all listeners + * + * @param event a non-null event + */ + protected void fireEvent(final ScalarWebEvent event) { + Objects.requireNonNull(event, "event cannot be null"); + for (final SonyTransportListener listener : listeners) { + listener.onEvent(event); + } + } + + @Override + public void setOption(final TransportOption option) { + if (option instanceof TransportOptionHeader header) { + final String headerName = header.getHeader().getName(); + options.removeIf(entry -> entry instanceof TransportOptionHeader + && headerName.equalsIgnoreCase(((TransportOptionHeader) entry).getHeader().getName())); + } else { + final Class optionClass = option.getClass(); + options.removeIf(entry -> optionClass.equals(entry.getClass())); + } + options.add(option); + } + + @Override + public void removeOption(final TransportOption option) { + Objects.requireNonNull(option, "option cannot be null"); + options.remove(option); + } + + @Override + public List getOptions() { + return Collections.unmodifiableList(options); + } + + /** + * Helper method to get all options that are assignable to the passed option class - regardless if it's a global + * option or passed options + * + * @param clazz the non-null class to use + * @param options a possibly empty list of local options to query + * @return a non-null, possibly empty list of matching options + */ + @SuppressWarnings("unchecked") + protected List getOptions(final Class clazz, final TransportOption... options) { + Objects.requireNonNull(clazz, "clazz cannot be null"); + return Stream.concat(Arrays.stream(options), this.options.stream()) + .filter(obj -> obj.getClass().isAssignableFrom(clazz)).map(obj -> (O) obj).collect(Collectors.toList()); + } + + /** + * Helper method to determine if a specific option is part of either the global options or the passed options + * + * @param clazz the non-null class of the option + * @param options the local options to also check + * @return true if found, false otherwise + */ + protected boolean hasOption(final Class clazz, final TransportOption... options) { + Objects.requireNonNull(clazz, "clazz cannot be null"); + return !getOptions(clazz, options).isEmpty(); + } + + /** + * Helper method to get all the headers from both the global options and the passed options + * + * @param options the local options to also check + * @return a non-null, but possibly empty array of headers + */ + protected Header[] getHeaders(final TransportOption... options) { + return getOptions(TransportOptionHeader.class, options).stream().map(h -> h.getHeader()) + .collect(Collectors.toList()).toArray(new Header[0]); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyAuthFilter.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyAuthFilter.java new file mode 100644 index 0000000000000..c645f3543b743 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyAuthFilter.java @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.URI; +import java.util.Date; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.scalarweb.gson.GsonUtilities; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebMethod; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebRequest; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebService; +import org.openhab.binding.sony.internal.scalarweb.models.api.ActRegisterId; +import org.openhab.binding.sony.internal.scalarweb.models.api.ActRegisterOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * This class represents authorization filter used to reauthorize our sony connection + * + * @author Tim Roberts - Initial contribution + * @author andan - Changed cookie handling + */ +@NonNullByDefault +public class SonyAuthFilter implements ClientRequestFilter, ClientResponseFilter { + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(SonyAuthFilter.class); + + /** The map to hold authorization cookies per host */ + private static final ConcurrentMap hostAuthCookieMap = new ConcurrentHashMap<>(); + private static final NewCookie EMPTY_COOKIE = new NewCookie("auth", ""); + + /** The name of the authorization cookie */ + private static final String AUTHCOOKIENAME = "auth"; + + /** The base URL of the access control service */ + private final URI baseUri; + + /** The host for which the access control service applies */ + private final String host; + + /** The function interface to determine whether we should automatically apply the auth logic */ + private final AutoAuth autoAuth; + + /** + * The clientBuilder used in HttpRequest + */ + private final ClientBuilder clientBuilder; + + /** + * A boolean to determine if we've already tried the authorization and failed (true to continue trying, false + * otherwise) + */ + private final AtomicBoolean tryAuth = new AtomicBoolean(true); + + /** + * Instantiates a new sony auth filter from the device information + * + * @param baseUri the non-null, base URI for the access control service + * @param autoAuth the non-null auto auth callback + * @param clientBuilder the client builder used to request new authorization cookie if needed + */ + public SonyAuthFilter(final URI baseUri, final AutoAuth autoAuth, final ClientBuilder clientBuilder) { + Objects.requireNonNull(baseUri, "baseUrl cannot be empty"); + Objects.requireNonNull(autoAuth, "autoAuth cannot be null"); + this.baseUri = baseUri; + this.autoAuth = autoAuth; + this.host = baseUri.getHost(); + // this.autoAuth = () -> true; + this.clientBuilder = clientBuilder; + logger.debug( + "Created SonyAuthFilter for host: {}, baseUri: {}, isAutoAuth: {} with initial authorization cookie: {} having expiry date: {}", + host, baseUri.getPath(), this.autoAuth.isAutoAuth(), hostAuthCookieMap.getOrDefault(host, EMPTY_COOKIE), + hostAuthCookieMap.getOrDefault(host, EMPTY_COOKIE).getExpiry()); + } + + @Override + public void filter(final @Nullable ClientRequestContext requestCtx) throws IOException { + Objects.requireNonNull(requestCtx, "requestCtx cannot be null"); + + // get expiry date from cookie and check for expiry + // Note: ConcurrentHashMap is optimzed for get, so only apply putIfAbsent if necessary + final Date expiryDate = hostAuthCookieMap.computeIfAbsent(host, k -> EMPTY_COOKIE).getExpiry(); + // NewCookie authCookie = hostAuthCookieMap.getOrDefault(host, EMPTY_COOKIE); + if (expiryDate == null || new Date().after(expiryDate)) { + logger.debug("SonyAuthFilter for baseUri: {} Cookie {} expired or null, isAutoAuth {}", baseUri, expiryDate, + autoAuth.isAutoAuth()); + + if (tryAuth.get() && autoAuth.isAutoAuth()) { + // request new cookie value for host + logger.debug("SonyAuthFilter for baseUri: {} Trying to renew our authorization cookie for host: {}", + baseUri, host); + NewCookie newAuthCookie = hostAuthCookieMap.compute(host, (k, v) -> computeAndSetNewAuthCookie()); + if (!EMPTY_COOKIE.equals(newAuthCookie)) { + logger.debug("New auth cookie: {} with expiry date: {} for host: {}", newAuthCookie.getValue(), + newAuthCookie.getExpiry(), host); + } else { + logger.debug("No authorization cookie was returned"); + } + } + } + requestCtx.getHeaders().putSingle("Cookie", hostAuthCookieMap.get(host).toCookie()); + } + + private NewCookie computeAndSetNewAuthCookie() { + final Client client = clientBuilder.build(); + final String actControlUrl = NetUtil.getSonyUri(baseUri, ScalarWebService.ACCESSCONTROL); + + final WebTarget target = client.target(actControlUrl); + final Gson gson = GsonUtilities.getDefaultGson(); + + final String json = gson.toJson(new ScalarWebRequest(ScalarWebMethod.ACTREGISTER, ScalarWebMethod.V1_0, + new ActRegisterId(), new Object[] { new ActRegisterOptions() })); + + try { + final Response rsp = target.request().post(Entity.json(json)); + + final Map newCookies = rsp.getCookies(); + if (newCookies != null) { + final NewCookie newAuthCookie = newCookies.get(AUTHCOOKIENAME); + if (newAuthCookie != null) { + // create cookie with expiry date using 80% of maxAge given in seconds + final Date newExpiryDate = new Date(new Date().getTime() + 800L * newAuthCookie.getMaxAge()); + final NewCookie newAuthCookieToStore = new NewCookie(newAuthCookie.toCookie(), + newAuthCookie.getComment(), newAuthCookie.getMaxAge(), newExpiryDate, + newAuthCookie.isSecure(), newAuthCookie.isHttpOnly()); + return newAuthCookieToStore; + } + } + } catch (final ProcessingException e) { + if (e.getCause() instanceof ConnectException) { + tryAuth.set(false); + } + logger.debug("Could not renew authorization cookie: {}", e.getMessage()); + } + return EMPTY_COOKIE; + } + + @Override + public void filter(final @Nullable ClientRequestContext requestCtx, + final @Nullable ClientResponseContext responseCtx) throws IOException { + Objects.requireNonNull(responseCtx, "responseCtx cannot be null"); + + // The response may include an auth cookie that we need to save + final Map newCookies = responseCtx.getCookies(); + if (newCookies != null && !newCookies.isEmpty()) { + final NewCookie authCookie = newCookies.get(AUTHCOOKIENAME); + if (authCookie != null) { + logger.debug("New auth cookie: {} for host: {}", authCookie.getValue(), host); + hostAuthCookieMap.put(host, authCookie); + logger.debug("Auth cookie found and saved"); + } + } + } + + /** + * This is the functional interface to determing if auto auth is needed + */ + public interface AutoAuth { + /** + * Determines if auto auth is needed + * + * @return true if needed, false otherwise + */ + boolean isAutoAuth(); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyContentTypeFilter.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyContentTypeFilter.java new file mode 100644 index 0000000000000..71d4f4fbf56a0 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyContentTypeFilter.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class will filter all content-type headers to remove any double-quotes or quotes from the content-type. Sony + * devices weren't exactly written to spec and cannot handle either. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyContentTypeFilter implements ClientResponseFilter { + @Override + public void filter(final @Nullable ClientRequestContext requestCtx, + final @Nullable ClientResponseContext responseCtx) throws IOException { + Objects.requireNonNull(responseCtx, "responseCtx cannot be null"); + + final List contentValues = responseCtx.getHeaders().remove("content-type"); + if (contentValues != null) { + final List newValues = contentValues.stream().map(e -> e.replaceAll("[\"\']", "")) + .collect(Collectors.toList()); + responseCtx.getHeaders().put("content", newValues); + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyHttpTransport.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyHttpTransport.java new file mode 100644 index 0000000000000..db4ad045176e2 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyHttpTransport.java @@ -0,0 +1,383 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.net.Header; +import org.openhab.binding.sony.internal.net.HttpRequest; +import org.openhab.binding.sony.internal.net.HttpResponse; +import org.openhab.binding.sony.internal.net.NetUtil; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebRequest; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * This implementation of a sony transport will simply communicate over HTTP (or HTTPS) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyHttpTransport extends AbstractSonyTransport { + /** The logger */ + protected Logger logger = LoggerFactory.getLogger(SonyHttpTransport.class); + + /** The HTTP request object to use */ + private final HttpRequest requestor; + + /** GSON used to serialize/deserialize objects */ + private final Gson gson; + + /** + * Constructs the transport using the specified URL and gson (for serialization) + * + * @param baseUrl a non-null base URL + * @param gson a non-null GSON to use for serialzation + * @param clientBuilder a non-null client builder + * @throws URISyntaxException if the base URL has a bad syntax + */ + public SonyHttpTransport(final String baseUrl, final Gson gson, final ClientBuilder clientBuilder) + throws URISyntaxException { + super(new URI(baseUrl)); + Objects.requireNonNull(gson, "gson cannot be null"); + + requestor = new HttpRequest(clientBuilder); + + requestor.addHeader("User-Agent", SonyBindingConstants.NET_USERAGENT); + requestor.addHeader("X-CERS-DEVICE-INFO", SonyBindingConstants.NET_USERAGENT); + requestor.addHeader("X-CERS-DEVICE-ID", NetUtil.getDeviceId()); + requestor.addHeader("Connection", "close"); + + this.requestor.register(new SonyContentTypeFilter()); + this.requestor.register(new SonyAuthFilter(getBaseUri(), () -> { + final boolean authNeeded = getOptions(TransportOptionAutoAuth.class).stream() + .anyMatch(e -> e == TransportOptionAutoAuth.TRUE); + return authNeeded; + }, clientBuilder)); + this.setOption(TransportOptionAutoAuth.FALSE); + + this.gson = gson; + } + + @Override + public CompletableFuture execute(final TransportPayload payload, + final TransportOption... options) { + final Stream oldOptions = getOptions(TransportOptionAutoAuth.class).stream(); + final TransportOptionAutoAuth oldAutoAuth = oldOptions.findFirst().orElse(TransportOptionAutoAuth.FALSE); + Objects.requireNonNull(oldAutoAuth, "Transport option cannot be null"); + + final Stream newOptions = getOptions(TransportOptionAutoAuth.class, options).stream(); + final TransportOptionAutoAuth newAutoAuth = newOptions.findFirst().orElse(oldAutoAuth); + Objects.requireNonNull(newAutoAuth, "Transport option cannot be null"); + + if (oldAutoAuth != newAutoAuth) { + setOption(newAutoAuth); + } + + try { + final TransportOptionMethod method = getOptions(TransportOptionMethod.class, options).stream().findFirst() + .orElse(TransportOptionMethod.POST_JSON); + + if (method == TransportOptionMethod.GET) { + if (!(payload instanceof TransportPayloadHttp)) { + throw new IllegalArgumentException( + "payload must be a TransportPayloadHttp: " + payload.getClass().getName()); + } + + return executeGet((TransportPayloadHttp) payload, options); + } else if (method == TransportOptionMethod.DELETE) { + if (!(payload instanceof TransportPayloadHttp)) { + throw new IllegalArgumentException( + "payload must be a TransportPayloadHttp: " + payload.getClass().getName()); + } + + return executeDelete((TransportPayloadHttp) payload, options); + } else if (method == TransportOptionMethod.POST_XML) { + if (!(payload instanceof TransportPayloadHttp)) { + throw new IllegalArgumentException( + "payload must be a TransportPayloadHttp: " + payload.getClass().getName()); + } + + return executePostXml((TransportPayloadHttp) payload, options); + } else { + if (payload instanceof TransportPayloadScalarWebRequest) { + return executePostJson((TransportPayloadScalarWebRequest) payload, options).thenApply(r -> { + if (r.getResponse().getHttpCode() == HttpStatus.OK_200) { + final String content = r.getResponse().getContent(); + final ScalarWebResult res = Objects + .requireNonNull(gson.fromJson(content, ScalarWebResult.class)); + return new TransportResultScalarWebResult(res); + } else { + return new TransportResultScalarWebResult(new ScalarWebResult(r.getResponse())); + } + }); + } else if (payload instanceof TransportPayloadHttp) { + return executePostJson((TransportPayloadHttp) payload, options); + } else { + throw new IllegalArgumentException( + "payload must be a TransportPayloadHttp or TransportPayloadScalarWebRequest: " + + payload.getClass().getName()); + } + } + } finally { + if (oldAutoAuth != newAutoAuth) { + setOption(oldAutoAuth); + } + } + } + + /** + * A helper method to execute a GET on a specific URL. This helper function simply allows us to call the GET with a + * string URL (rather than a payload implementation) and return an {@link HttpResponse} (rather than a transport + * result implementation) + * + * @param url a non-null, non-empty string url to execute + * @param options any transport options to use + * @return a non-null {@link HttpResponse} + */ + public HttpResponse executeGet(final String url, final TransportOption... options) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + return execute(url, append(options, TransportOptionMethod.GET)); + } + + /** + * A helper method to execute a DELETE on a specific URL. This helper function simply allows us to call the DETELE + * with a string URL (rather than a payload implementation) and return an {@link HttpResponse} (rather than a + * transport result implementation) + * + * @param url a non-null, non-empty string url to execute + * @param options any transport options to use + * @return a non-null {@link HttpResponse} + */ + public HttpResponse executeDelete(final String url, final TransportOption... options) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + return execute(url, append(options, TransportOptionMethod.DELETE)); + } + + /** + * A helper method to execute a POST on a specific URL and a string JSON payload. This helper function simply allows + * us to call the POST with a string URL and JSON payload (rather than a payload implementation) and return an + * {@link HttpResponse} (rather than a transport result implementation) + * + * @param url a non-null, non-empty string url to execute + * @param payload a non-null, non-empty string payload to send + * @param options any transport options to use + * @return a non-null {@link HttpResponse} + */ + public HttpResponse executePostJson(final String url, final String payload, final TransportOption... options) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + SonyUtil.validateNotEmpty(payload, "payload cannot be empty"); + + return execute(url, new TransportPayloadHttp(url, payload), append(options, TransportOptionMethod.POST_JSON)); + } + + /** + * A helper method to execute a POST on a specific URL and a string XML payload. This helper function simply allows + * us to call the POST with a string URL and XML payload (rather than a payload implementation) and return an + * {@link HttpResponse} (rather than a transport result implementation) + * + * @param url a non-null, non-empty string url to execute + * @param payload a non-null, possibly empty string payload to send + * @param options any transport options to use + * @return a non-null {@link HttpResponse} + */ + public HttpResponse executePostXml(final String url, final String payload, final TransportOption... options) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + Objects.requireNonNull(payload, "payload cannot be null"); + + return execute(url, new TransportPayloadHttp(url, payload), append(options, TransportOptionMethod.POST_XML)); + } + + /** + * Execute the give URL with the specified options + * + * @param url a non-null, non-empty string url to execute + * @param options any transport options to use + * @return a non-null {@link HttpResponse} + */ + private HttpResponse execute(final String url, final TransportOption... options) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + return execute(url, new TransportPayloadHttp(url), options); + } + + /** + * Execute the give URL with the specified options + * + * @param url a non-null, non-empty string url to execute + * @param payload a non-null, possibly empty string payload to send + * @param options any transport options to use + * @return a non-null {@link HttpResponse} + */ + private HttpResponse execute(final String url, final TransportPayloadHttp payload, + final TransportOption... options) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + Objects.requireNonNull(payload, "payload cannot be null"); + + try { + final TransportResult result = execute(payload, options).get(SonyBindingConstants.RSP_WAIT_TIMEOUTSECONDS, + TimeUnit.SECONDS); + if (result instanceof TransportResultHttpResponse) { + return ((TransportResultHttpResponse) result).getResponse(); + } else { + return new HttpResponse(HttpStatus.INTERNAL_SERVER_ERROR_500, "Execution of " + url + + " didn't return a TransportResultHttpResponse: " + result.getClass().getName()); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + return new HttpResponse(HttpStatus.INTERNAL_SERVER_ERROR_500, + "Execution of " + url + " threw an exception: " + e.getMessage()); + } + } + + /** + * Helper method to execute a GET + * + * @param cmd the non-null command (which contains the GET URL to call) + * @param options any options to use for this specific call + * @return a future response + */ + private CompletableFuture executeGet(final TransportPayloadHttp cmd, + final TransportOption... options) { + Objects.requireNonNull(cmd, "cmd cannot be null"); + + final String url = cmd.getUrl(); + SonyUtil.validateNotEmpty(url, "url within the cmd cannot be empty"); + + final Header[] headers = getHeaders(options); + return CompletableFuture + .completedFuture(new TransportResultHttpResponse(requestor.sendGetCommand(url, headers))); + } + + /** + * Helper method to execute a DELETE + * + * @param cmd the non-null command (which contains the DELETE URL to call) + * @param options any options to use for this specific call + * @return a future response + */ + private CompletableFuture executeDelete(final TransportPayloadHttp cmd, + final TransportOption... options) { + Objects.requireNonNull(cmd, "cmd cannot be null"); + + final String url = cmd.getUrl(); + SonyUtil.validateNotEmpty(url, "url within the cmd cannot be empty"); + final Header[] headers = getHeaders(options); + return CompletableFuture + .completedFuture(new TransportResultHttpResponse(requestor.sendDeleteCommand(url, headers))); + } + + /** + * Helper method to execute a POST of ScalarWebRequest (which will be json'd) + * + * @param request the non-null scalar web request to send (to the base uri) + * @param options any options to use for this specific call + * @return a future response + */ + private CompletableFuture executePostJson( + final TransportPayloadScalarWebRequest request, final TransportOption... options) { + Objects.requireNonNull(request, "request cannot be null"); + final ScalarWebRequest payload = request.getRequest(); + + Objects.requireNonNull(payload, "payload cannot be null"); + final String jsonRequest = gson.toJson(payload); + + return executePostJson(new TransportPayloadHttp(getBaseUri().toString(), jsonRequest), options); + } + + /** + * Helper method to execute a POST of JSON text to the specified URL + * + * @param request the non-null request to send + * @param options any options to use for this specific call + * @return a future response + */ + private CompletableFuture executePostJson(final TransportPayloadHttp request, + final TransportOption... options) { + Objects.requireNonNull(request, "request cannot be null"); + + final String payload = request.getBody(); + Objects.requireNonNull(payload, "payload cannot be null"); // may be empty however + + final String url = request.getUrl(); + SonyUtil.validateNotEmpty(url, "url within the cmd cannot be empty"); + + final Header[] headers = getHeaders(options); + + return CompletableFuture + .completedFuture(new TransportResultHttpResponse(requestor.sendPostJsonCommand(url, payload, headers))); + } + + /** + * Helper method to execute a POST of XML + * + * @param request the non-null request to send + * @param options any options to use for this specific call + * @return a future response + */ + private CompletableFuture executePostXml(final TransportPayloadHttp request, + final TransportOption... options) { + Objects.requireNonNull(request, "request cannot be null"); + + final String payload = request.getBody(); + Objects.requireNonNull(payload, "payload cannot be null"); // may be empty however + + final String url = request.getUrl(); + SonyUtil.validateNotEmpty(url, "url within the cmd cannot be empty"); + + final Header[] headers = getHeaders(options); + return CompletableFuture + .completedFuture(new TransportResultHttpResponse(requestor.sendPostXmlCommand(url, payload, headers))); + } + + @Override + public String getProtocolType() { + return SonyTransportFactory.HTTP; + } + + @Override + public void close() { + logger.debug("Closing http client"); + requestor.close(); + } + + /** + * Helper method to append a option to an array of options + * + * @param options a non-null, possibly empty list of options + * @param option a non-null option to append + * @return a non-null, non-empty list of options + */ + private static TransportOption[] append(final TransportOption[] options, final TransportOption option) { + Objects.requireNonNull(options, "options cannot be null"); + Objects.requireNonNull(option, "option cannot be null"); + final TransportOption[] optionsExtended = Arrays.copyOf(options, options.length + 1); + optionsExtended[optionsExtended.length - 1] = option; + return optionsExtended; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyTransport.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyTransport.java new file mode 100644 index 0000000000000..73deed8559582 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyTransport.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import java.io.Closeable; +import java.net.URI; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.sony.internal.SonyBindingConstants; +import org.openhab.binding.sony.internal.net.HttpResponse; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebRequest; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; + +/** + * This interface defines the contract for all sony transports and provides some default logic to each + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface SonyTransport extends Closeable { + /** + * Executes the payload on the transport using the specified options (if any) + * + * @param payload a non-null payload to deliver + * @param options any transport options to use + * @return the non-null future result + */ + public CompletableFuture execute(TransportPayload payload, TransportOption... options); + + /** + * Sets a global option on the transport + * + * @param option a non-null option + */ + public void setOption(TransportOption option); + + /** + * Removes a global option from the transport. + * + * @param option a non-null option + */ + public void removeOption(TransportOption option); + + /** + * Returns a list of all global options used by the transport + * + * @return a non-null, possibly empty list of global transport options + */ + public List getOptions(); + + /** + * Adds a listener to the transport + * + * @param listener a non-null listener to add + */ + public void addListener(SonyTransportListener listener); + + /** + * Removes a listener from the transport + * + * @param listener a non-null listener to remove + * @return + */ + public boolean removeListener(SonyTransportListener listener); + + /** + * Get's the protocol type used by this transport (matches one of the protocol types on + * {@link SonyTransportFactory}) + * + * @return a non-null, non-empty protocol type + */ + public String getProtocolType(); + + /** + * Returns the base URI used by the transport + * + * @return a non-null base URI + */ + public URI getBaseUri(); + + /** + * Helper metohd to execute a {@link ScalarWebRequest} on the transport and return a {@link ScalarWebResult} + * + * @param payload a non-null {@link ScalarWebRequest} + * @param options any transport options to include + * @return a non-null {@link ScalarWebRequest} + */ + public default ScalarWebResult execute(final ScalarWebRequest payload, final TransportOption... options) { + Objects.requireNonNull(payload, "payload cannot be null"); + try { + final TransportResult result = execute(new TransportPayloadScalarWebRequest(payload), options) + .get(SonyBindingConstants.RSP_WAIT_TIMEOUTSECONDS, TimeUnit.SECONDS); + if (result instanceof TransportResultScalarWebResult) { + return ((TransportResultScalarWebResult) result).getResult(); + } else { + return new ScalarWebResult(new HttpResponse(HttpStatus.INTERNAL_SERVER_ERROR_500, "Execution of " + + payload + " didn't return a TransportResultScalarWebResult: " + result.getClass().getName())); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + return new ScalarWebResult(new HttpResponse(HttpStatus.INTERNAL_SERVER_ERROR_500, + "Execution of " + payload + " threw an exception: " + e.getMessage())); + } + }; + + @Override + public void close(); +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyTransportFactory.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyTransportFactory.java new file mode 100644 index 0000000000000..12ea679d288ac --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyTransportFactory.java @@ -0,0 +1,282 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeoutException; + +import javax.ws.rs.client.ClientBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.gson.GsonUtilities; +import org.openhab.binding.sony.internal.scalarweb.models.api.ServiceProtocol; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * This factory will be responsible for creating a {@link SonyTransport} for the caller. There are two transports that + * can be used: + *
    + *
  1. {@link #HTTP} the http transport which will allow GET/DELETE/POST calls to a URL
  2. + *
  3. {@link #WEBSOCKET} a websocket transport which will use websockets to send JSON payloads (and receive them)
  4. + *
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyTransportFactory { + + /** The transport type that allows algorithms to determine which to use */ + public static final String AUTO = "auto"; + + /** The transport type for HTTP (note: the value matches what Sony uses for ease) */ + public static final String HTTP = "xhrpost:jsonizer"; + + /** The transpor type for Websockets (note: the value matches what Sony uses for ease) */ + public static final String WEBSOCKET = "websocket:jsonizer"; + + /** The logger */ + private final Logger logger = LoggerFactory.getLogger(SonyTransportFactory.class); + + /** The base URL of the factory */ + private final URL baseUrl; + + /** The default GSON to use */ + private final Gson gson; + + /** Potentially a websocket client to use */ + private final @Nullable WebSocketClient webSocketClient; + + /** Potentially a scheduler to use */ + private final @Nullable ScheduledExecutorService scheduler; + + /** The clientBuilder used in HttpRequest */ + private final ClientBuilder clientBuilder; + + /** + * Constructs the transport factory + * + * @param baseUrl a non-null base url + * @param gson a non-null gson to use + * @param webSocketClient a potentially null websocket client + * @param scheduler a potentially null scheduler + * @param clientBuilder a non-null client builder + */ + public SonyTransportFactory(final URL baseUrl, final Gson gson, final @Nullable WebSocketClient webSocketClient, + final @Nullable ScheduledExecutorService scheduler, final ClientBuilder clientBuilder) { + Objects.requireNonNull(baseUrl, "baseUrl cannot be null"); + Objects.requireNonNull(gson, "gson cannot be null"); + + this.baseUrl = baseUrl; + this.gson = gson; + this.webSocketClient = webSocketClient; + this.scheduler = scheduler; + this.clientBuilder = clientBuilder; + } + + /** + * Attempts to create a sony transport suitable to the service protocol + * + * @param serviceProtocol a non-null service protocol + * @return the sony transport or null if none could be found + */ + public @Nullable SonyTransport getSonyTransport(final ServiceProtocol serviceProtocol) { + Objects.requireNonNull(serviceProtocol, "serviceProtocol cannot be null"); + String protocol; + if (serviceProtocol.hasWebsocketProtocol()) { + protocol = WEBSOCKET; + } else if (serviceProtocol.hasHttpProtocol()) { + protocol = HTTP; + } else { + protocol = AUTO; + } + final String serviceName = serviceProtocol.getServiceName(); + final SonyTransport transport = getSonyTransport(serviceName, protocol); + return transport == null ? getSonyTransport(serviceName, AUTO) : transport; + } + + /** + * Attempts to create a sony transport suitable to the specified service using {@link #AUTO} for the protocol + * + * @param serviceName a non-null, non-empty service name + * @return the sony transport or null if none could be found + */ + public @Nullable SonyTransport getSonyTransport(final String serviceName) { + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + + return getSonyTransport(serviceName, AUTO); + } + + /** + * Attempts to create a sony transport suitable to the specified service using the specified protocl + * + * @param serviceName a non-null, non-empty service name + * @param protocol a non-null, non-empty protocol to use + * @return the sony transport or null if none could be found + */ + public @Nullable SonyTransport getSonyTransport(final String serviceName, final String protocol) { + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + SonyUtil.validateNotEmpty(protocol, "protocol cannot be empty"); + + switch (protocol) { + case AUTO: + final SonyWebSocketTransport wst = createWebSocketTransport(serviceName); + return wst == null ? createServiceHttpTransport(serviceName) : wst; + + case HTTP: + return createServiceHttpTransport(serviceName); + + case WEBSOCKET: + return createWebSocketTransport(serviceName); + + default: + logger.debug("Unknown protocol: {} for service {}", protocol, serviceName); + return null; + } + } + + /** + * Helper method to create a websocket transport to the specified service name + * + * @param serviceName a non-null, non-empty service name + * @return the sony websocket transport or null if none could be found + */ + private @Nullable SonyWebSocketTransport createWebSocketTransport(final String serviceName) { + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + + final WebSocketClient localWebSocketClient = webSocketClient; + if (localWebSocketClient == null) { + logger.debug("No websocket client specified - cannot create an websocket transport"); + return null; + } + + try { + final String baseFile = baseUrl.getFile(); + final URI uri = new URI( + String.format("ws://%s:%d/%s", baseUrl.getHost(), baseUrl.getPort() > 0 ? baseUrl.getPort() : 10000, + baseFile + (baseFile.endsWith("/") ? "" : "/") + + (serviceName.startsWith("/") ? serviceName.substring(1) : serviceName))) + .normalize(); + return new SonyWebSocketTransport(localWebSocketClient, uri, gson, scheduler); + } catch (URISyntaxException | InterruptedException | ExecutionException | TimeoutException | IOException e) { + logger.debug("Exception occurred creating transport: {}", e.getMessage()); + return null; + } + } + + /** + * Helper method to create a HTTP transport to the specified service name + * + * @param serviceName a non-null, non-empty service name + * @return the sony http transport or null if none could be found + */ + private @Nullable SonyHttpTransport createServiceHttpTransport(final String serviceName) { + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + + final String base = baseUrl.toString(); + final String baseUrlString = base + (base.endsWith("/") ? "" : "/") + + (serviceName.startsWith("/") ? serviceName.substring(1) : serviceName); + + try { + return new SonyHttpTransport(baseUrlString, gson, clientBuilder); + } catch (final URISyntaxException e) { + logger.debug("Exception occurred creating transport: {}", e.getMessage()); + return null; + } + } + + /** + * Public helper method to create an HTTP transport for the given base and gson + * + * @param baseUrl a non-null, non-empty base URL + * @param gson a non-null gson to use + * @return a non-null sony http transport + * @throws URISyntaxException if the baseUrl is malformed + */ + public static SonyHttpTransport createHttpTransport(final String baseUrl, final Gson gson, + final ClientBuilder clientBuilder) throws URISyntaxException { + SonyUtil.validateNotEmpty(baseUrl, "baseUrl cannot be empty"); + Objects.requireNonNull(gson, "gson cannot be null"); + return new SonyHttpTransport(baseUrl, gson, clientBuilder); + } + + /** + * Public helper method to create an HTTP transport for the given base and default gson + * + * @param baseUrl a non-null, non-empty base URL + * @return a non-null sony http transport + * @throws URISyntaxException if the baseUrl is malformed + */ + public static SonyHttpTransport createHttpTransport(final String baseUrl, final ClientBuilder clientBuilder) + throws URISyntaxException { + SonyUtil.validateNotEmpty(baseUrl, "baseUrl cannot be empty"); + return createHttpTransport(baseUrl, GsonUtilities.getApiGson(), clientBuilder); + } + + /** + * Public helper method to create an HTTP transport for the given base and default gson + * + * @param baseUrl a non-null base URL + * @return a non-null sony http transport + * @throws URISyntaxException if the baseUrl is malformed + */ + public static SonyHttpTransport createHttpTransport(final URL baseUrl, final ClientBuilder clientBuilder) + throws URISyntaxException { + Objects.requireNonNull(baseUrl, "baseUrl cannot be null"); + return createHttpTransport(baseUrl.toExternalForm(), clientBuilder); + } + + /** + * Public helper method to create an HTTP transport for the given base and specified gson + * + * @param baseUrl a non-null base URL + * @param gson a non-null gson to use + * @return a non-null sony http transport + * @throws URISyntaxException if the baseUrl is malformed + */ + public static SonyHttpTransport createHttpTransport(final URL baseUrl, final Gson gson, + final ClientBuilder clientBuilder) throws URISyntaxException { + Objects.requireNonNull(baseUrl, "baseUrl cannot be null"); + Objects.requireNonNull(gson, "gson cannot be null"); + return createHttpTransport(baseUrl.toExternalForm(), gson, clientBuilder); + } + + /** + * Public helper method to create an HTTP transport for the given base and specified serviceName + * + * @param baseUrl a non-null base URL + * @param serviceName + * @param serviceName a non-null, non-empty service name + * @throws URISyntaxException if the baseUrl is malformed + */ + public static SonyHttpTransport createHttpTransport(final URL baseUrl, final String serviceName, + final ClientBuilder clientBuilder) throws URISyntaxException { + Objects.requireNonNull(baseUrl, "baseUrl cannot be null"); + SonyUtil.validateNotEmpty(serviceName, "serviceName cannot be empty"); + final String base = baseUrl.toString(); + final String baseUrlString = base + (base.endsWith("/") ? "" : "/") + + (serviceName.startsWith("/") ? serviceName.substring(1) : serviceName); + return new SonyHttpTransport(baseUrlString, GsonUtilities.getApiGson(), clientBuilder); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyTransportListener.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyTransportListener.java new file mode 100644 index 0000000000000..176ca86fb6256 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyTransportListener.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebEvent; + +/** + * The {@link SonyTransportListener} allows listeners to receive events from a {@link SonyTransport} + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface SonyTransportListener { + /** + * Triggered when an event is received from the underlying transport + * + * @param event a non-null event + */ + void onEvent(ScalarWebEvent event); + + /** + * Triggered when an error occurs during communication + * + * @param t the non-null throwable + */ + void onError(Throwable t); +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyWebSocketTransport.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyWebSocketTransport.java new file mode 100644 index 0000000000000..be2ea9934da71 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/SonyWebSocketTransport.java @@ -0,0 +1,327 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeException; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.openhab.binding.sony.internal.ExpiringMap; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebEvent; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebRequest; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * This sony transport will handle communicating over a web socket communication using the REST API. Basically + * everything is sent as a ScalarWebRequest (that has been serialized via json) and you'll get back a ScalarWebResponse + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class SonyWebSocketTransport extends AbstractSonyTransport { + /** The looger */ + private final Logger logger = LoggerFactory.getLogger(SonyWebSocketTransport.class); + + /** The expiration (in seconds) during a websocket connect */ + private static final int CONN_EXPIRE_TIMEOUT_SECONDS = 10; + + /** The expiration (in seconds) to get a response from a command */ + private static final int CMD_EXPIRE_TIMEOUT_SECONDS = 30; + + /** The interval (in seconds) to send a ping to the device */ + private static final int PING_SECONDS = 5; + + /** The websocket URI */ + private final URI uri; + + /** The GSON used to serialize/deserialize */ + private final Gson gson; + + /** + * The map of pending commands [futures] by command id. Any time a command (ScalarWebRequest) is sent, a future is + * created by that command id. When the respond to that command is received, the corresponding future (by is) is + * completed. + */ + private final ExpiringMap> futures; + + /** The websocket sessions being used */ + private @Nullable Session session; + + /** Current ping number (meaningless really - just an increasing number) */ + private int ping = 0; + + /** The ping task */ + private final @Nullable ScheduledFuture pingTask; + + /** + * Creates the websocket transport + * + * @param webSocketClient a non-null websocket client + * @param uri a non-null websocket URI + * @param gson a non-null GSON + * @param scheduler a potentially null scheduler + * @throws InterruptedException if a task was interrupted + * @throws ExecutionException if a task had an execution exception + * @throws TimeoutException if we timed out connecting to the websocket + * @throws IOException if an IO exception occurred + */ + public SonyWebSocketTransport(final WebSocketClient webSocketClient, final URI uri, final Gson gson, + final @Nullable ScheduledExecutorService scheduler) + throws InterruptedException, ExecutionException, TimeoutException, IOException { + super(uri); + Objects.requireNonNull(webSocketClient, "webSocketClient cannot be null"); + Objects.requireNonNull(uri, "uri cannot be null"); + Objects.requireNonNull(gson, "gson cannot be null"); + + this.gson = gson; + this.uri = uri; + + futures = new ExpiringMap>(scheduler, CMD_EXPIRE_TIMEOUT_SECONDS, + TimeUnit.SECONDS); + futures.addExpireListener((k, v) -> { + logger.debug("Execution of {} took too long and is being cancelled", k); + v.cancel(true); + }); + + logger.debug("Starting websocket connection to {}", uri); + webSocketClient.connect(new WebSocketCallback(), uri, new ClientUpgradeRequest()) + .get(CONN_EXPIRE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + logger.debug("Websocket connection successful to {}", uri); + + // Setup pinging to prevent connection from timing out due to inactivity + // Note: this is probably overkill but is an easy thing to do + if (scheduler == null) { + logger.debug("No scheduler specified - pinging disabled"); + pingTask = null; + } else { + pingTask = scheduler.scheduleWithFixedDelay(() -> { + final Session localSession = session; + if (localSession != null) { + final RemoteEndpoint remote = localSession.getRemote(); + + final ByteBuffer payload = ByteBuffer.allocate(4).putInt(ping++); + try { + logger.debug("Pinging {}", uri); + remote.sendPing(payload); + } catch (final IOException e) { + logger.debug("Pinging {} failed: {}", uri, e.getMessage()); + } + } + }, PING_SECONDS, PING_SECONDS, TimeUnit.SECONDS); + } + } + + @Override + public String getProtocolType() { + return SonyTransportFactory.WEBSOCKET; + } + + @Override + public void close() { + futures.close(); + + // if there is an old web socket then clean up and destroy + final Session localSession = session; + if (localSession != null && !localSession.isOpen()) { + logger.debug("Closing session {}", uri); + try { + localSession.close(); + } catch (final Exception e) { + logger.debug("Closing of session {} failed: {}", uri, e.getMessage(), e); + } + } + session = null; + + if (pingTask != null) { + SonyUtil.cancel(pingTask); + } + } + + @Override + public CompletableFuture execute(final TransportPayload payload, + final TransportOption... options) { + if (!(payload instanceof TransportPayloadScalarWebRequest)) { + throw new IllegalArgumentException( + "payload must be a TransportPayloadRequest: " + payload.getClass().getName()); + } + + final Session localSession = session; + if (localSession == null) { + return CompletableFuture.completedFuture(new TransportResultScalarWebResult(new ScalarWebResult( + HttpStatus.INTERNAL_SERVER_ERROR_500, "No session established yet - wait for it to be connected"))); + } + + final ScalarWebRequest cmd = ((TransportPayloadScalarWebRequest) payload).getRequest(); + final String jsonRequest = gson.toJson(cmd); + + final CompletableFuture future = new CompletableFuture<>(); + futures.put(cmd.getId(), future); + + logger.debug("Sending {} to {}", jsonRequest, uri); + + // Use the async message (we don't really care about the returned future) + localSession.getRemote().sendStringByFuture(jsonRequest); + return future; + } + + /** + * The following class provides the callback method to handle websocket events + */ + @NonNullByDefault + @WebSocket + public class WebSocketCallback { + /** + * Called when the websocket client has connected + * + * @param session the websocket session (shouldn't be null) + */ + @OnWebSocketConnect + public void onConnect(final @Nullable Session session) { + logger.trace("websocket.onConnect({})", session); + if (session == null) { + logger.debug("Connected to a session that was null - weird!"); + } else { + logger.debug("Connected successfully to server {}", uri); + SonyWebSocketTransport.this.session = session; + } + } + + /** + * Called when a message was received from the websocket connection + * + * @param message a potentially null (shouldn't really happen), potentially empty (could happen) message + */ + @OnWebSocketMessage + public void onMessage(final @Nullable String message) { + logger.trace("websocket.onMessage({})", message); + if (message == null || message.isEmpty()) { + logger.debug("Received an empty message - ignoring"); + } else { + try { + final JsonObject json = gson.fromJson(message, JsonObject.class); + if (json.has("id")) { + final ScalarWebResult result = gson.fromJson(json, ScalarWebResult.class); + final Integer resultId = result.getId(); + if (resultId == null) { + logger.debug("Response from server has an unknown id: {}", message); + } else { + final CompletableFuture future = futures.get(resultId); + if (future != null) { + logger.debug("Response received from server: {}", message); + futures.remove(resultId); + future.complete(new TransportResultScalarWebResult(result)); + } else { + logger.debug( + "Response received from server but a waiting command wasn't found - ignored: {}", + message); + } + } + } else { + final ScalarWebEvent event = Objects.requireNonNull(gson.fromJson(json, ScalarWebEvent.class)); + logger.debug("Event received from server: {}", message); + fireEvent(event); + } + } catch (final JsonParseException e) { + logger.debug("JSON parsing error: {} for {}", e.getMessage(), message, e); + } + } + } + + /** + * Called when an exception has occurred on the websocket connection + * + * @param t a potentially null (shouldn't happen) error + */ + @OnWebSocketError + public void onError(final @Nullable Throwable t) { + if (t == null) { + logger.debug("Received a null throwable in onError - ignoring"); + } else { + logger.trace("websocket.onError({})", t.getMessage(), t); + if (t instanceof UpgradeException) { + final UpgradeException e = (UpgradeException) t; + // 404 happens when the individual service has no websocket connection + // but there is a websocket server listening for other services + if (e.getResponseStatusCode() == HttpStatus.NOT_FOUND_404) { + logger.debug("No websocket listening for specific service {}", e.getRequestURI()); + return; + } else if (e.getResponseStatusCode() == 0) { + // Weird second exception thrown when you get a connection refused + // when using upgrade - ignore this since it was logged below + return; + } + } + + // suppress stack trace on connection refused + if (t.getMessage().toLowerCase().contains("connection refused")) { + logger.debug("Connection refused for {}: {}", uri, t.getMessage()); + return; + } + + // suppress stack trace on connection refused + if (t.getMessage().toLowerCase().contains("idle timeout")) { + logger.debug("Idle Timeout for {}: {}", uri, t.getMessage()); + return; + } + + logger.debug("Exception occurred during websocket communication for {}: {}", uri, t.getMessage(), t); + fireOnError(t); + } + } + + /** + * Called when the websocket connection has been closed + * + * @param statusCode the status code for the close + * @param reason the reason of the close + */ + @OnWebSocketClose + public void onClose(final int statusCode, final String reason) { + logger.trace("websocket.onClose({}, {})", statusCode, reason); + + final Session localSession = session; + if (localSession != null) { + logger.debug("Closing session from close event {}", uri); + localSession.close(); + } + session = null; + } + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOption.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOption.java new file mode 100644 index 0000000000000..eb13234c4f921 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOption.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * A marker interface for a transport option. + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface TransportOption { +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOptionAutoAuth.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOptionAutoAuth.java new file mode 100644 index 0000000000000..8e51a44d83b54 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOptionAutoAuth.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * A transport option to specify whether to automatically authenticate the communication + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public enum TransportOptionAutoAuth implements TransportOption { + /** Do automatically authenticate */ + TRUE, + + /** Do NOT automatically authenticate */ + FALSE +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOptionHeader.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOptionHeader.java new file mode 100644 index 0000000000000..287d3902f5e62 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOptionHeader.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http.HttpHeader; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.net.Header; + +/** + * The transport option to specify a header value (a string key/value) + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class TransportOptionHeader implements TransportOption { + /** The header to use */ + private final Header header; + + /** + * Construct the transport option from a header + * + * @param header a non-null header + */ + public TransportOptionHeader(final Header header) { + Objects.requireNonNull(header, "header cannot be null"); + this.header = header; + } + + /** + * Constructs the transport option from a http header and a value + * + * @param hdr a non-null http header + * @param value a non-null, non-empty header value + */ + public TransportOptionHeader(final HttpHeader hdr, final String value) { + Objects.requireNonNull(hdr, "hdr cannot be null"); + SonyUtil.validateNotEmpty(value, "value cannot be empty"); + + this.header = new Header(hdr.asString(), value); + } + + /** + * Constructs the transport option from a http header and a value + * + * @param key a non-null, non-empty header key + * @param value a non-null, non-empty header value + */ + public TransportOptionHeader(final String key, final String value) { + SonyUtil.validateNotEmpty(value, "value cannot be empty"); + SonyUtil.validateNotEmpty(value, "value cannot be empty"); + this.header = new Header(key, value); + } + + /** + * Get's the header for this transport option + * + * @return a non-null header + */ + public Header getHeader() { + return header; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOptionMethod.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOptionMethod.java new file mode 100644 index 0000000000000..15e255d095204 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportOptionMethod.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * A transport option that describes the type of method to use on a call + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public enum TransportOptionMethod implements TransportOption { + + /** Represents a GET method */ + GET, + + /** Represents a POST method for XML */ + POST_XML, + + /** Represents a POST method for JSON */ + POST_JSON, + + /** Represents a DELETE method */ + DELETE +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportPayload.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportPayload.java new file mode 100644 index 0000000000000..c8a82c2cd19bc --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportPayload.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This is a marker interface for a transport payload + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface TransportPayload { +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportPayloadHttp.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportPayloadHttp.java new file mode 100644 index 0000000000000..6794a91e039aa --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportPayloadHttp.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +/** + * This class represents an HTTP payload. An HTTP payload includes both the URL address to send something to and + * possibly a string to include as the body + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class TransportPayloadHttp implements TransportPayload { + /** The URL to call */ + private final String url; + + /** Possibly the body (XML or JSON) */ + private final @Nullable String body; + + /** + * Constructs the HTTP payload with a URL and no body + * + * @param url a non-null, non-empty URL + */ + public TransportPayloadHttp(final String url) { + this(url, null); + } + + /** + * Constructs the HTTP payload with a URL and body + * + * @param url a non-null, non-empty URL + * @param body a possibly null, possibly empty body + */ + public TransportPayloadHttp(final String url, final @Nullable String body) { + SonyUtil.validateNotEmpty(url, "url cannot be empty"); + this.url = url; + this.body = body == null ? "" : body; // convert null to empty + } + + /** + * The URL for the HTTP request + * + * @return a non-null, non-empty URL + */ + public String getUrl() { + return url; + } + + /** + * The body for the HTTP request + * + * @return the body for the HTTP request or null if not applicable + */ + public @Nullable String getBody() { + return body; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportPayloadScalarWebRequest.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportPayloadScalarWebRequest.java new file mode 100644 index 0000000000000..1d4736da77d3d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportPayloadScalarWebRequest.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebRequest; + +/** + * This class represents a ScalarWebRequest payload + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class TransportPayloadScalarWebRequest implements TransportPayload { + /** The request payload */ + private final ScalarWebRequest request; + + /** + * Constructs the payload from the request + * + * @param request a non-null request + */ + public TransportPayloadScalarWebRequest(final ScalarWebRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + this.request = request; + } + + /** + * Gets the request for this payload + * + * @return a non-null request + */ + public ScalarWebRequest getRequest() { + return request; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportResult.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportResult.java new file mode 100644 index 0000000000000..981965f8742d4 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportResult.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * A marker interface for a transport result + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public interface TransportResult { +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportResultHttpResponse.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportResultHttpResponse.java new file mode 100644 index 0000000000000..84b2e81691c0e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportResultHttpResponse.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.net.HttpResponse; + +/** + * This class represents an HTTP response + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class TransportResultHttpResponse implements TransportResult { + /** The http response */ + private final HttpResponse response; + + /** + * Constructs the response from the HTTP response + * + * @param response a non-null HTTP response + */ + public TransportResultHttpResponse(final HttpResponse response) { + Objects.requireNonNull(response, "response cannot be null"); + this.response = response; + } + + /** + * Gets the HTTP response + * + * @return the non-null HTTP response + */ + public HttpResponse getResponse() { + return response; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportResultScalarWebResult.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportResultScalarWebResult.java new file mode 100644 index 0000000000000..26551b81f2c7d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/transports/TransportResultScalarWebResult.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.transports; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.sony.internal.scalarweb.models.ScalarWebResult; + +/** + * This class represents a {@link ScalarWebResult} + * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class TransportResultScalarWebResult implements TransportResult { + /** The scalar web result */ + private final ScalarWebResult result; + + /** + * Constructs the result from the {@link ScalarWebResult} + * + * @param result a non-null {@link ScalarWebResult} + */ + public TransportResultScalarWebResult(final ScalarWebResult result) { + Objects.requireNonNull(result, "result cannot be null"); + this.result = result; + } + + /** + * Gets the {@link ScalarWebResult} + * + * @return a non-null {@link ScalarWebResult} + */ + public ScalarWebResult getResult() { + return result; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpd.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpd.java new file mode 100644 index 0000000000000..6c33eefeec822 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpd.java @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.upnp.models; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents the deserialized results of an UPNP service XML. The following is an example of the + * results that will be deserialized: + * + *
+ * {@code
+      
+      
+        
+          1
+          0
+        
+        
+          
+            X_SendIRCC
+            
+              
+                IRCCCode
+              in
+              X_A_ARG_TYPE_IRCCCode
+            
+          
+        
+        
+          X_GetStatus
+          
+            
+              CategoryCode
+              in
+              X_A_ARG_TYPE_Category
+            
+            
+              CurrentStatus
+              out
+              X_A_ARG_TYPE_CurrentStatus
+            
+            
+              CurrentCommandInfo
+              out
+              X_A_ARG_TYPE_CurrentCommandInfo
+            
+          
+        
+      
+      
+        
+          X_A_ARG_TYPE_IRCCCode
+          string
+        
+        
+          X_A_ARG_TYPE_Category
+          string
+        
+        
+          X_A_ARG_TYPE_CurrentStatus
+          string
+          
+            0
+            801
+            804
+            805
+            806
+          
+        
+        
+          X_A_ARG_TYPE_CurrentCommandInfo
+          string
+        
+      
+    
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@XStreamAlias("scpd") +public class UpnpScpd { + @XStreamAlias("serviceStateTable") + private @Nullable UpnpScpdStateTable stateTable; + + @XStreamAlias("actionList") + private @Nullable UpnpScpdActionList actionList; + + /** + * Gets the action for the given action name + * + * @param actionName a non-null, non-empty action name + * @return a possibly null action (null if not found) + */ + public @Nullable UpnpScpdAction getAction(final String actionName) { + final UpnpScpdActionList localActionList = actionList; + final List actions = localActionList == null ? null : localActionList.actions; + if (actions != null) { + for (final UpnpScpdAction action : actions) { + if (actionName.equalsIgnoreCase(action.getActionName())) { + return action; + } + } + } + return null; + } + + /** + * Gets the SOAP request for the specified serviceType, action name and parameters + * + * @param serviceType a non-null, non-empty service type + * @param actionName a non-null, non-empty action name + * @param parms the optional parameters + * @return a string representing the SOAP action or null if serviceType/actionName was not found + */ + public @Nullable String getSoap(final String serviceType, final String actionName, final String... parms) { + SonyUtil.validateNotEmpty(serviceType, "serviceType cannnot be empty"); + SonyUtil.validateNotEmpty(actionName, "actionName cannnot be empty"); + + final UpnpScpdActionList localActionList = actionList; + final List actions = localActionList == null ? null : localActionList.actions; + if (actions != null) { + for (final UpnpScpdAction action : actions) { + if (actionName.equalsIgnoreCase(action.getActionName())) { + final StringBuilder sb = new StringBuilder( + "\n\n \n "); + + int parmIdx = 0; + for (final UpnpScpdArgument arg : action.getArguments()) { + if (arg.isIn()) { + final String parm = parmIdx >= parms.length ? "" : parms[parmIdx++]; + sb.append("\n<"); + sb.append(arg.getArgName()); + sb.append(">"); + sb.append(parm); + sb.append(""); + } + } + + sb.append("\n \n \n\n"); + return sb.toString(); + } + } + } + return null; + } + + /** + * The action list class that holds the list of actions + * + * @author Tim Roberts - Initial Contribution + * + */ + @NonNullByDefault + class UpnpScpdActionList { + @XStreamImplicit + private @Nullable List actions; + } + + /** + * The state table that holds a list of state variables + * + * @author Tim Roberts - Initial Contribution + * + */ + @NonNullByDefault + public class UpnpScpdStateTable { + @XStreamImplicit + private @Nullable List variables; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpdAction.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpdAction.java new file mode 100644 index 0000000000000..0de0e159e874b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpdAction.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.upnp.models; + +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * + * This class represents the deserialized results of an UPNP service action. The following is an example of the + * results that will be deserialized: + * + *
+ * {@code
+          
+            X_SendIRCC
+            
+              
+                IRCCCode
+                in
+                X_A_ARG_TYPE_IRCCCode
+            
+          
+        
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@XStreamAlias("action") +public class UpnpScpdAction { + + /** The action name */ + @XStreamAlias("name") + private @Nullable String actionName; + + /** The arguments */ + @XStreamAlias("argumentList") + private @Nullable UpnpScpdArgumentList argumentList; + + /** + * Gets the action name. + * + * @return the action name + */ + public @Nullable String getActionName() { + return actionName; + } + + /** + * Returns the list of arguments for this action + * + * @return a possibly empty list of arguments + */ + public List getArguments() { + final UpnpScpdArgumentList localArgumentList = argumentList; + final List arguments = localArgumentList == null ? null : localArgumentList.arguments; + return arguments == null ? Collections.emptyList() : Collections.unmodifiableList(arguments); + } + + /** + * The list of arguments for the action + * + * @author Tim Roberts - Initial Contribution + * + */ + @NonNullByDefault + class UpnpScpdArgumentList { + @XStreamImplicit + private @Nullable List arguments; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpdArgument.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpdArgument.java new file mode 100644 index 0000000000000..4f2f8866104d9 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpdArgument.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.upnp.models; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamConverter; +import com.thoughtworks.xstream.converters.basic.BooleanConverter; + +/** + * This class represents the deserialized results of an UPNP service action argument. The following is an example of the + * results that will be deserialized: + * + *
+ * {@code
+          
+            X_SendIRCC
+            
+              
+                IRCCCode
+                in
+                X_A_ARG_TYPE_IRCCCode
+            
+          
+        
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@XStreamAlias("argument") +public class UpnpScpdArgument { + + /** The argument name */ + @XStreamAlias("name") + private @Nullable String argName; + + /** Whether the argument is inbound or outbound */ + @XStreamAlias("direction") + @XStreamConverter(value = BooleanConverter.class, booleans = { false }, strings = { "in", "out" }) + private boolean in; + + /** The related state variable (found in the state table) */ + @XStreamAlias("relatedStateVariable") + private @Nullable String relatedStateVariable; + + /** + * Gets the argument name + * + * @return the possibly null, possibly empty argument name + */ + public @Nullable String getArgName() { + return argName; + } + + /** + * Checks if the argument is inbound or outbound + * + * @return true for inbound, false otherwise + */ + public boolean isIn() { + return in; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpdStateVariable.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpdStateVariable.java new file mode 100644 index 0000000000000..1a76b386ff56d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpScpdStateVariable.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.upnp.models; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * This class represents the deserialized results of an UPNP state variable. The following is an example of the + * results that will be deserialized: + * + *
+ * {@code
+        
+          X_A_ARG_TYPE_IRCCCode
+          string
+        
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@XStreamAlias("stateVariable") +public class UpnpScpdStateVariable { + /** The state variable name */ + @XStreamAlias("name") + private @Nullable String name; + + /** The state variable data type */ + @XStreamAlias("dataType") + private @Nullable String dataType; + + /** + * Returns the name of the state variable + * + * @return a possibly null, possibly empty name + */ + public @Nullable String getName() { + return name; + } + + /** + * Returns the data type of the state variable + * + * @return a possibly null, possibly empty data type + */ + public @Nullable String getDataType() { + return dataType; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpService.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpService.java new file mode 100644 index 0000000000000..062e311e500d0 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpService.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.upnp.models; + +import java.net.URL; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyUtil; +import org.openhab.binding.sony.internal.net.NetUtil; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * This class represents the deserialized results of an UPNP service. The following is an example of the + * results that will be deserialized: + * + *
+ * {@code
+     need example
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +@XStreamAlias("service") +public class UpnpService { + + /** The service identifier */ + @XStreamAlias("serviceId") + private final @Nullable String serviceId; + + /** The service type */ + @XStreamAlias("serviceType") + private final @Nullable String serviceType; + + /** The scpd url */ + @XStreamAlias("SCPDURL") + private final @Nullable String scpdUrl; + + /** The control url */ + @XStreamAlias("controlURL") + private final @Nullable String controlUrl; + + /** + * Constructs a UpnpService + * + * @param serviceId a non-null service ID + * @param serviceType a non-null service type + * @param scpdUrl a non-null scpd URL + * @param controlUrl a non-null control URL + */ + public UpnpService(final String serviceId, final String serviceType, final String scpdUrl, + final String controlUrl) { + SonyUtil.validateNotEmpty(serviceId, "serviceId cannot be empty"); + SonyUtil.validateNotEmpty(serviceType, "serviceType cannot be empty"); + SonyUtil.validateNotEmpty(scpdUrl, "scpdUrl cannot be empty"); + SonyUtil.validateNotEmpty(controlUrl, "controlUrl cannot be empty"); + + this.serviceId = serviceId; + this.serviceType = serviceType; + this.scpdUrl = scpdUrl; + this.controlUrl = controlUrl; + } + + /** + * Gets the SCPD URL given the base URL + * + * @param baseUrl the non-null base url to use as a reference + * + * @return the control url + */ + public @Nullable URL getScpdUrl(final URL baseUrl) { + Objects.requireNonNull(baseUrl, "baseUrl cannot be null"); + + final String localScpdUrl = scpdUrl; + return localScpdUrl == null || localScpdUrl.isEmpty() ? null : NetUtil.getUrl(baseUrl, localScpdUrl); + } + + /** + * Gets the control url + * + * @param baseUrl the non-null base url to use as a reference + * @return the control url + */ + public @Nullable URL getControlUrl(final URL baseUrl) { + Objects.requireNonNull(baseUrl, "baseUrl cannot be null"); + + final String localControlUrl = controlUrl; + return localControlUrl == null || localControlUrl.isEmpty() ? null : NetUtil.getUrl(baseUrl, localControlUrl); + } + + /** + * Gets the service type + * + * @return the service type + */ + public @Nullable String getServiceType() { + return serviceType; + } + + /** + * Gets the service id + * + * @return the service id + */ + public @Nullable String getServiceId() { + return serviceId; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpServiceList.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpServiceList.java new file mode 100644 index 0000000000000..e4606ec5e971f --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpServiceList.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.upnp.models; + +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +/** + * This class represents the deserialized results of an UPNP services. The following is an example of the + * results that will be deserialized: + * + *
+ * {@code
+     need example
+ * }
+ * 
+ * + * @author Tim Roberts - Initial contribution + */ +@NonNullByDefault +public class UpnpServiceList { + /** The list of services */ + @XStreamImplicit + private @Nullable List services; + + /** + * The list of services + * + * @return a non-null, possibly empty list of services + */ + public List getServices() { + return services == null ? Collections.emptyList() : Collections.unmodifiableList(services); + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpXmlReader.java b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpXmlReader.java new file mode 100644 index 0000000000000..de6cf20721606 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/upnp/models/UpnpXmlReader.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2024 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.sony.internal.upnp.models; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sony.internal.SonyHandlerFactory; +import org.openhab.binding.sony.internal.SonyUtil; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.io.xml.StaxDriver; +import com.thoughtworks.xstream.security.NoTypePermission; + +/** + * This class represents creates the various XML readers (using XStream) to deserialize various calls. + * + * @author Tim Roberts - Initial contribution + * + * @param the generic type to cast the XML to + */ +@NonNullByDefault +public class UpnpXmlReader { + + /** The XStream instance */ + private final XStream xstream = new XStream(new StaxDriver()); + + /** The XML reader for SCPD */ + public static final UpnpXmlReader SCPD = new UpnpXmlReader<>(new Class[] { UpnpScpd.class, + UpnpScpd.UpnpScpdActionList.class, UpnpScpd.UpnpScpdStateTable.class, UpnpScpdAction.class, + UpnpScpdAction.UpnpScpdArgumentList.class, UpnpScpdArgument.class, UpnpScpdStateVariable.class }); + + /** + * Constructs the reader using the specified classes to process annotations with + * + * @param classes a non-null, non-empty array of classes + */ + private UpnpXmlReader(@SuppressWarnings("rawtypes") final Class[] classes) { + Objects.requireNonNull(classes, "classes cannot be null"); + + xstream.addPermission(NoTypePermission.NONE); + xstream.allowTypesByWildcard(new String[] { SonyHandlerFactory.class.getPackageName() + ".**" }); + xstream.setClassLoader(getClass().getClassLoader()); + xstream.ignoreUnknownElements(); + xstream.processAnnotations(classes); + } + + /** + * Will translate the XML and casts to the specified class + * + * @param xml the non-null, possibly empty XML to process + * @return the possibly null translation + */ + @SuppressWarnings("unchecked") + public @Nullable T fromXML(final String xml) { + if (!SonyUtil.isEmpty(xml)) { + return (T) this.xstream.fromXML(xml); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..6d598f9e4f9f2 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,11 @@ + + + + binding + Sony Binding + This is a generic binding for Sony products (TVs, receivers and wireless speakers). + local + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..7e5928139cc93 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Sony Binding + This is the binding for Sony Products. + Tim Roberts + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/dial.xml b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/dial.xml new file mode 100644 index 0000000000000..f31dc0b430b8c --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/dial.xml @@ -0,0 +1,41 @@ + + + + + + network-address + + The URL access point for the DIAL service + + + + The MAC address of the device + + + + The access code (or "RQST" to request one) + RQST + + + + The interval, in seconds, to refresh the device state (-1 to disable) + 30 + true + + + + The time, in seconds, to retry a connection attempt (-1 to disable) + 10 + true + + + + The time, in seconds, to check the device status (-1 to disable) + 30 + true + + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/ircc.xml b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/ircc.xml new file mode 100644 index 0000000000000..8d5c34ba59e5d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/ircc.xml @@ -0,0 +1,51 @@ + + + + + + network-address + + The URL access point for the Ircc + + + + The MAC address of the device (TV, bluray, etc) + + + + The Commands Map File + true + + + + The access code (or "RQST" to request one) + RQST + + + + The interval, in seconds, to refresh the device state (-1 to disable) + 30 + true + + + + The time, in seconds, to retry a connection attempt (-1 to disable) + 10 + true + + + + The time, in seconds, to check the device status (-1 to disable) + 30 + true + + + + The Commands Map File that was initially discovered - use the Command Maps File to override + true + + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/scalar.xml b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/scalar.xml new file mode 100644 index 0000000000000..260ef17ab24c6 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/scalar.xml @@ -0,0 +1,77 @@ + + + + + + network-address + + The URL access point for the Scalar Web + + + + The MAC address of the device (TV, bluray, etc) + + + url + + The URL access point for the IRCC Service + + + + The Commands Map File + true + + + + Specify the model name if not automatically discovered (see Discovered Model Name) + true + + + + The access code (or "RQST" to request one) + RQST + + + + The interval, in seconds, to refresh the device state (-1 to disable) + 30 + true + + + + The time, in seconds, to retry a connection attempt (-1 to disable) + 10 + true + + + + The time, in seconds, to check the device status (-1 to disable) + 30 + true + + + + Enable for file based configuration of TV presets + false + true + + + + The MAC address that was initially discovered - use the Device Mac Address to override + true + + + + The Commands Map File that was initially discovered - use the Commands Map File to override + true + + + + The Model Name that was initially discovered - use the Device Model Name to override + true + + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/simpleip.xml b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/simpleip.xml new file mode 100644 index 0000000000000..fa2908fe5238d --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/config/simpleip.xml @@ -0,0 +1,52 @@ + + + + + + network-address + + The IP or host name of the simple ip access point + + + + The MAC address of the device + + + + The Commands Map File + true + + + + The interface being used (eth0 for wired, wlan1 for wireless) + eth0 + true + + + + The interval, in seconds, to refresh the device state (-1 to disable) + 30 + true + + + + The time, in seconds, to retry a connection attempt (-1 to disable) + 10 + true + + + + The time, in seconds, to check the device status (-1 to disable) + 30 + true + + + + The Commands Map File that was initially discovered - use the Command Maps File to override + true + + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/dial.xml b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/dial.xml new file mode 100644 index 0000000000000..593548ed251fa --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/dial.xml @@ -0,0 +1,41 @@ + + + + + + Sony Dial (DIscovery And Launch) device + + + + + + + + DialUDN + + + + + + String + + The title of the application + + + + + Image + + The icon representing the DIAL application + + + + + Switch + + Whether the DIAL application is active (ON) or not (OFF) + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/ircc.xml b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/ircc.xml new file mode 100644 index 0000000000000..950bd058e1cdc --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/ircc.xml @@ -0,0 +1,231 @@ + + + + + + Sony Ircc device + + + + + + + + IrccUDN + + + + + + Common channels used by IRCC + + + + + + + + + + + + + The channels that are active when viewing a source + + + + + + + + + + + + The channels that are active when content is available + + + + + + + + + + + + + + + + + + + + + + Switch + + The Power Status + + + + String + + The IRCC Command to send + + + + String + + The URL of the current content + + + + String + + The text for the current field + + + + Switch + + True if in a text field, false otherwise + + + + + Switch + + True if in a web browser, false otherwise + + + + + Switch + + True if viewing a source, false otherwise + + + + + + String + + The material identifier + + + + + String + + The title of the material + + + + + String + + The class of the material (video, etc) + + + + + String + + The source of the material (DVD, etc) + + + + + String + + The source of the material on zone 2(DVD, etc) + + + + + String + + The media type of the material (DVD, USB, etc) + + + + + String + + The media format of the material (VIDEO, etc) + + + + + String + + The edition of the material + + + + + String + + The material's description + + + + + String + + The genre of the material (Action, Adventure, etc) + + + + + Number + + The duration (in seconds) of the material + + + + String + + The rating of the material (G, PG, etc) + + + + + DateTime + + The release date of the material + + + + String + + The director(s) of the material + + + + + String + + The producer(s) of the material + + + + + String + + The screen writer(s) of the material + + + + + Image + + The icon representing the material + + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/scalar.xml b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/scalar.xml new file mode 100644 index 0000000000000..52d081a6c6423 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/scalar.xml @@ -0,0 +1,1723 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sony Scalar Web + + + + + + + + + + + + + + + + ScalarUDN + + + + + + String + + Dummy Channel - please ignore + + + + + Dimmer + + Scalar Volume + + + + Switch + + Mute audio on all modes [headphone, speakers, etc] + + + + Number + + General Setting for a number + + + + Switch + + General Setting for a switch + + + + Switch + + General Setting for a string + + + + Dimmer + + General Setting for a dimmer + + + + DateTime + + Current time of the device + + + + String + + The current language of the device + + + + String + + The status of the LED indicator (Off/Low/High) + + + + String + + The power savings mode (Off/Low/High) + + + + Switch + + The power status of the device + + + + Switch + + The wake on lan (WOL) mode + + + + String + + The IRCC command to send to the system + + + + Switch + + Send 'ON' to reboot the device + + + + String + + The postal code of the device + + + + String + + The storage name + + + + + String + + Any storage error messages + + + + + String + + The storage file system + + + + + String + + The storage finalization status + + + + + String + + The storage format + + + + + String + + The storage format status + + + + + String + + The storage formattable status + + + + + String + + The storage formatting status + + + + + Number:DataAmount + + The storage free capacity + + + + + String + + Whether the storage has non-standard data + + + + + String + + Whether the storage has unsupported content + + + + + String + + The storage availability status + + + + + String + + The storage locked status + + + + + String + + The storage management information full status + + + + + String + + The storage protection status + + + + + String + + The storage registration status + + + + + String + + The storage self recorded status + + + + + String + + The storage SQV (standard quality voice) status + + + + + Number + + The storage logical unit number + + + + + String + + The storage mounted status + + + + + String + + The storage permission status + + + + + String + + The position of the storage (front, back, internal, etc) + + + + + String + + The storage protocol + + + + + String + + The storage registration date + + + + + Number:DataAmount + + The storage system capacity + + + + + Number:Time + + The time to finalize + + + + + Number:Time + + The time to get storage contents + + + + + String + + The storage type + + + + + String + + The storage uri + + + + + String + + The storage USB device type + + + + + String + + The storage label + + + + + Number:DataAmount + + The storage whole capacity + + + + + String + + App title + + + + + Image + + App Icon + + + + + String + + App Data + + + + + String + + The application command + + + + + + + + + + Switch + + Status of indicator + + + + String + + Current Text Form entry + + + + Switch + + Whether the device will control CEC devices or not + + + + Switch + + Whether the device will automatically change inputs to active MHL sources + + + + Switch + + Whether the device will feed power to MHL devices + + + + Switch + + Whether the device will turn off with CEC + + + + Switch + + Whether the device will power on with source + + + + String + + Source of audio for the screen + + + + String + + Banner mode for TV source + + + + String + + Multiple Screen Mode + + + + String + + Picture-in-Picture (PIP) screen position + + + + String + + The scene setting for the video screen + + + + String + + A comma separated list of schemes + + + + + String + + A comma separated list of sources + + + + + String + + The URI of the content + + + + + String + + Content Parent URI + + + + Number + + The index position of the content + + + + + Number + + The total count of content's children + + + + + String + + The command for the currently selected content + + + + + + + + + String + + The title of the content + + + + + String + + The display number of the content + + + + + String + + The original display number of the content + + + + + String + + The triplet identifier of the content + + + + + Number + + The program number of the content + + + + + String + + The media type of the content + + + + + String + + The program media type of the content + + + + + Number + + The direct remote number for the content + + + + + String + + The EPG visibility for the content + + + + String + + The channel surfing visibility for the content + + + + String + + The visibility for the content + + + + String + + The date the content was started + + + + + String + + The channel name of the content + + + + + Number:DataAmount + + The content file size + + + + + String + + Whether the content is protected or not + + + + String + + Whether the content has been already been played or not + + + + + String + + The content product id + + + + + String + + The type of content + + + + + String + + The content storage URI + + + + + String + + The video codec of the content + + + + + Number + + The chapter count the content + + + + + Number:Time + + The duration of the content + + + + + Number:Time + + The duration of the content + + + + + String + + Comma separated list of audio codecs available + + + + + String + + Comma separated list of audio frequencies available + + + + + String + + Comma separated list of audio channels available + + + + + String + + Comma separated list of subtitle languages available + + + + + String + + Comma separated list of subtitle titles available + + + + + String + + Comma separated list of parental ratings + + + + + String + + Comma separated list of parental rating systems + + + + + String + + Comma separated list of parental rating countries + + + + + Number:DataAmount + + Size of the content + + + + + String + + Time content was created + + + + + Switch + + Whether the content was user created or not + + + + + String + + Content Folder Number + + + + + String + + Content File Number + + + + + String + + Content Artist + + + + + String + + Content Genre + + + + + String + + Content Album Name + + + + + String + + Content Content Kind + + + + + String + + Content is Playable + + + + + String + + Content is Browsable + + + + + String + + Content Remote Play Type + + + + + String + + Content Play List Name + + + + + String + + Content Podcast Name + + + + + Number:Frequency + + Content Broadcast Frequency + + + + + String + + Content Broadcast Band + + + + + Number + + Content Parent Index + + + + + String + + Content is 3D + + + + + String + + Content is 4K + + + + + String + + Content Path + + + + + Number + + Content Clip Count + + + + + String + + Content Description + + + + + String + + Content Event ID + + + + + Number + + Content Global Playback Count + + + + + String + + Content has Resume + + + + + String + + Content is Auto Delete + + + + + String + + Content is New + + + + + String + + Content is Playlist + + + + + String + + Content is Sound Photo + + + + + String + + The output identifier of the content + + + + + String + + The content program service type + + + + + String + + The content program title + + + + + String + + The content repeat type + + + + + String + + The content service + + + + + String + + The content source + + + + + String + + The content source label + + + + + String + + The content sync priority + + + + + Number + + The total count of content available + + + + + String + + The Bravia Internet Video Link (BIVL) Service ID + + + + + String + + The Bravia Internet Video Link (BIVL) Asset ID + + + + + String + + The Bravia Internet Video Link (BIVL) Provider + + + + + String + + The Digital Audio Broadcasting (DAB) Component Label + + + + + String + + The Digital Audio Broadcasting (DAB) Dynamic Label + + + + + String + + The Digital Audio Broadcasting (DAB) Ensemble Label + + + + + String + + The Digital Audio Broadcasting (DAB) Service Label + + + + + String + + The state of the content + + + + + String + + The state supplement information of the content + + + + + String + + URI of the input + + + + + String + + Title of the input + + + + + Switch + + Whether the input is currently being used or not + + + + + String + + Label of the input + + + + + String + + Icon meta data type representing the input + + + + + String + + The input status + + + + + String + + The source playing on the terminal + + + + String + + URI of the Terminal + + + + + String + + Title of the Terminal + + + + + String + + The terminal status + + + + + String + + Label of the Terminal + + + + + Image + + Icon representing the Terminal + + + + + Switch + + Whether the terminal is active (powered on for multi zones) or not + + + + Number + + The parental rating age type + + + + + String + + Sony's parental rating type + + + + + String + + The parental rating country + + + + + String + + The TV parental rating + + + + + String + + The MPAA parental rating + + + + + String + + The CA English rating + + + + + String + + The CA French rating + + + + + Switch + + Whether unrated is locked or not + + + + + String + + The command to control what is currently playing + + + + + + + + + + + + + + + + + + + Number + + The number of the preset to assign + + + + + String + + The URI of the content currently playing + + + + + String + + The Source of the content currently playing + + + + + String + + The Title of the content currently playing + + + + + String + + The display number of the content currently playing + + + + + String + + The original display number of the content currently playing + + + + + String + + The triplet of the content currently playing + + + + + Number + + The program number of the content currently playing + + + + + String + + The program title of the content currently playing + + + + + String + + The start date of the content currently playing + + + + + Number:Time + + The duration of the content currently playing + + + + + String + + The media type of the content currently playing + + + + + String + + The speed of the content currently playing + + + + + String + + The Bravia Internet Video Link (BIVL) service id of the content currently playing + + + + + String + + The Bravia Internet Video Link (BIVL) asset id of the content currently playing + + + + + String + + The Bravia Internet Video Link (BIVL) provider of the content currently playing + + + + + String + + The source label of the content currently playing + + + + + String + + The output identifier of the content currently playing + + + + + String + + The state of the content currently playing + + + + + String + + The state supplement of the content currently playing + + + + + Number:Time + + The current position of the content currently playing + + + + + Number:Time + + The current position of the content currently playing + + + + + Number:Time + + The current duration of the content currently playing + + + + + Number + + The playing step speed of the content currently playing + + + + + String + + The repeat type for the content currently playing + + + + + Number + + The chapter index for the content currently playing + + + + + Number + + The chapter count for the content currently playing + + + + + Number + + The subtitle index for the content currently playing + + + + + String + + The artist for the content currently playing + + + + + String + + The genre for the content currently playing + + + + + String + + The album name for the content currently playing + + + + + String + + The kind of content for the content currently playing + + + + + String + + The file number for the content currently playing + + + + + String + + The channel name for the content currently playing + + + + + String + + The playlist name for the content currently playing + + + + + String + + The podcast name for the content currently playing + + + + + Number + + The total count of content available + + + + + Number:Frequency + + The broad cast frequency of the content currently playing + + + + + String + + The broad cast frequency band of the content currently playing + + + + + String + + The digital audio broadcasting (DAB) component label of the content currently playing + + + + + String + + The digital audio broadcasting (DAB) dynamic label (song, text, etc) of the content currently playing + + + + + String + + The digital audio broadcasting (DAB) ensemble label of the content currently playing + + + + + String + + The digital audio broadcasting (DAB) service label of the content currently playing + + + + + String + + The audio channel of the content currently playing + + + + + String + + The audio codec of the content currently playing + + + + + String + + The audio frequency of the content currently playing + + + + + String + + The parent URI of the content currently playing + + + + + String + + The service of the content currently playing + + + + + Number + + The index of the content currently playing + + + + + Number + + The parent index of the content currently playing + + + + + String + + The video codec of the content currently playing + + + + + String + + The 3d status of the content currently playing + + + + + String + + The path to the content currently playing + + + + + String + + The application name currently playing + + + + + String + + The preset channel + + + + String + + The activates the browser + + + + String + + The URL of the browser + + + + String + + The title of the current browser page + + + + + String + + The type of the current browser page + + + + + Image + + The favicon of the current browser page + + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/simpleip.xml b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/simpleip.xml new file mode 100644 index 0000000000000..c4d43a48ef9b3 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/OH-INF/thing/simpleip.xml @@ -0,0 +1,122 @@ + + + + + + Sony Simple IP Device + + + + + + + + + + + + + + + + + + + + SimpleUDN + + + + + String + + The IR Code to send + + + + Switch + + Power on/off device + + + + Switch + + Toggle Power on/off + + + + Dimmer + + The volume + + + + Switch + + Mute/Unmute the audio + + + + String + + The channel ("5.1", "50.2", etc) + + + + String + + The triplet channel ("32736.32736.1024") + + + + String + + The Input Source ("atsct", etc) + + + + String + + The Input ("HDMI", "TV", etc) + + + + String + + The scene ("auto", "auto24pSync", "general", etc) + + + + Switch + + Mute/Unmute the picture + + + + Switch + + Toggles the picture mute + + + + Switch + + Enables/Disabled Picture-in-Picture + + + + Switch + + Toggles Picture-in-Picture + + + + Switch + + Toggles Picture-in-Picture Position + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/OSGI-INF/SonyDefinitionProviderImpl.properties b/bundles/org.openhab.binding.sony/src/main/resources/OSGI-INF/SonyDefinitionProviderImpl.properties new file mode 100644 index 0000000000000..c37e33088e398 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/OSGI-INF/SonyDefinitionProviderImpl.properties @@ -0,0 +1,4 @@ +SonyFolderSource.Folder.ThingTypes = /db/local/types +SonyFolderSource.Folder.DefinitionTypes = /definitions/types +SonyFolderSource.Folder.DefinitionCapabilities = /definitions/capabilities +SonyFolderSource.WatchDog.Interval = 30 \ No newline at end of file diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/.gitignore b/bundles/org.openhab.binding.sony/src/main/resources/web/.gitignore new file mode 100644 index 0000000000000..a0dddc6fb8c6b --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/.gitignore @@ -0,0 +1,21 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/site.css b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/site.css new file mode 100644 index 0000000000000..d25aefdee5779 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/site.css @@ -0,0 +1,107 @@ +#app { + font-family: "Avenir", Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: #2c3e50; + margin: 5px; +} + +#app h1 { + padding: 0 15px 15px 0; + margin: 0; + display: table-cell; +} + +.even { + background-color: lavender; +} + +tr.diff { + background-color: orange; +} + +table tr.selected { + background-color: aquamarine; +} + +.fileRead input[type="file"] { + display: none; +} + +.fileRead .btn { + margin: 0 0 3px 5px; + display: inline-block; + border: solid 1px black; + padding: 3px; +} + +.fileRead .btn p { + display: inline-block; + margin: 0 0 0 5px; +} + +.method { + float: left; + width: 50em; +} + +.method #baseUrl { + width: 20em; +} + +.method #service { + width: 7em; +} + +.method #version { + width: 4em; +} + +.method #parms { + width: 35em; +} +.method label { + display: inline-block; + width: 7em; +} + +.method button svg { + margin-right: 0.5em; +} + +.methods #methods { + border: solid 1px black; + padding: 5px; + margin-top: 5px; + overflow: auto; + height: 20em; +} + +.methods #methods td { + white-space: nowrap; + padding: 0px 2px; + border: solid 1px lightgray; +} + +.methods .title { + margin-left: 0.5em; +} + +.methods .variationEditor { + width: 2em; +} + +.methods button { + margin-left: 0.5em; +} + +.methods button svg { + margin-right: 0.5em; +} + +#results { + clear: both; + display: block; + width: 100em; + height: 20em; +} diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/thirdparty/all.min.css b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/thirdparty/all.min.css new file mode 100644 index 0000000000000..740543d89fe96 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/thirdparty/all.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.1.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{animation:a 2s infinite linear}.fa-pulse{animation:a 1s infinite steps(8)}@keyframes a{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-balance-scale:before{content:"\f24e"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bicycle:before{content:"\f206"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blind:before{content:"\f29d"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-open:before{content:"\f518"}.fa-bookmark:before{content:"\f02e"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-certificate:before{content:"\f0a3"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-concierge-bell:before{content:"\f562"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-credit-card:before{content:"\f09d"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-deviantart:before{content:"\f1bd"}.fa-diagnoses:before{content:"\f470"}.fa-dice:before{content:"\f522"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-gift:before{content:"\f06b"}.fa-git:before{content:"\f1d3"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hashtag:before{content:"\f292"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-houzz:before{content:"\f27c"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-internet-explorer:before{content:"\f26b"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mercury:before{content:"\f223"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-motorcycle:before{content:"\f21c"}.fa-mouse-pointer:before{content:"\f245"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-nintendo-switch:before{content:"\f418"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-old-republic:before{content:"\f510"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-people-carry:before{content:"\f4ce"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-volume:before{content:"\f2a0"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poo:before{content:"\f2fe"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-r-project:before{content:"\f4f7"}.fa-random:before{content:"\f074"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-rendact:before{content:"\f3e4"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-sass:before{content:"\f41e"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-search:before{content:"\f002"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skull:before{content:"\f54c"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowflake:before{content:"\f2dc"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toolbox:before{content:"\f552"}.fa-tooth:before{content:"\f5c9"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-train:before{content:"\f238"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-moving:before{content:"\f4df"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/thirdparty/jquery.jgrowl.min.css b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/thirdparty/jquery.jgrowl.min.css new file mode 100644 index 0000000000000..ea39484150f2e --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/thirdparty/jquery.jgrowl.min.css @@ -0,0 +1 @@ +.jGrowl{z-index:9999;color:#fff;font-size:12px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;position:fixed}.jGrowl.top-left{left:0;top:0}.jGrowl.top-right{right:0;top:0}.jGrowl.bottom-left{left:0;bottom:0}.jGrowl.bottom-right{right:0;bottom:0}.jGrowl.center{top:0;width:50%;left:25%}.jGrowl.center .jGrowl-closer,.jGrowl.center .jGrowl-notification{margin-left:auto;margin-right:auto}.jGrowl-notification{background-color:#000;opacity:.9;-ms-filter:alpha(90);filter:alpha(90);zoom:1;width:250px;padding:10px;margin:10px;text-align:left;display:none;border-radius:5px;min-height:40px}.jGrowl-notification .ui-state-highlight,.jGrowl-notification .ui-widget-content .ui-state-highlight,.jGrowl-notification .ui-widget-header .ui-state-highlight{border:1px solid #000;background:#000;color:#fff}.jGrowl-notification .jGrowl-header{font-weight:700;font-size:.85em}.jGrowl-notification .jGrowl-close{background-color:transparent;color:inherit;border:none;z-index:99;float:right;font-weight:700;font-size:1em;cursor:pointer}.jGrowl-closer{background-color:#000;opacity:.9;-ms-filter:alpha(90);filter:alpha(90);zoom:1;width:250px;padding:10px;margin:10px;display:none;border-radius:5px;padding-top:4px;padding-bottom:4px;cursor:pointer;font-size:.9em;font-weight:700;text-align:center}.jGrowl-closer .ui-state-highlight,.jGrowl-closer .ui-widget-content .ui-state-highlight,.jGrowl-closer .ui-widget-header .ui-state-highlight{border:1px solid #000;background:#000;color:#fff}@media print{.jGrowl{display:none}} \ No newline at end of file diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.eot b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.eot new file mode 100644 index 0000000000000..f57c1e0feb495 Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.eot differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.svg b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.svg new file mode 100644 index 0000000000000..59645e6870d6f --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.svg @@ -0,0 +1,1130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.ttf b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000000000..748fa4b6fb7de Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.ttf differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.woff b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.woff new file mode 100644 index 0000000000000..f1357f3ddb9c9 Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.woff differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.woff2 b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000000000..3424267f2d1ff Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-brands-400.woff2 differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.eot b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.eot new file mode 100644 index 0000000000000..506c3b1a0469e Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.eot differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.svg b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.svg new file mode 100644 index 0000000000000..de18ef1508890 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.ttf b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000000000..3ba10c3627de1 Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.ttf differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.woff b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.woff new file mode 100644 index 0000000000000..712e10be4977a Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.woff differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.woff2 b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000000000..8fba9253f8168 Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-regular-400.woff2 differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.eot b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.eot new file mode 100644 index 0000000000000..7bc20d4e5fedc Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.eot differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.svg b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.svg new file mode 100644 index 0000000000000..1534b64befb11 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.svg @@ -0,0 +1,2231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.ttf b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000000000..deae781f89f5d Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.ttf differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.woff b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.woff new file mode 100644 index 0000000000000..b1e79db9e64af Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.woff differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.woff2 b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000000000..5d6dd4daf2ad6 Binary files /dev/null and b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/css/webfonts/fa-solid-900.woff2 differ diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/index.html b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/index.html new file mode 100644 index 0000000000000..c71bed4f58311 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/index.html @@ -0,0 +1,133 @@ + + + + + + Sony + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + +
+
+
InputOutput
notValidnotValid
extInput:bd-dvdin-bd-dvd (bd-dvd for input only)
extInput:hdmiin-hdmi (hdmi for input only)
extInput:hdmi?port=1in-hdmi1
extInput:video?port=2in-video2
extOutput:zone?zone=2out-zone2
extInput:bluetooth?blah=3in-bluetooth
extInput:cec?type=recorder&port=2&logicalAddr=1in-cec2-1
+ + + + + + + + + + + + + +
+ + + +
+ + + + +
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/app.js b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/app.js new file mode 100644 index 0000000000000..de192fed6deb0 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/app.js @@ -0,0 +1,4 @@ +(function(helper, $, undefined) {})( + (window.sonyapp = window.sonyapp || {}), + jQuery +); diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/capabilities.js b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/capabilities.js new file mode 100644 index 0000000000000..15229d1c1d2fa --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/capabilities.js @@ -0,0 +1,427 @@ +(function(helper, $, undefined) { + helper.initialize = function(jqueryId) { + const $jqueryId = $(jqueryId); + + const koModel = new myModel(); + ko.applyBindings(koModel, $jqueryId[0]); + }; + + class myModel { + constructor() { + this.method = ko.observable(new currentMethod()); + this.result = ko.observable(); + this.fileTitle = ko.observable(); + this.selectedSortIdx = ko.observable(); + this.methods = ko.observableArray(); + this.sortedMethods = ko.pureComputed(() => { + return this.methods.sorted(this.sort); + }); + } + + loadFile(elm, event) { + const files = event.target.files; + if (files.length > 0) { + const reader = new FileReader(); + + reader.onload = e => { + const jsonData = JSON.parse(reader.result); + const defs = this.getMethods(jsonData); + this.selectedSortIdx(-1); + this.method(new currentMethod()); + this.fileTitle(jsonData.modelName); + this.methods(defs); + $.jGrowl(`Loaded ${defs.length} methods`); + }; + reader.readAsText(files[0]); + } + } + + loadRestFile(elm, event) { + const files = event.target.files; + if (files.length > 0) { + const reader = new FileReader(); + + reader.onload = e => { + const jsonData = JSON.parse(reader.result); + const defs = this.getMethodsFromRestApi(jsonData); + this.selectedSortIdx(-1); + this.method(new currentMethod()); + this.fileTitle(jsonData.modelName); + this.methods(defs); + $.jGrowl(`Loaded ${defs.length} methods`); + }; + reader.readAsText(files[0]); + } + } + + mergeFile(elm, event) { + const files = event.target.files; + if (files.length > 0) { + const reader = new FileReader(); + + reader.onload = e => { + const jsonData = JSON.parse(reader.result); + if (!this.fileTitle().includes(jsonData.modelName)) { + const defs = this.getMethods(jsonData); + this.selectedSortIdx(-1); + this.method(new currentMethod()); + + let idx = 0; + const mthds = this.methods(); + defs.forEach(def => { + if (!mthds.find(mthd => def.isSameDef(mthd))) { + idx++; + mthds.push(def); + } + }); + + this.methods(mthds); + + if (this.fileTitle() === "") { + this.fileTitle(jsonData.modelName); + } else { + this.fileTitle(this.fileTitle() + "," + jsonData.modelName); + } + $.jGrowl(`Loaded ${idx} methods`); + } else { + $.jGrowl(`Already loaded ${jsonData.modelName} methods`); + } + }; + reader.readAsText(files[0]); + } + } + + saveFile() { + const services = new Map(); + this.methods().forEach(m => { + const srv = services.get(m.serviceName); + + if (srv === undefined) { + services.set(m.serviceName, new RestApi(m)); + } else { + if (m.methodType === MethodDef.Method) { + srv.methods.push(new RestApiMethod(m.method)); + } else { + srv.notifications.push(new RestApiMethod(m.method)); + } + } + }); + + const restApi = [...services.values()]; + const blb = new Blob([JSON.stringify(restApi)], { + type: "application/json" + }); + saveAs(blb, "restapi.json"); + } + + selectMethod(data, sortIdx) { + this.selectedSortIdx(sortIdx); + const idx = this.methods().indexOf(data); + this.method(new currentMethod(this.methods()[idx])); + } + + deleteMethod(idx) { + this.method(this.method().splice(idx, 1)); + } + + runCommand() { + this.result("waiting..."); + let parms = this.method().parms(); + if (Array.isArray(parms)) { + parms = parms.join(","); + } + + axios + .post("/sony/app/execute", { + baseUrl: this.method().baseUrl(), + serviceName: this.method().service(), + transport: this.method().transport(), + command: this.method().command(), + version: this.method().version(), + parms + }) + .then( + res => { + if (res.data.success === true) { + this.result(res.data.results); + } else { + this.result(res.data.message); + } + }, + res => { + const msg = + res.response === undefined + ? res.message + : res.response.status + " " + res.response.statusText; + $.jGrowl("Error executing: " + msg, { + theme: "jgrowl-error", + sticky: true + }); + this.result(msg); + } + ); + } + + getMethods(jsonData) { + const defs = []; + jsonData.services.forEach(srv => { + srv.methods.forEach(mthd => + defs.push( + new MethodDef( + jsonData.baseURL, + jsonData.modelName, + srv.serviceName, + srv.version, + srv.transport, + new Method( + mthd.baseUrl, + mthd.service, + mthd.transport, + mthd.methodName, + mthd.version, + mthd.variation, + mthd.parms, + mthd.retVals + ), + MethodDef.Method + ) + ) + ); + srv.notifications.forEach(mthd => + defs.push( + new MethodDef( + jsonData.baseURL, + jsonData.modelName, + srv.serviceName, + srv.version, + srv.transport, + new Method( + mthd.baseUrl, + mthd.service, + mthd.transport, + mthd.methodName, + mthd.version, + mthd.variation, + mthd.parms, + mthd.retVals + ), + MethodDef.Notification + ) + ) + ); + }); + return defs; + } + + getMethodsFromRestApi(jsonData) { + const defs = []; + jsonData.forEach(srv => { + srv.methods.forEach(m => { + defs.push( + new MethodDef( + "", + "", + srv.serviceName, + srv.version, + "", + new Method( + "", + srv.serviceName, + "", + m.methodName, + m.version, + m.variation, + m.parms, + m.retVals + ), + MethodDef.Method + ) + ); + }); + srv.notifications.forEach(m => { + defs.push( + new MethodDef( + "", + "", + srv.serviceName, + srv.version, + "", + new Method( + "", + srv.serviceName, + "", + m.methodName, + m.version, + m.variation, + m.parms, + m.retVals + ), + MethodDef.Notification + ) + ); + }); + }); + + return defs; + } + + sort(a, b) { + if (a.serviceName < b.serviceName) { + return -1; + } + if (a.serviceName > b.serviceName) { + return 1; + } + if (a.methodType < b.methodType) { + return -1; + } + if (a.methodType > b.methodType) { + return 1; + } + if (a.method.command < b.method.command) { + return -1; + } + if (a.method.command > b.method.command) { + return 1; + } + const an = parseFloat(a.method.version); + const bn = parseFloat(b.method.version); + return an - bn; + } + } + + class currentMethod { + constructor(mth) { + this.baseUrl = ko.observable(mth === undefined ? "" : mth.baseUrl); + this.transport = ko.observable(mth === undefined ? "" : mth.transport); + this.service = ko.observable(mth === undefined ? "" : mth.serviceName); + this.command = ko.observable(mth === undefined ? "" : mth.method.command); + this.version = ko.observable(mth === undefined ? "" : mth.method.version); + this.parms = ko.observable( + mth === undefined || mth.method.parms === undefined + ? "" + : mth.method.parms.join(",") + ); + } + } + + class koResult { + constructor() { + this.result = ko.observable(); + } + } + + class MethodDef { + static Method = "M"; + static Notification = "N"; + + constructor( + baseUrl, + modelName, + serviceName, + serviceVersion, + transport, + method, + methodType + ) { + this.baseUrl = baseUrl; + this.modelName = modelName; + this.serviceName = serviceName; + this.serviceVersion = serviceVersion; + this.transport = transport; + this.method = method; + this.methodType = methodType; + } + + isDuplicateKey(mth) { + return ( + this.serviceName === mth.serviceName && + this.method.command === mth.method.command && + this.method.version === mth.method.version && + this.method.variation === mth.method.variation + ); + } + + isSameDef(mth) { + return ( + this.serviceName === mth.serviceName && + this.method.command === mth.method.command && + this.method.version === mth.method.version && + this.method.variation === mth.method.variation && + this.arrEquals(this.method.parms, mth.method.parms) && + this.arrEquals(this.method.retVals, mth.method.retVals) && + this.methodType === mth.methodType + ); + } + + arrEquals(array1, array2) { + const arr1 = array1.sort(); + const arr2 = array2.sort(); + return ( + arr1.length === arr2.length && + arr1.every((value, index) => value === arr2[index]) + ); + } + } + + class Method { + constructor( + baseUrl, + service, + transport, + command, + version, + variation, + parms, + retVals + ) { + this.baseUrl = + baseUrl === undefined ? "http://192.168.1.167/sony" : baseUrl; + this.service = service === undefined ? "service" : service; + this.transport = transport === undefined ? "auto" : transport; + this.command = command === undefined ? "getPowerStatus" : command; + this.version = version === undefined ? "1.1" : version; + this.variation = variation === undefined ? 0 : variation; + + const myParms = + parms === undefined + ? undefined + : parms.filter(e => e !== undefined && e !== null && e !== ""); + this.parms = myParms === undefined ? [] : myParms; + + const myVals = + retVals === undefined + ? undefined + : retVals.filter(e => e !== undefined && e !== null && e !== ""); + this.retVals = myVals === undefined ? [] : myVals; + } + } + + class RestApi { + constructor(methodDef) { + this.serviceName = methodDef.serviceName; + this.version = methodDef.serviceVersion; + this.methods = new Array(); + this.notifications = new Array(); + + if (methodDef.method !== undefined) { + if (methodDef.methodType === MethodDef.Method) { + this.methods.push(new RestApiMethod(methodDef.method)); + } else { + this.notifications.push(new RestApiMethod(methodDef.method)); + } + } + } + } + + class RestApiMethod { + constructor(mthd) { + this.methodName = mthd.command; + this.version = mthd.version; + this.variation = mthd.variation; + + this.parms = mthd.parms; + this.retVals = mthd.retVals; + } + } +})((window.sonyapp.capabilities = window.sonyapp.capabilities || {}), jQuery); diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/thirdparty/FileSaver.min.js b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/thirdparty/FileSaver.min.js new file mode 100644 index 0000000000000..183d42a103536 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/thirdparty/FileSaver.min.js @@ -0,0 +1,3 @@ +(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(b,c,d){var e=new XMLHttpRequest;e.open("GET",b),e.responseType="blob",e.onload=function(){a(e.response,c,d)},e.onerror=function(){console.error("could not download file")},e.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(a,b,d,e){if(e=e||open("","_blank"),e&&(e.document.title=e.document.body.innerText="downloading..."),"string"==typeof a)return c(a,b,d);var g="application/octet-stream"===a.type,h=/constructor/i.test(f.HTMLElement)||f.safari,i=/CriOS\/[\d]+/.test(navigator.userAgent);if((i||g&&h)&&"undefined"!=typeof FileReader){var j=new FileReader;j.onloadend=function(){var a=j.result;a=i?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),e?e.location.href=a:location=a,e=null},j.readAsDataURL(a)}else{var k=f.URL||f.webkitURL,l=k.createObjectURL(a);e?e.location=l:location.href=l,e=null,setTimeout(function(){k.revokeObjectURL(l)},4E4)}});f.saveAs=a.saveAs=a,"undefined"!=typeof module&&(module.exports=a)}); + +//# sourceMappingURL=FileSaver.min.js.map \ No newline at end of file diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/thirdparty/axios.min.js b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/thirdparty/axios.min.js new file mode 100644 index 0000000000000..b87c0e3863645 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/thirdparty/axios.min.js @@ -0,0 +1,3 @@ +/* axios v0.19.2 | (c) 2020 by Matt Zabriskie */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";function r(e){var t=new s(e),n=i(s.prototype.request,t);return o.extend(n,s.prototype,t),o.extend(n,t),n}var o=n(2),i=n(3),s=n(4),a=n(22),u=n(10),c=r(u);c.Axios=s,c.create=function(e){return r(a(c.defaults,e))},c.Cancel=n(23),c.CancelToken=n(24),c.isCancel=n(9),c.all=function(e){return Promise.all(e)},c.spread=n(25),e.exports=c,e.exports.default=c},function(e,t,n){"use strict";function r(e){return"[object Array]"===j.call(e)}function o(e){return"undefined"==typeof e}function i(e){return null!==e&&!o(e)&&null!==e.constructor&&!o(e.constructor)&&"function"==typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}function s(e){return"[object ArrayBuffer]"===j.call(e)}function a(e){return"undefined"!=typeof FormData&&e instanceof FormData}function u(e){var t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function c(e){return"string"==typeof e}function f(e){return"number"==typeof e}function p(e){return null!==e&&"object"==typeof e}function d(e){return"[object Date]"===j.call(e)}function l(e){return"[object File]"===j.call(e)}function h(e){return"[object Blob]"===j.call(e)}function m(e){return"[object Function]"===j.call(e)}function y(e){return p(e)&&m(e.pipe)}function g(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function v(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function x(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product&&"NativeScript"!==navigator.product&&"NS"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function w(e,t){if(null!==e&&"undefined"!=typeof e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n=200&&e<300}};u.headers={common:{Accept:"application/json, text/plain, */*"}},i.forEach(["delete","get","head"],function(e){u.headers[e]={}}),i.forEach(["post","put","patch"],function(e){u.headers[e]=i.merge(a)}),e.exports=u},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){r.forEach(e,function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])})}},function(e,t,n){"use strict";var r=n(2),o=n(13),i=n(5),s=n(16),a=n(19),u=n(20),c=n(14);e.exports=function(e){return new Promise(function(t,f){var p=e.data,d=e.headers;r.isFormData(p)&&delete d["Content-Type"];var l=new XMLHttpRequest;if(e.auth){var h=e.auth.username||"",m=e.auth.password||"";d.Authorization="Basic "+btoa(h+":"+m)}var y=s(e.baseURL,e.url);if(l.open(e.method.toUpperCase(),i(y,e.params,e.paramsSerializer),!0),l.timeout=e.timeout,l.onreadystatechange=function(){if(l&&4===l.readyState&&(0!==l.status||l.responseURL&&0===l.responseURL.indexOf("file:"))){var n="getAllResponseHeaders"in l?a(l.getAllResponseHeaders()):null,r=e.responseType&&"text"!==e.responseType?l.response:l.responseText,i={data:r,status:l.status,statusText:l.statusText,headers:n,config:e,request:l};o(t,f,i),l=null}},l.onabort=function(){l&&(f(c("Request aborted",e,"ECONNABORTED",l)),l=null)},l.onerror=function(){f(c("Network Error",e,null,l)),l=null},l.ontimeout=function(){var t="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),f(c(t,e,"ECONNABORTED",l)),l=null},r.isStandardBrowserEnv()){var g=n(21),v=(e.withCredentials||u(y))&&e.xsrfCookieName?g.read(e.xsrfCookieName):void 0;v&&(d[e.xsrfHeaderName]=v)}if("setRequestHeader"in l&&r.forEach(d,function(e,t){"undefined"==typeof p&&"content-type"===t.toLowerCase()?delete d[t]:l.setRequestHeader(t,e)}),r.isUndefined(e.withCredentials)||(l.withCredentials=!!e.withCredentials),e.responseType)try{l.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&l.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&l.upload&&l.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then(function(e){l&&(l.abort(),f(e),l=null)}),void 0===p&&(p=null),l.send(p)})}},function(e,t,n){"use strict";var r=n(14);e.exports=function(e,t,n){var o=n.config.validateStatus;!o||o(n.status)?e(n):t(r("Request failed with status code "+n.status,n.config,null,n.request,n))}},function(e,t,n){"use strict";var r=n(15);e.exports=function(e,t,n,o,i){var s=new Error(e);return r(s,t,n,o,i)}},function(e,t){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e.isAxiosError=!0,e.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},e}},function(e,t,n){"use strict";var r=n(17),o=n(18);e.exports=function(e,t){return e&&!r(t)?o(e,t):t}},function(e,t){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t,n){"use strict";var r=n(2),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,i,s={};return e?(r.forEach(e.split("\n"),function(e){if(i=e.indexOf(":"),t=r.trim(e.substr(0,i)).toLowerCase(),n=r.trim(e.substr(i+1)),t){if(s[t]&&o.indexOf(t)>=0)return;"set-cookie"===t?s[t]=(s[t]?s[t]:[]).concat([n]):s[t]=s[t]?s[t]+", "+n:n}}),s):s}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){function e(e){var t=e;return n&&(o.setAttribute("href",t),t=o.href),o.setAttribute("href",t),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:"/"===o.pathname.charAt(0)?o.pathname:"/"+o.pathname}}var t,n=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a");return t=e(window.location.href),function(n){var o=r.isString(n)?e(n):n;return o.protocol===t.protocol&&o.host===t.host}}():function(){return function(){return!0}}()},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,i,s){var a=[];a.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&a.push("expires="+new Date(n).toGMTString()),r.isString(o)&&a.push("path="+o),r.isString(i)&&a.push("domain="+i),s===!0&&a.push("secure"),document.cookie=a.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){t=t||{};var n={},o=["url","method","params","data"],i=["headers","auth","proxy"],s=["baseURL","url","transformRequest","transformResponse","paramsSerializer","timeout","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","maxContentLength","validateStatus","maxRedirects","httpAgent","httpsAgent","cancelToken","socketPath"];r.forEach(o,function(e){"undefined"!=typeof t[e]&&(n[e]=t[e])}),r.forEach(i,function(o){r.isObject(t[o])?n[o]=r.deepMerge(e[o],t[o]):"undefined"!=typeof t[o]?n[o]=t[o]:r.isObject(e[o])?n[o]=r.deepMerge(e[o]):"undefined"!=typeof e[o]&&(n[o]=e[o])}),r.forEach(s,function(r){"undefined"!=typeof t[r]?n[r]=t[r]:"undefined"!=typeof e[r]&&(n[r]=e[r])});var a=o.concat(i).concat(s),u=Object.keys(t).filter(function(e){return a.indexOf(e)===-1});return r.forEach(u,function(r){"undefined"!=typeof t[r]?n[r]=t[r]:"undefined"!=typeof e[r]&&(n[r]=e[r])}),n}},function(e,t){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},function(e,t,n){"use strict";function r(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new o(e),t(n.reason))})}var o=n(23);r.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},r.source=function(){var e,t=new r(function(t){e=t});return{token:t,cancel:e}},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}}])}); +//# sourceMappingURL=axios.min.map \ No newline at end of file diff --git a/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/thirdparty/jquery.jgrowl.min.js b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/thirdparty/jquery.jgrowl.min.js new file mode 100644 index 0000000000000..7e8b6fb3510a8 --- /dev/null +++ b/bundles/org.openhab.binding.sony/src/main/resources/web/sonyapp/js/thirdparty/jquery.jgrowl.min.js @@ -0,0 +1,2 @@ +!function(a){a.jGrowl=function(b,c){0===a("#jGrowl").length&&a('
').addClass(c&&c.position?c.position:a.jGrowl.defaults.position).appendTo(c&&c.appendTo?c.appendTo:a.jGrowl.defaults.appendTo),a("#jGrowl").jGrowl(b,c)},a.fn.jGrowl=function(b,c){if(void 0===c&&a.isPlainObject(b)&&(c=b,b=c.message),a.isFunction(this.each)){var d=arguments;return this.each(function(){void 0===a(this).data("jGrowl.instance")&&(a(this).data("jGrowl.instance",a.extend(new a.fn.jGrowl,{notifications:[],element:null,interval:null})),a(this).data("jGrowl.instance").startup(this)),a.isFunction(a(this).data("jGrowl.instance")[b])?a(this).data("jGrowl.instance")[b].apply(a(this).data("jGrowl.instance"),a.makeArray(d).slice(1)):a(this).data("jGrowl.instance").create(b,c)})}},a.extend(a.fn.jGrowl.prototype,{defaults:{pool:0,header:"",group:"",sticky:!1,position:"top-right",appendTo:"body",glue:"after",theme:"default",themeState:"highlight",corners:"10px",check:250,life:3e3,closeDuration:"normal",openDuration:"normal",easing:"swing",closer:!0,closeTemplate:"×",closerTemplate:"
[ close all ]
",log:function(){},beforeOpen:function(){},afterOpen:function(){},open:function(){},beforeClose:function(){},close:function(){},click:function(){},animateOpen:{opacity:"show"},animateClose:{opacity:"hide"}},notifications:[],element:null,interval:null,create:function(b,c){var d=a.extend({},this.defaults,c);"undefined"!=typeof d.speed&&(d.openDuration=d.speed,d.closeDuration=d.speed),this.notifications.push({message:b,options:d}),d.log.apply(this.element,[this.element,b,d])},render:function(b){var c=this,d=b.message,e=b.options;e.themeState=""===e.themeState?"":"ui-state-"+e.themeState;var f=a("
").addClass("jGrowl-notification alert "+e.themeState+" ui-corner-all"+(void 0!==e.group&&""!==e.group?" "+e.group:"")).append(a("