diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index a572a4b2a52ca..080aa41376db4 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1756,6 +1756,11 @@
org.openhab.binding.sonos${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.sony
+ ${project.version}
+ org.openhab.addons.bundlesorg.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
+
+
+
+
+
+
+
+
+
+ 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
+ *
+ *
+ *
+ * @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:
+ *
+ *
+ *
+ * @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
+ *
+ *
+ *
+ * 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.
+ *
+ *
+ *
+ * @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
+ *
+ *
+ *
+ * @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
+ *
+ *
+ *
+ * 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:
+ *
+ *
+ *
+ * @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("" + TEXTROOT + ">");
+ }
+
+ 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.
+ *
+ *
+ *
+ * @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..e78fc66a71e28
--- /dev/null
+++ b/bundles/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/net/HttpRequest.java
@@ -0,0 +1,259 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 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