diff --git a/bundles/org.openhab.binding.amazonechocontrol/NOTICE b/bundles/org.openhab.binding.amazonechocontrol/NOTICE index 38d625e349232..7441ee9ea95d1 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/NOTICE +++ b/bundles/org.openhab.binding.amazonechocontrol/NOTICE @@ -11,3 +11,5 @@ https://www.eclipse.org/legal/epl-2.0/. == Source Code https://github.com/openhab/openhab-addons + +Parts of this code have been forked from https://github.com/smarthomej/addons \ No newline at end of file diff --git a/bundles/org.openhab.binding.amazonechocontrol/README.md b/bundles/org.openhab.binding.amazonechocontrol/README.md index 2f42cde8b8816..4e323e83f16a9 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/README.md +++ b/bundles/org.openhab.binding.amazonechocontrol/README.md @@ -1,6 +1,22 @@ # Amazon Echo Control Binding -This binding can control Amazon Echo devices (Alexa). +This binding can control Amazon Echo devices (Alexa) and Smarthome devices connected through Alexa or a skill. + +Upgrade notice: + +- The `lastVoiceCommand` channel of the `amazonechocontrol` binding changed its behavior in version 5.0.0. +Due to a wrong implementation the channel changed it's state to an empty string if the same command was received again. +This has been corrected. +If you want to be notified about every state update, please adjust your rule triggers to "received update". +If you want to be notified about state changes (i.e. different commands), use "state changed". +- The write-only channels now use `autoUpdatePolicy=veto` (i.e. they don't update the item's state when a command was send). +- The channels `amazonMusic`, `amazonMusicTrackId`, `amazonPlaylistId`, `radio` and `radioStationId` have been removed because they are no longer supported from Amazon. +You can use the `textCommand` channel with a value of `Play playlist CrazyMusic on AmazonMusic` instead. +- The `lastVoiceCommand` channel will be converted to a read-only channel. +Using commands to that channel is deprecated and will stop working in future versions. +Please use the `textToSpeech` channel instead. + +## What this can be used for It provides features to control and view the current state of echo devices: @@ -36,6 +52,7 @@ It also provides features to control devices connected to your echo: - control groups of lights or just single bulbs - receive the current state of the lights - turn on/off smart plugs (e. g. OSRAM) +- receive states of attached sensors like particulate matter or carbon monoxide Restrictions: @@ -86,8 +103,8 @@ With the possibility to control your lights you could do: You must define an `account` (Bridge) before defining any other Thing can be used. -1. Create an 'Amazon Account' thing -1. open the url YOUR_OPENHAB/amazonechocontrol in your browser (e.g. `http://openhab:8080/amazonechocontrol/`), click the link for your account thing and login. +1. Create an Amazon `account` thing +1. Open the url `YOUR_OPENHAB/amazonechocontrol` in your browser (e.g. http://openhab:8080/amazonechocontrol/), click the link for your account thing and login. 1. You should see now a message that the login was successful 1. If you encounter redirect/page refresh issues, enable two-factor authentication (2FA) on your Amazon account. @@ -108,11 +125,19 @@ See section *Smart Home Devices* below for more information. ### `account` Bridge Configuration -| Configuration name | Default | Description | -|-------------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------| -| discoverSmartHome | 0 | 0...No discover, 1...Discover direct connected, 2...Discover direct and Alexa skill devices, 3...Discover direct, Alexa and openHAB skill devices | -| pollingIntervalSmartHomeAlexa | 30 | Defines the time in seconds for openHAB to pull the state of the Alexa connected devices. The minimum is 10 seconds. | -| pollingIntervalSmartSkills | 120 | Defines the time in seconds for openHAB to pull the state of the over a skill connected devices. The minimum is 60 seconds. | +| Configuration name | Default | Description | +|---------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `discoverSmartHome` | 0 | 0...No discover, 1...Discover direct connected, 2...Discover direct and Alexa skill devices, 3...Discover direct, Alexa and openHAB skill devices | +| `pollingIntervalSmartHomeAlexa` | 30 | Defines the time in seconds for openHAB to pull the state of the Alexa connected devices. The minimum is 10 seconds. | +| `pollingIntervalSmartSkills` | 120 | Defines the time in seconds for openHAB to pull the state of the over a skill connected devices. The minimum is 60 seconds. | +| `activityRequestDelay` | 10 | The number of seconds between a voice command was detected and the received command is requested from the server. The minimum is 2 seconds. Lower values improve response time but may result in loss of events. | + +### Channels + +| Channel Type ID | Item Type | Access Mode | Thing Type | Description | +|-----------------|-----------|-------------|------------|--------------------------------------------------| +| `sendMessage` | String | W | account | Write Only! Sends a message to the Echo devices. | + ### Thing Configuration @@ -120,102 +145,64 @@ The `echo`, `echospot`, `echoshow` and `wha` have the same configuration: | Configuration name | Description | |--------------------------|----------------------------------------------------| -| serialNumber | Serial number of the Amazon Echo in the Alexa app | - -You will find the serial number in the Alexa app or on the webpage YOUR_OPENHAB/amazonechocontrol/YOUR_ACCOUNT (e.g. `http://openhab:8080/amazonechocontrol/account1`). - -#### `flashbriefingprofile` Thing Configuration +| `serialNumber` | Serial number of the Amazon Echo in the Alexa app | + +You will find the serial number in the Alexa app or on the webpage YOUR_OPENHAB/amazonechocontrol/YOUR_ACCOUNT (e.g. http://openhab:8080/amazonechocontrol/account1). + +### Channels + +| Channel Type ID | Item Type | Access Mode | Thing Type | Description | +|-----------------------|-------------|-------------|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| player | Player | R/W | echo, echoshow, echospot, wha | Control the music player (Supported commands: PLAY or ON, PAUSE or OFF, NEXT, PREVIOUS, REWIND, FASTFORWARD) | +| volume | Dimmer | R/W | echo, echoshow, echospot | Control the volume | +| equalizerTreble | Number | R/W | echo, echoshow, echospot | Control the treble (value from -6 to 6) | +| equalizerMidrange | Number | R/W | echo, echoshow, echospot | Control the midrange (value from -6 to 6) | +| equalizerBass | Number | R/W | echo, echoshow, echospot | Control the bass (value from -6 to 6) | +| shuffle | Switch | R/W | echo, echoshow, echospot, wha | Shuffle play if applicable, e.g. playing a playlist | +| imageUrl | String | R | echo, echoshow, echospot, wha | Url of the album image or radio station logo | +| title | String | R | echo, echoshow, echospot, wha | Title of the current media | +| subtitle1 | String | R | echo, echoshow, echospot, wha | Subtitle of the current media | +| subtitle2 | String | R | echo, echoshow, echospot, wha | Additional subtitle of the current media | +| providerDisplayName | String | R | echo, echoshow, echospot, wha | Name of the music provider | +| bluetoothMAC | String | R/W | echo, echoshow, echospot | Bluetooth device MAC. Used to connect to a specific device or disconnect if an empty string was provided | +| bluetooth | Switch | R/W | echo, echoshow, echospot | Connect/Disconnect to the last used bluetooth device (works after a bluetooth connection was established after the openHAB start) | +| bluetoothDeviceName | String | R | echo, echoshow, echospot | User friendly name of the connected bluetooth device | +| radio | Switch | R/W | echo, echoshow, echospot, wha | Start playing of the last used TuneIn radio station (works after the radio station started after the openHAB start) | +| remind | String | W | echo, echoshow, echospot | Write Only! Speak the reminder and sends a notification to the Alexa app | +| nextReminder | DateTime | R | echo, echoshow, echospot | Next reminder on the device | +| playAlarmSound | String | W | echo, echoshow, echospot | Write Only! Plays an Alarm sound | +| nextAlarm | DateTime | R | echo, echoshow, echospot | Next alarm on the device | +| nextMusicAlarm | DateTime | R | echo, echoshow, echospot | Next music alarm on the device | +| nextTimer | DateTime | R | echo, echoshow, echospot | Next timer on the device | +| startRoutine | String | W | echo, echoshow, echospot | Write Only! Type in what you normally say to Alexa without the preceding "Alexa," | +| musicProviderId | String | R/W | echo, echoshow, echospot | Current Music provider | +| playMusicVoiceCommand | String | W | echo, echoshow, echospot | Write Only! Voice command as text. E.g. 'Yesterday from the Beatles' | +| startCommand | String | W | echo, echoshow, echospot | Write Only! Used to start anything. Available options: Weather, Traffic, GoodMorning, SingASong, TellStory, FlashBriefing and FlashBriefing. (Note: The options are case sensitive) | +| announcement | String | W | echo, echoshow, echospot | Write Only! Display the announcement message on the display. See in the tutorial section to learn how it’s possible to set the title and turn off the sound. | +| textToSpeech | String | W | echo, echoshow, echospot | Write Only! Write some text to this channel and Alexa will speak it. It is possible to use plain text or SSML: e.g. `I want to tell you a secret.I am not a real human.` | +| textToSpeechVolume | Dimmer | R/W | echo, echoshow, echospot | Volume of the textToSpeech channel, if 0 the current volume will be used | +| textCommand | String | W | echo, echoshow, echospot | Write Only! Execute a text command (like a spoken text) | +| lastVoiceCommand | String | R | echo, echoshow, echospot | Last voice command spoken to the device. | +| lastSpokenText | String | R | echo, echoshow, echospot | Last spoken text from the device. (for example statements, answers and text to speeches) | +| mediaProgress | Dimmer | R/W | echo, echoshow, echospot | Media progress in percent | +| mediaProgressTime | Number:Time | R/W | echo, echoshow, echospot | Media play time | +| mediaLength | Number:Time | R | echo, echoshow, echospot | Media length | +| notificationVolume | Dimmer | R | echo, echoshow, echospot | Notification volume | +| ascendingAlarm | Switch | R/W | echo, echoshow, echospot | Ascending alarm up to the configured volume | +| doNotDisturb | Switch | R/W | echo, echoshow, echospot | Do Not Disturb mode enabled | -The `flashbriefingprofile` has no configuration parameters. -It will be configured at runtime by using the save channel to store the current flash briefing configuration in the thing. Create a `flashbriefingprofile` Thing for each set you need. -E.g. One Flashbriefing profile with technical news and wheater, one for playing world news and one for sport news. - -#### `smartHomeDevice` and `smartHomeDeviceGroup` Thing Configuration - -| Configuration name | Description | -|--------------------------|---------------------------------------------------------------------------| -| id | The id of the device or device group | +## Advanced Feature Technically Experienced Users -The only possibility to find out the id is by using the discover function in the UI. You can use then the id, if you want define the Thing in a file. +The url /amazonechocontrol//PROXY/ provides a proxy server with an authenticated connection to the Amazon Alexa server. +This can be used to call Alexa API from rules. -1. Open the url YOUR_OPENHAB/amazonechocontrol in your browser (e.g. `http://openhab:8080/amazonechocontrol/`) -1. Click on the name of the account thing -1. Click on the name of the echo thing -1. Scroll to the channel and copy the required ID +E.g. to read out the history call from an installation on openhab:8080 with an account named account1: -## Channels - -| Channel Type ID | Item Type | Access Mode | Thing Type | Description | -|--------------------------|----------------------|-------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| player | Player | R/W | echo, echoshow, echospot, wha | Control the music player (Supported commands: PLAY or ON, PAUSE or OFF, NEXT, PREVIOUS, REWIND, FASTFORWARD) | -| volume | Dimmer | R/W | echo, echoshow, echospot | Control the volume | -| equalizerTreble | Number | R/W | echo, echoshow, echospot | Control the treble (value from -6 to 6) | -| equalizerMidrange | Number | R/W | echo, echoshow, echospot | Control the midrange (value from -6 to 6) | -| equalizerBass | Number | R/W | echo, echoshow, echospot | Control the bass (value from -6 to 6) | -| shuffle | Switch | R/W | echo, echoshow, echospot, wha | Shuffle play if applicable, e.g. playing a playlist | -| imageUrl | String | R | echo, echoshow, echospot, wha | Url of the album image or radio station logo | -| title | String | R | echo, echoshow, echospot, wha | Title of the current media | -| subtitle1 | String | R | echo, echoshow, echospot, wha | Subtitle of the current media | -| subtitle2 | String | R | echo, echoshow, echospot, wha | Additional subtitle of the current media | -| providerDisplayName | String | R | echo, echoshow, echospot, wha | Name of the music provider | -| bluetoothMAC | String | R/W | echo, echoshow, echospot | Bluetooth device MAC. Used to connect to a specific device or disconnect if an empty string was provided | -| bluetooth | Switch | R/W | echo, echoshow, echospot | Connect/Disconnect to the last used bluetooth device (works after a bluetooth connection was established after the openHAB start) | -| bluetoothDeviceName | String | R | echo, echoshow, echospot | User friendly name of the connected bluetooth device | -| radioStationId | String | R/W | echo, echoshow, echospot, wha | Start playing of a TuneIn radio station by specifying its id or stops playing if an empty string was provided | -| radio | Switch | R/W | echo, echoshow, echospot, wha | Start playing of the last used TuneIn radio station (works after the radio station started after the openHAB start) | -| amazonMusicTrackId | String | R/W | echo, echoshow, echospot, wha | Start playing of an Amazon Music track by its id or stops playing if an empty string was provided | -| amazonMusicPlayListId | String | W | echo, echoshow, echospot, wha | Start playing of an Amazon Music playlist by specifying its id or stops playing if an empty string was provided. | -| amazonMusic | Switch | R/W | echo, echoshow, echospot, wha | Start playing of the last used Amazon Music song (works after at least one song was started after the openHAB start) | -| remind | String | R/W | echo, echoshow, echospot | Speak the reminder and sends a notification to the Alexa app | -| nextReminder | DateTime | R | echo, echoshow, echospot | Next reminder on the device | -| playAlarmSound | String | W | echo, echoshow, echospot | Plays an Alarm sound | -| nextAlarm | DateTime | R | echo, echoshow, echospot | Next alarm on the device | -| nextMusicAlarm | DateTime | R | echo, echoshow, echospot | Next music alarm on the device | -| nextTimer | DateTime | R | echo, echoshow, echospot | Next timer on the device | -| startRoutine | String | W | echo, echoshow, echospot | Type in what you normally say to Alexa without the preceding "Alexa," | -| musicProviderId | String | R/W | echo, echoshow, echospot | Current Music provider | -| playMusicVoiceCommand | String | W | echo, echoshow, echospot | Voice command as text. E.g. 'Yesterday from the Beatles' | -| startCommand | String | W | echo, echoshow, echospot | Used to start anything. Available options: Weather, Traffic, GoodMorning, SingASong, TellStory, FlashBriefing and FlashBriefing.\ (Note: The options are case sensitive) | -| announcement | String | W | echo, echoshow, echospot | Display the announcement message on the display. See in the tutorial section to learn how it’s possible to set the title and turn off the sound. | -| textToSpeech | String | W | echo, echoshow, echospot | Write some plain text to this channel and Alexa will speak it. SSML is also possible: e.g. `I want to tell you a secret.I am not a real human.` | -| textToSpeechVolume | Dimmer | R/W | echo, echoshow, echospot | Volume of the textToSpeech channel, if 0 the current volume will be used | -| textCommand | String | W | echo, echoshow, echospot | Execute a text command (like a spoken text) | -| lastVoiceCommand | String | R/W | echo, echoshow, echospot | Last voice command spoken to the device. Writing to the channel starts voice output. | -| mediaProgress | Dimmer | R/W | echo, echoshow, echospot | Media progress in percent | -| mediaProgressTime | Number:Time | R/W | echo, echoshow, echospot | Media play time | -| mediaLength | Number:Time | R | echo, echoshow, echospot | Media length | -| notificationVolume | Dimmer | R | echo, echoshow, echospot | Notification volume | -| ascendingAlarm | Switch | R/W | echo, echoshow, echospot | Ascending alarm up to the configured volume | -| sendMessage | String | W | account | Sends a message to the Echo devices. | -| save | Switch | W | flashbriefingprofile | Stores the current configuration of flash briefings within the thing | -| active | Switch | R/W | flashbriefingprofile | Active the profile | -| playOnDevice | String | W | flashbriefingprofile | Specify the echo serial number or name to start the flash briefing. | -| powerState | Switch | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows and changes the state (ON/OFF) of your device | -| brightness | Dimmer | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows and changes the brightness of your lamp | -| color | Color | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows the color of your light | -| colorName | String | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows and changes the color name of your light (groups are not able to show their color) | -| colorTemperatureInKelvin | Number:Temperature | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows the color temperature of your light | -| colorTemperatureName | String | R/W | smartHomeDevice, smartHomeDeviceGroup | White temperatures name of your lights (groups are not able to show their color) | -| armState | String | R/W | smartHomeDevice, smartHomeDeviceGroup | State of your alarm guard. Options: ARMED_AWAY, ARMED_STAY, ARMED_NIGHT, DISARMED (groups are not able to show their state) | -| burglaryAlarm | Contact | R | smartHomeDevice | Burglary alarm | -| carbonMonoxideAlarm | Contact | R | smartHomeDevice | Carbon monoxide detection alarm | -| fireAlarm | Contact | R | smartHomeDevice | Fire alarm | -| waterAlarm | Contact | R | smartHomeDevice | Water alarm | -| glassBreakDetectionState | Contact | R | smartHomeDevice | Glass break detection alarm | -| smokeAlarmDetectionState | Contact | R | smartHomeDevice | Smoke detection alarm | -| temperature | Number:Temperature | R | smartHomeDevice | Temperature | -| targetSetpoint | Number:Temperature | R/W | smartHomeDevice | Thermostat target setpoint | -| upperSetpoint | Number:Temperature | R/W | smartHomeDevice | Thermostat upper setpoint (AUTO) | -| lowerSetpoint | Number:Temperature | R/W | smartHomeDevice | Thermostat lower setpoint (AUTO) | -| relativeHumidity | Number:Dimensionless | R | smartHomeDevice | Thermostat humidity | -| thermostatMode | String | R/W | smartHomeDevice | Thermostat operation mode | - -*note* the channels of `smartHomeDevices` and `smartHomeDeviceGroup` will be created dynamically based on the capabilities reported by the amazon server. This can take a little bit of time. -The polling interval configured in the Account Thing to get the state is specified in minutes and has a minimum of 10. This means it takes up to 10 minutes to see the state of a channel. The reason for this low interval is, that the polling causes a big server load for the Smart Home Skills. +http://openhab:8080/amazonechocontrol/account1/PROXY/api/activities?startTime=&size=50&offset=1 -## Full Example +### Example -### echo.things +#### echo.things ```java Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" [discoverSmartHome=2, pollingIntervalSmartHomeAlexa=30, pollingIntervalSmartSkills=120] @@ -229,8 +216,7 @@ Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" [discove Thing smartHomeDevice smartHomeDevice1 "Smart Home Device 1" @ "Living Room" [id="ID"] Thing smartHomeDevice smartHomeDevice2 "Smart Home Device 2" @ "Living Room" [id="ID"] Thing smartHomeDevice smartHomeDevice3 "Smart Home Device 3" @ "Living Room" [id="ID"] - Thing smartHomeDeviceGroup smartHomeDeviceGroup1 "Living Room Group" @ "Living Room" [id="ID"] -} + Thing smartHomeDeviceGroup smartHomeDeviceGroup1 "Living Room Group" @ "Living Room" [id="ID"]} ``` #### echo.items @@ -240,96 +226,63 @@ Take a look in the channel description above to know, which channels are support ```java // Account -String Echo_Living_Room_SendMessage "SendMessage" {channel="amazonechocontrol:account:account1:sendMessage"} +String Echo_Living_Room_SendMessage "SendMessage" {channel="amazonechocontrol:account:account1:sendMessage"} Group Alexa_Living_Room // Player control -Player Echo_Living_Room_Player "Player" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:player"} -Dimmer Echo_Living_Room_Volume "Volume [%.0f %%]" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:volume"} -Number Echo_Living_Room_Treble "Treble" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:equalizerTreble"} -Number Echo_Living_Room_Midrange "Midrange" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:equalizerMidrange"} -Number Echo_Living_Room_Bass "Bass" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:equalizerBass"} -Switch Echo_Living_Room_Shuffle "Shuffle" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:shuffle"} +Player Echo_Living_Room_Player "Player" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:player"} +Dimmer Echo_Living_Room_Volume "Volume [%.0f %%]" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:volume"} +Number Echo_Living_Room_Treble "Treble" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:equalizerTreble"} +Number Echo_Living_Room_Midrange "Midrange" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:equalizerMidrange"} +Number Echo_Living_Room_Bass "Bass" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:equalizerBass"} +Switch Echo_Living_Room_Shuffle "Shuffle" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:shuffle"} // Media channels -Dimmer Echo_Living_Room_MediaProgress "Media progress" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:mediaProgress"} -Number:Time Echo_Living_Room_MediaProgressTime "Media progress time [%d %unit%]" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:mediaProgressTime"} -Number:Time Echo_Living_Room_MediaLength "Media length [%d %unit%]" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:mediaLength"} +Dimmer Echo_Living_Room_MediaProgress "Media progress" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:mediaProgress"} +Number:Time Echo_Living_Room_MediaProgressTime "Media progress time [%d %unit%]" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:mediaProgressTime"} +Number:Time Echo_Living_Room_MediaLength "Media length [%d %unit%]" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:mediaLength"} // Player Information -String Echo_Living_Room_ImageUrl "Image URL" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:imageUrl"} -String Echo_Living_Room_Title "Title" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:title"} -String Echo_Living_Room_Subtitle1 "Subtitle 1" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:subtitle1"} -String Echo_Living_Room_Subtitle2 "Subtitle 2" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:subtitle2"} -String Echo_Living_Room_ProviderDisplayName "Provider" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:providerDisplayName"} +String Echo_Living_Room_ImageUrl "Image URL" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:imageUrl"} +String Echo_Living_Room_Title "Title" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:title"} +String Echo_Living_Room_Subtitle1 "Subtitle 1" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:subtitle1"} +String Echo_Living_Room_Subtitle2 "Subtitle 2" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:subtitle2"} +String Echo_Living_Room_ProviderDisplayName "Provider" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:providerDisplayName"} // Music provider and start command -String Echo_Living_Room_MusicProviderId "Music Provider Id" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:musicProviderId"} -String Echo_Living_Room_PlayMusicCommand "Play music voice command (Write Only)" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playMusicVoiceCommand"} -String Echo_Living_Room_StartCommand "Start Information" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:startCommand"} - -// TuneIn Radio -String Echo_Living_Room_RadioStationId "TuneIn Radio Station Id" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:radioStationId"} -Switch Echo_Living_Room_Radio "TuneIn Radio" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:radio"} - -// Amazon Music -String Echo_Living_Room_AmazonMusicTrackId "Amazon Music Track Id" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusicTrackId"} -String Echo_Living_Room_AmazonMusicPlayListId "Amazon Music Playlist Id" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusicPlayListId"} -Switch Echo_Living_Room_AmazonMusic "Amazon Music" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusic"} +String Echo_Living_Room_MusicProviderId "Music Provider Id" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:musicProviderId"} +String Echo_Living_Room_PlayMusicCommand "Play music voice command (Write Only)" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playMusicVoiceCommand"} +String Echo_Living_Room_StartCommand "Start Information" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:startCommand"} // Bluetooth -String Echo_Living_Room_BluetoothMAC "Bluetooth MAC Address" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothMAC"} -Switch Echo_Living_Room_Bluetooth "Bluetooth" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetooth"} -String Echo_Living_Room_BluetoothDeviceName "Bluetooth Device" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothDeviceName"} +String Echo_Living_Room_BluetoothMAC "Bluetooth MAC Address" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothMAC"} +Switch Echo_Living_Room_Bluetooth "Bluetooth" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetooth"} +String Echo_Living_Room_BluetoothDeviceName "Bluetooth Device" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothDeviceName"} // Commands -String Echo_Living_Room_Announcement "Announcement" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:announcement"} -String Echo_Living_Room_TTS "Text to Speech" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:textToSpeech"} -Dimmer Echo_Living_Room_TTS_Volume "Text to Speech Volume" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:textToSpeechVolume"} -String Echo_Living_Room_Remind "Remind" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:remind"} -String Echo_Living_Room_PlayAlarmSound "Play Alarm Sound" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playAlarmSound"} -String Echo_Living_Room_StartRoutine "Start Routine" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:startRoutine"} -Dimmer Echo_Living_Room_NotificationVolume "Notification volume" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:notificationVolume"} -Switch Echo_Living_Room_AscendingAlarm "Ascending alarm" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:ascendingAlarm"} +String Echo_Living_Room_Announcement "Announcement" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:announcement"} +String Echo_Living_Room_TTS "Text to Speech" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:textToSpeech"} +Dimmer Echo_Living_Room_TTS_Volume "Text to Speech Volume" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:textToSpeechVolume"} +String Echo_Living_Room_Remind "Remind" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:remind"} +String Echo_Living_Room_PlayAlarmSound "Play Alarm Sound" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playAlarmSound"} +String Echo_Living_Room_StartRoutine "Start Routine" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:startRoutine"} +Dimmer Echo_Living_Room_NotificationVolume "Notification volume" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:notificationVolume"} +Switch Echo_Living_Room_AscendingAlarm "Ascending alarm" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:ascendingAlarm"} // Feedbacks -String Echo_Living_Room_LastVoiceCommand "Last voice command" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:lastVoiceCommand"} -DateTime Echo_Living_Room_NextReminder "Next reminder" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextReminder"} -DateTime Echo_Living_Room_NextAlarm "Next alarm" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextAlarm"} -DateTime Echo_Living_Room_NextMusicAlarm "Next music alarm" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextMusicAlarm"} -DateTime Echo_Living_Room_NextTimer "Next timer" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextTimer"} - -// Flashbriefings -Switch FlashBriefing_Technical_Save "Save (Write only)" {channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:save"} -Switch FlashBriefing_Technical_Active "Active" {channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:active"} -String FlashBriefing_Technical_Play "Play (Write only)" {channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:playOnDevice"} - -Switch FlashBriefing_LifeStyle_Save "Save (Write only)" {channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing2:save"} -Switch FlashBriefing_LifeStyle_Active "Active" {channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing2:active"} -String FlashBriefing_LifeStyle_Play "Play (Write only)" {channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing2:playOnDevice"} - -// Lights and lightgroups -Switch Light_State "On/Off" {channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:powerState"} -Dimmer Light_Brightness "Brightness" {channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:brightness"} -Color Light_Color "Color" {channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:color"} -String Light_Color_Name "Color Name" {channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:colorName"} -String Light_White "White temperature" {channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:colorTemperatureName"} - -// Smart plugs -Switch Plug_State "On/Off" {channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice2:powerState"} - -// Alexa Guard -Switch Arm_State "State" {channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice3:armState"} - -// Smart Home device group -Switch Group_State "On/Off" {channel="amazonechocontrol:smartHomeDeviceGroup:account1:smartHomeDeviceGroup1:powerState"} +String Echo_Living_Room_LastVoiceCommand "Last voice command" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:lastVoiceCommand"} +String Echo_Living_Room_LastSpokenText "Last spoken text" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:lastSpokenText"} +DateTime Echo_Living_Room_NextReminder "Next reminder" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextReminder"} +DateTime Echo_Living_Room_NextAlarm "Next alarm" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextAlarm"} +DateTime Echo_Living_Room_NextMusicAlarm "Next music alarm" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextMusicAlarm"} +DateTime Echo_Living_Room_NextTimer "Next timer" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:nextTimer"} ``` #### echo.sitemap -```perl -sitemap amazonechocontrol label="Amazone Devices" +```java +sitemap amazonechocontrol label="Echo Devices" { Frame label="Alexa" { Default item=Echo_Living_Room_Player @@ -355,14 +308,6 @@ sitemap amazonechocontrol label="Amazone Devices" // To start one of your flashbriefings use Flashbriefing. Selection item=Echo_Living_Room_StartCommand mappings=[ 'Weather'='Weather', 'Traffic'='Traffic', 'GoodMorning'='Good Morning', 'SingASong'='Song', 'TellStory'='Story', 'FlashBriefing'='Flash Briefing', 'FlashBriefing.flashbriefing1'='Technical', 'FlashBriefing.flashbriefing2'='Life Style' ] - Selection item=Echo_Living_Room_RadioStationId mappings=[ ''='Off', 's1139'='Antenne Steiermark', 's8007'='Hitradio Ö3', 's16793'='Radio 10', 's8235'='FM4' ] - Text item=Echo_Living_Room_RadioStationId - Switch item=Echo_Living_Room_Radio - - Text item=Echo_Living_Room_AmazonMusicTrackId - Text item=Echo_Living_Room_AmazonMusicPlayListId - Switch item=Echo_Living_Room_AmazonMusic - Text item=Echo_Living_Room_BluetoothMAC // Change the Place holder with the MAC address shown, if Alexa is connected to the device Selection item=Echo_Living_Room_BluetoothMAC mappings=[ ''='Disconnected', ''='Bluetooth Device 1', ''='Bluetooth Device 2'] @@ -373,10 +318,78 @@ sitemap amazonechocontrol label="Amazone Devices" Switch item=Echo_Living_Room_Bluetooth Text item=Echo_Living_Room_BluetoothDeviceName Text item=Echo_Living_Room_LastVoiceCommand + Text item=Echo_Living_Room_LastSpokenText Slider item=Echo_Living_Room_NotificationVolume Switch item=Echo_Living_Room_AscendingAlarm } +} +``` + +### `flashbriefingprofile` Thing Configuration + +The `flashbriefingprofile` has no configuration parameters. +It will be configured at runtime by using the save channel to store the current flash briefing configuration in the thing. Create a `flashbriefingprofile` Thing for each set you need. +E.g. One Flashbriefing profile with technical news and wheater, one for playing world news and one for sport news. + +Flash briefings are sets of information that can be configured in your Alexa app. +The app only allows to have one flash-briefing configuration at the same time (e.g. weather and news). +The `flashbriefingprofile` thing helps you to overcome this limitation. + +To set it up using managed (UI) configuration: + +1. Use the app to create the flash-briefing configuration you want. +2. Start the discovery, you should see a new "flashbriefingprofile" thing. If this is not the case, a thing with the current configuration already exists. +3. Add that thing (you can use a custom name if you want). +4. Repeat steps 1-3 for other configurations (you can add as many as you want, make sure they are different). + +Textual configuration (untested, not recommended): + +1. Add a new `flashbriefiungprofilething` to your `.things` file. +2. Use the app to create the flash-briefing configuration you want. +3. Send `ON` to the `save` channel of the thing you created in step 1. +4. Repeat steps 1-3 for other configurations (you can add as many as you want, make sure they are different). + +#### Channels + +| Channel Type ID | Item Type | Access Mode | Description | +|-----------------|-----------|:-----------:|-----------------------------------------------------------------------------------------------------------------------------------------------| +| `save` | Switch | W | Write Only! Stores the current configuration of flash briefings. | +| `active` | Switch | R/W | Activates this flash briefing as default ON ALL DEVICES. | +| `playOnDevice` | String | W | Specify the echo serial number or name to start the flash-briefing. This is only exceuted once and the default configuration does not change. | + +**Attention:** Be careful when using the `save` channel. +Storing the same configuration to several things may result in unpredictable behavior. + +### Example + +#### flashbriefings.things + +```java +Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" [discoverSmartHome=2] +{ + Thing flashbriefingprofile flashbriefing1 "Flash Briefing Technical" @ "Flash Briefings" + Thing flashbriefingprofile flashbriefing2 "Flash Briefing Life Style" @ "Flash Briefings" +} +``` + +#### flashbriefings.items + +```java +// Flashbriefings +Switch FlashBriefing_Technical_Save "Save (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:save"} +Switch FlashBriefing_Technical_Active "Active" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:active"} +String FlashBriefing_Technical_Play "Play (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:playOnDevice"} +Switch FlashBriefing_LifeStyle_Save "Save (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing2:save"} +Switch FlashBriefing_LifeStyle_Active "Active" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing2:active"} +String FlashBriefing_LifeStyle_Play "Play (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing2:playOnDevice"} +``` + +#### flashbriefings.sitemap + +```java +sitemap flashbriefings label="Flash Briefings" +{ Frame label="Flash Briefing Technical" { Switch item=FlashBriefing_Technical_Save Switch item=FlashBriefing_Technical_Active @@ -388,7 +401,106 @@ sitemap amazonechocontrol label="Amazone Devices" Switch item=FlashBriefing_LifeStyle_Active Text item=FlashBriefing_LifeStyle_Play } +} +``` + +### `smartHomeDevice` and `smartHomeDeviceGroup` Thing Configuration + +| Configuration name | Description | +|--------------------------|---------------------------------------------------------------------------| +| id | The id of the device or device group | + +The only possibility to find out the id is using discovery function in the UI. +You can use that id if you want to define the thing in a file. + +1. Open the url YOUR_OPENHAB/amazonechocontrol in your browser (e.g. `http://openhab:8080/amazonechocontrol/`) +1. Click on the name of the account thing +1. Click on the name of the echo thing +1. Scroll to the channel and copy the required ID + +Discovered smart home devices show a `deviceIdentifierList` in their thing properties, containing one or more serial numbers. +You can check if any of these serial numbers is associated with another device and use this to identify devices with similar/same names. + +### Channels + +The channels of the smarthome devices will be generated at runtime. +Check in the UI thing configurations, which channels are created. + +| Channel ID | Item Type | Access Mode | Thing Type | Description | +|--------------------------|----------------------|-------------|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------| +| powerState | Switch | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows and changes the state (ON/OFF) of your device | +| brightness | Dimmer | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows and changes the brightness of your lamp | +| color | Color | R/(W) | smartHomeDevice, smartHomeDeviceGroup | Shows the color of your light | +| colorName | String | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows and changes the color name of your light (groups are not able to show their color) | +| colorTemperatureInKelvin | Number:Temperature | R/W | smartHomeDevice, smartHomeDeviceGroup | Shows the color temperature of your light | +| colorTemperatureName | String | R/W | smartHomeDevice, smartHomeDeviceGroup | White temperatures name of your lights (groups are not able to show their color) | +| armState | String | R/W | smartHomeDevice, smartHomeDeviceGroup | State of your alarm guard. Options: ARMED_AWAY, ARMED_STAY, ARMED_NIGHT, DISARMED (groups are not able to show their state) | +| burglaryAlarm | Contact | R | smartHomeDevice | Burglary alarm | +| carbonMonoxideAlarm | Contact | R | smartHomeDevice | Carbon monoxide detection alarm | +| fireAlarm | Contact | R | smartHomeDevice | Fire alarm | +| waterAlarm | Contact | R | smartHomeDevice | Water alarm | +| glassBreakDetectionState | Contact | R | smartHomeDevice | Glass break detection alarm | +| smokeAlarmDetectionState | Contact | R | smartHomeDevice | Smoke detection alarm | +| temperature | Number | R | smartHomeDevice | Temperature | +| targetSetpoint | Number:Temperature | R/W | smartHomeDevice | Thermostat Setpoint | +| upperSetpoint | Number:Temperature | R/W | smartHomeDevice | Thermostat Upper Setpoint | +| lowerSetpoint | Number:Temperature | R/W | smartHomeDevice | Thermostat Lower Setpoint | +| relativeHumidity | Number:Dimensionless | R | smartHomeDevice | Thermostat humidity | +| thermostatMode | String | R/W | smartHomeDevice | Thermostat Mode (`AUTO`, `COOL`, `HEAT`, `OFF`, `ECO`) | +| motionDetected | Switch | R | smartHomeDevice | A motion was detected if ON | +| contact | Contact | R | smartHomeDevice | A contact sensor OPEN if detected, CLOSED if NOT_DETECTED | +| geoLocation | Location | R | smartHomeDevice | The location (e.g. of a Tile) | + +*Note* the channels of `smartHomeDevices` and `smartHomeDeviceGroup` will be created dynamically based on the capabilities reported by the Amazon server. This can take a little bit of time. +The polling interval configured in the Account Thing to get the state is specified in minutes and has a minimum of 10. This means it takes up to 10 minutes to see the state of a channel. The reason for this low interval is, that the polling causes a big server load for the Smart Home Skills. + +*Note*: The `color` channel is read-only by default because Alexa does only support setting colors by their name. +It has a configuration parameter `matchColors` which enables writing to that channel and tries to find the closes available color when sending a command to Alexa. + +### Example +#### smarthome.things + +```java +Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" [discoverSmartHome=2, pollingIntervalSmartHomeAlexa=30, pollingIntervalSmartSkills=120] +{ + Thing smartHomeDevice smartHomeDevice1 "Smart Home Device 1" @ "Living Room" [id="ID"] + Thing smartHomeDevice smartHomeDevice2 "Smart Home Device 2" @ "Living Room" [id="ID"] + Thing smartHomeDevice smartHomeDevice3 "Smart Home Device 3" @ "Living Room" [id="ID"] + Thing smartHomeDeviceGroup smartHomeDeviceGroup1 "Living Room Group" @ "Living Room" [id="ID"] +} +``` + +#### smarthome.items + +Sample for the Thing echo1 only. But it will work in the same way for the other things, only replace the thing name in the channel link. +Take a look in the channel description above to know which channels are supported by your thing type. + +```java +// Lights and lightgroups +Switch Light_State "On/Off" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:powerState" } +Dimmer Light_Brightness "Brightness" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:brightness" } +Color Light_Color "Color" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:color" } +String Light_Color_Name "Color Name" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:colorName" } +String Light_White "White temperature" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice1:colorTemperatureName" } + +// Smart plugs +Switch Plug_State "On/Off" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice2:powerState" } + +// Alexa Guard +Switch Arm_State "State" { channel="amazonechocontrol:smartHomeDevice:account1:smartHomeDevice3:armState" } + +// Smart Home device group +Switch Group_State "On/Off" { channel="amazonechocontrol:smartHomeDeviceGroup:account1:smartHomeDeviceGroup1:powerState" } +``` + +The only possibility to find out the id for the smartHomeDevice and smartHomeDeviceGroup Things is by using the discover function. + +#### smarthome.sitemap + +```java +sitemap smarthome label="Smart Home Devices" +{ Frame label="Lights and light groups" { Switch item=Light_State Slider item=Light_Brightness @@ -402,6 +514,17 @@ sitemap amazonechocontrol label="Amazone Devices" } ``` +#### Rule for calculating the distance of a Tile from your home + +Link the `geoLocation` channel of the Tile thing to a `Location` item named `CarLocation`. +Add a second item of type `Number:Length` with the name `CarDistance` (adjust state description to your needs, e.g. miles or km as unit). +Create a rule that triggers on change of that item with the DSL script as action: + +``` +var homeLocation = new PointType("50.273448, 8.409950") +CarDistance.postUpdate(homeLocation.distanceFrom(CarLocation.state as PointType).toString + " m") +``` + ## Advanced Feature Technically Experienced Users The url /amazonechocontrol//PROXY/ provides a proxy server with an authenticated connection to the Amazon Alexa server. @@ -409,7 +532,7 @@ This can be used to call Alexa API from rules. E.g. to read out the history call from an installation on openhab:8080 with an account named account1: -`http://openhab:8080/amazonechocontrol/account1/PROXY/api/activities?startTime=&size=50&offset=1` +http://openhab:8080/amazonechocontrol/account1/PROXY/api/activities?startTime=&size=50&offset=1 To resolve login problems the connection settings of an `account` thing can be reset via the karaf console. The command `amazonechocontrol listAccounts` shows a list of all available `account` things. @@ -462,7 +585,7 @@ Expert: You can use a json formatted string to control title, sound and volume: ```json -{"sound": true, "speak":"", "title": "", "body": "<Body Text>", "volume": 20} +{ "sound": true, "speak":"<Speak>", "title": "<Title>", "body": "<Body Text>", "volume": 20} ``` The combination of `sound=true` and `speak` in SSML syntax is not allowed. @@ -473,20 +596,21 @@ No specification uses the volume from the `textToSpeechVolume` channel. Note: If you turn off the sound and Alexa is playing music, it will anyway turn down the volume for a moment. This behavior can not be changed. + ```java rule "Say welcome if the door opens" when Item Door_Contact changed to OPEN then - Echo_Living_Room_Announcement.sendCommand('{"sound": false, "title": "Doorstep", "body": "Door opened"}') + Echo_Living_Room_Announcement.sendCommand('{ "sound": false, "title": "Doorstep", "body": "Door opened"}') end ``` ## Playing an alarm sound for 15 seconds with an openHAB rule if a door contact was opened -1. Do get the ID of your sound, follow the steps in "How To Get IDs" -1. Write down the text in the square brackets. e.g. ECHO:system_alerts_repetitive01 for the nightstand sound -1. Create a rule for start playing the sound: +1) Do get the ID of your sound, follow the steps in "How To Get IDs" +2) Write down the text in the square brackets. e.g. ECHO:system_alerts_repetitive01 for the nightstand sound +3) Create a rule for start playing the sound: ```java var Timer stopAlarmTimer = null @@ -514,9 +638,9 @@ Note 2: The rule have no effect for your default alarm sound used in the Alexa a ### Play a spotify playlist if a switch was changed to on -1. Do get the ID of your sound, follow the steps in "How To Get IDs" -1. Write down the text in the square brackets. e.g. SPOTIFY for the spotify music provider -1. Create a rule for start playing a song or playlist: +1) Do get the ID of your sound, follow the steps in "How To Get IDs" +2) Write down the text in the square brackets. e.g. SPOTIFY for the spotify music provider +3) Create a rule for start playing a song or playlist: ```java rule "Play a playlist on spotify if a switch was changed" @@ -532,8 +656,8 @@ Note: It is recommended to test the command send to play music command first wit ### Start playing weather/traffic/etc -1. Pick up one of the available commands: Weather, Traffic, GoodMorning, SingASong, TellStory, FlashBriefing -1. Create a rule for start playing the information where you provide the command as string: +1) Pick up one of the available commands: Weather, Traffic, GoodMorning, SingASong, TellStory, FlashBriefing +2) Create a rule for start playing the information where you provide the command as string: ```java rule "Start wheater info" @@ -546,9 +670,9 @@ end ### Start playing a custom flashbriefing on a device -1. Do get the ID of your sound, follow the steps in "How To Get IDs" -1. Write down the text in the square brackets. e.g. flashbriefing.flashbriefing1 -1. Create a rule for start playing the information where you provide the command as string: +1) Do get the ID of your sound, follow the steps in "How To Get IDs" +2) Write down the text in the square brackets. e.g. flashbriefing.flashbriefing1 +2) Create a rule for start playing the information where you provide the command as string: ```java rule "Start wheater info" @@ -571,7 +695,8 @@ The binding is tested with amazon.de, amazon.fr, amazon.it, amazon.com and amazo The idea for writing this binding came from this blog: [https://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html](https://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html) (German). Thank you Alex! -The technical information for the web socket connection to get live Alexa state updates cames from Ingo. He has done the Alexa ioBroker implementation [https://github.com/Apollon77](https://github.com/Apollon77) +The technical information for the web socket connection to get live Alexa state updates cames from Ingo. +He has done the Alexa ioBroker implementation https://github.com/Apollon77 Thank you Ingo! ## Trademark Disclaimer diff --git a/bundles/org.openhab.binding.amazonechocontrol/pom.xml b/bundles/org.openhab.binding.amazonechocontrol/pom.xml index 6f837b62eb32d..62df51e221a16 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/pom.xml +++ b/bundles/org.openhab.binding.amazonechocontrol/pom.xml @@ -21,6 +21,12 @@ <version>1.1.6.RELEASE</version> <scope>compile</scope> </dependency> + <dependency> + <groupId>org.apache.velocity</groupId> + <artifactId>velocity-engine-core</artifactId> + <version>2.3</version> + <scope>provided</scope> + </dependency> </dependencies> </project> diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/feature/feature.xml b/bundles/org.openhab.binding.amazonechocontrol/src/main/feature/feature.xml index 8b54251981d87..3ce96a3bbb6ac 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/feature/feature.xml @@ -4,6 +4,7 @@ <feature name="openhab-binding-amazonechocontrol" description="Amazon Echo Control Binding" version="${project.version}"> <feature>openhab-runtime-base</feature> + <bundle dependency="true">mvn:org.apache.velocity/velocity-engine-core/2.3</bundle> <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.amazonechocontrol/${project.version}</bundle> </feature> </features> diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java index 802cf70800e33..0160e004093e0 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java @@ -25,4 +25,5 @@ public class AccountHandlerConfig { public int discoverSmartHome = 0; public int pollingIntervalSmartHomeAlexa = 60; public int pollingIntervalSmartSkills = 120; + public int activityRequestDelay = 10; } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java deleted file mode 100644 index d42dabb2713b7..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java +++ /dev/null @@ -1,719 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal; - -import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import javax.net.ssl.HttpsURLConnection; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.PairedDevice; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists.PlayList; -import org.openhab.core.thing.Thing; -import org.osgi.service.http.HttpService; -import org.osgi.service.http.NamespaceException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.unbescape.html.HtmlEscape; - -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -/** - * Provides the following functions - * --- Login --- - * Simple http proxy to forward the login dialog from amazon to the user through the binding - * so the user can enter a captcha or other extended login information - * --- List of devices --- - * Used to get the device information of new devices which are currently not known - * --- List of IDs --- - * Simple possibility for a user to get the ids needed for writing rules - * - * @author Michael Geramb - Initial Contribution - */ -@NonNullByDefault -public class AccountServlet extends HttpServlet { - - private static final long serialVersionUID = -1453738923337413163L; - private static final String FORWARD_URI_PART = "/FORWARD/"; - private static final String PROXY_URI_PART = "/PROXY/"; - - private final Logger logger = LoggerFactory.getLogger(AccountServlet.class); - - private final HttpService httpService; - private final String servletUrlWithoutRoot; - private final String servletUrl; - private final AccountHandler account; - private final String id; - private @Nullable Connection connectionToInitialize; - private final Gson gson; - - public AccountServlet(HttpService httpService, String id, AccountHandler account, Gson gson) { - this.httpService = httpService; - this.account = account; - this.id = id; - this.gson = gson; - - try { - servletUrlWithoutRoot = "amazonechocontrol/" + URLEncoder.encode(id, StandardCharsets.UTF_8); - servletUrl = "/" + servletUrlWithoutRoot; - - Hashtable<Object, Object> initParams = new Hashtable<>(); - initParams.put("servlet-name", servletUrl); - - httpService.registerServlet(servletUrl, this, initParams, httpService.createDefaultHttpContext()); - } catch (NamespaceException | ServletException e) { - throw new IllegalStateException(e.getMessage()); - } - } - - private Connection reCreateConnection() { - Connection oldConnection = connectionToInitialize; - if (oldConnection == null) { - oldConnection = account.findConnection(); - } - return new Connection(oldConnection, this.gson); - } - - public void dispose() { - httpService.unregister(servletUrl); - } - - @Override - protected void doPut(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) - throws ServletException, IOException { - doVerb("PUT", req, resp); - } - - @Override - protected void doDelete(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) - throws ServletException, IOException { - doVerb("DELETE", req, resp); - } - - @Override - protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) - throws ServletException, IOException { - doVerb("POST", req, resp); - } - - void doVerb(String verb, @Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException { - if (req == null) { - return; - } - if (resp == null) { - return; - } - String requestUri = req.getRequestURI(); - if (requestUri == null) { - return; - } - String baseUrl = requestUri.substring(servletUrl.length()); - String uri = baseUrl; - String queryString = req.getQueryString(); - if (queryString != null && queryString.length() > 0) { - uri += "?" + queryString; - } - - Connection connection = this.account.findConnection(); - if (connection != null && "/changedomain".equals(uri)) { - Map<String, String[]> map = req.getParameterMap(); - String[] domainArray = map.get("domain"); - if (domainArray == null) { - logger.warn("Could not determine domain"); - return; - } - String domain = domainArray[0]; - String loginData = connection.serializeLoginData(); - Connection newConnection = new Connection(null, this.gson); - if (newConnection.tryRestoreLogin(loginData, domain)) { - account.setConnection(newConnection); - } - resp.sendRedirect(servletUrl); - return; - } - if (uri.startsWith(PROXY_URI_PART)) { - // handle proxy request - - if (connection == null) { - returnError(resp, "Account not online"); - return; - } - String getUrl = "https://alexa." + connection.getAmazonSite() + "/" - + uri.substring(PROXY_URI_PART.length()); - - String postData = null; - if ("POST".equals(verb) || "PUT".equals(verb)) { - postData = req.getReader().lines().collect(Collectors.joining(System.lineSeparator())); - } - - this.handleProxyRequest(connection, resp, verb, getUrl, null, postData, true, connection.getAmazonSite()); - return; - } - - // handle post of login page - connection = this.connectionToInitialize; - if (connection == null) { - returnError(resp, "Connection not in initialize mode."); - return; - } - - resp.addHeader("content-type", "text/html;charset=UTF-8"); - - Map<String, String[]> map = req.getParameterMap(); - StringBuilder postDataBuilder = new StringBuilder(); - for (String name : map.keySet()) { - if (postDataBuilder.length() > 0) { - postDataBuilder.append('&'); - } - - postDataBuilder.append(name); - postDataBuilder.append('='); - String value = ""; - if ("failedSignInCount".equals(name)) { - value = "ape:AA=="; - } else { - String[] strings = map.get(name); - if (strings != null && strings.length > 0 && strings[0] != null) { - value = strings[0]; - } - } - postDataBuilder.append(URLEncoder.encode(value, StandardCharsets.UTF_8.name())); - } - - uri = req.getRequestURI(); - if (uri == null || !uri.startsWith(servletUrl)) { - returnError(resp, "Invalid request uri '" + uri + "'"); - return; - } - String relativeUrl = uri.substring(servletUrl.length()).replace(FORWARD_URI_PART, "/"); - - String site = connection.getAmazonSite(); - if (relativeUrl.startsWith("/ap/signin")) { - site = "amazon.com"; - } - String postUrl = "https://www." + site + relativeUrl; - queryString = req.getQueryString(); - if (queryString != null && queryString.length() > 0) { - postUrl += "?" + queryString; - } - String referer = "https://www." + site; - String postData = postDataBuilder.toString(); - handleProxyRequest(connection, resp, "POST", postUrl, referer, postData, false, site); - } - - @Override - protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException { - if (req == null) { - return; - } - if (resp == null) { - return; - } - String requestUri = req.getRequestURI(); - if (requestUri == null) { - return; - } - String baseUrl = requestUri.substring(servletUrl.length()); - String uri = baseUrl; - String queryString = req.getQueryString(); - if (queryString != null && queryString.length() > 0) { - uri += "?" + queryString; - } - logger.debug("doGet {}", uri); - try { - Connection connection = this.connectionToInitialize; - if (uri.startsWith(FORWARD_URI_PART) && connection != null) { - String getUrl = "https://www." + connection.getAmazonSite() + "/" - + uri.substring(FORWARD_URI_PART.length()); - - this.handleProxyRequest(connection, resp, "GET", getUrl, null, null, false, connection.getAmazonSite()); - return; - } - - connection = this.account.findConnection(); - if (uri.startsWith(PROXY_URI_PART)) { - // handle proxy request - - if (connection == null) { - returnError(resp, "Account not online"); - return; - } - String getUrl = "https://alexa." + connection.getAmazonSite() + "/" - + uri.substring(PROXY_URI_PART.length()); - - this.handleProxyRequest(connection, resp, "GET", getUrl, null, null, false, connection.getAmazonSite()); - return; - } - - if (connection != null && connection.verifyLogin()) { - // handle commands - if ("/logout".equals(baseUrl) || "/logout/".equals(baseUrl)) { - this.connectionToInitialize = reCreateConnection(); - this.account.setConnection(null); - resp.sendRedirect(this.servletUrl); - return; - } - // handle commands - if ("/newdevice".equals(baseUrl) || "/newdevice/".equals(baseUrl)) { - this.connectionToInitialize = new Connection(null, this.gson); - this.account.setConnection(null); - resp.sendRedirect(this.servletUrl); - return; - } - - if ("/devices".equals(baseUrl) || "/devices/".equals(baseUrl)) { - handleDevices(resp, connection); - return; - } - if ("/changeDomain".equals(baseUrl) || "/changeDomain/".equals(baseUrl)) { - handleChangeDomain(resp, connection); - return; - } - if ("/ids".equals(baseUrl) || "/ids/".equals(baseUrl)) { - String serialNumber = getQueryMap(queryString).get("serialNumber"); - Device device = account.findDeviceJson(serialNumber); - if (device != null) { - Thing thing = account.findThingBySerialNumber(device.serialNumber); - handleIds(resp, connection, device, thing); - return; - } - } - // return hint that everything is ok - handleDefaultPageResult(resp, "The Account is logged in.", connection); - return; - } - connection = this.connectionToInitialize; - if (connection == null) { - connection = this.reCreateConnection(); - this.connectionToInitialize = connection; - } - - if (!"/".equals(uri)) { - String newUri = req.getServletPath() + "/"; - resp.sendRedirect(newUri); - return; - } - - String html = connection.getLoginPage(); - returnHtml(connection, resp, html, "amazon.com"); - } catch (URISyntaxException | InterruptedException e) { - logger.warn("get failed with uri syntax error", e); - } - } - - public Map<String, String> getQueryMap(@Nullable String query) { - Map<String, String> map = new HashMap<>(); - if (query != null) { - String[] params = query.split("&"); - for (String param : params) { - String[] elements = param.split("="); - if (elements.length == 2) { - String name = elements[0]; - String value = URLDecoder.decode(elements[1], StandardCharsets.UTF_8); - map.put(name, value); - } - } - } - return map; - } - - private void handleChangeDomain(HttpServletResponse resp, Connection connection) { - StringBuilder html = createPageStart("Change Domain"); - html.append("<form action='"); - html.append(servletUrl); - html.append("/changedomain' method='post'>\nDomain:\n<input type='text' name='domain' value='"); - html.append(connection.getAmazonSite()); - html.append("'>\n<br>\n<input type=\"submit\" value=\"Submit\">\n</form>"); - - createPageEndAndSent(resp, html); - } - - private void handleDefaultPageResult(HttpServletResponse resp, String message, Connection connection) - throws IOException { - StringBuilder html = createPageStart(""); - html.append(HtmlEscape.escapeHtml4(message)); - // logout link - html.append(" <a href='" + servletUrl + "/logout' >"); - html.append(HtmlEscape.escapeHtml4("Logout")); - html.append("</a>"); - // newdevice link - html.append(" | <a href='" + servletUrl + "/newdevice' >"); - html.append(HtmlEscape.escapeHtml4("Logout and create new device id")); - html.append("</a>"); - // customer id - html.append("<br>Customer Id: "); - html.append(HtmlEscape.escapeHtml4(connection.getCustomerId())); - // customer name - html.append("<br>Customer Name: "); - html.append(HtmlEscape.escapeHtml4(connection.getCustomerName())); - // device name - html.append("<br>App name: "); - html.append(HtmlEscape.escapeHtml4(connection.getDeviceName())); - // connection - html.append("<br>Connected to: "); - html.append(HtmlEscape.escapeHtml4(connection.getAlexaServer())); - // domain - html.append(" <a href='"); - html.append(servletUrl); - html.append("/changeDomain'>Change</a>"); - - // Main UI link - html.append("<br><a href='/#!/settings/things/" + BINDING_ID + ":" - + URLEncoder.encode(THING_TYPE_ACCOUNT.getId(), "UTF8") + ":" + URLEncoder.encode(id, "UTF8") + "'>"); - html.append(HtmlEscape.escapeHtml4("Check Thing in Main UI")); - html.append("</a><br><br>"); - - // device list - html.append( - "<table><tr><th align='left'>Device</th><th align='left'>Serial Number</th><th align='left'>State</th><th align='left'>Thing</th><th align='left'>Family</th><th align='left'>Type</th><th align='left'>Customer Id</th></tr>"); - for (Device device : this.account.getLastKnownDevices()) { - - html.append("<tr><td>"); - html.append(HtmlEscape.escapeHtml4(nullReplacement(device.accountName))); - html.append("</td><td>"); - html.append(HtmlEscape.escapeHtml4(nullReplacement(device.serialNumber))); - html.append("</td><td>"); - html.append(HtmlEscape.escapeHtml4(device.online ? "Online" : "Offline")); - html.append("</td><td>"); - Thing accountHandler = account.findThingBySerialNumber(device.serialNumber); - if (accountHandler != null) { - html.append("<a href='" + servletUrl + "/ids/?serialNumber=" - + URLEncoder.encode(device.serialNumber, "UTF8") + "'>" - + HtmlEscape.escapeHtml4(accountHandler.getLabel()) + "</a>"); - } else { - html.append("<a href='" + servletUrl + "/ids/?serialNumber=" - + URLEncoder.encode(device.serialNumber, "UTF8") + "'>" + HtmlEscape.escapeHtml4("Not defined") - + "</a>"); - } - html.append("</td><td>"); - html.append(HtmlEscape.escapeHtml4(nullReplacement(device.deviceFamily))); - html.append("</td><td>"); - html.append(HtmlEscape.escapeHtml4(nullReplacement(device.deviceType))); - html.append("</td><td>"); - html.append(HtmlEscape.escapeHtml4(nullReplacement(device.deviceOwnerCustomerId))); - html.append("</td>"); - html.append("</tr>"); - } - html.append("</table>"); - createPageEndAndSent(resp, html); - } - - private void handleDevices(HttpServletResponse resp, Connection connection) - throws IOException, URISyntaxException, InterruptedException { - returnHtml(connection, resp, "<html>" + HtmlEscape.escapeHtml4(connection.getDeviceListJson()) + "</html>"); - } - - private String nullReplacement(@Nullable String text) { - if (text == null) { - return "<unknown>"; - } - return text; - } - - StringBuilder createPageStart(String title) { - StringBuilder html = new StringBuilder(); - html.append("<html><head><title>" - + HtmlEscape.escapeHtml4(BINDING_NAME + " - " + this.account.getThing().getLabel())); - if (!title.isEmpty()) { - html.append(" - "); - html.append(HtmlEscape.escapeHtml4(title)); - } - html.append(""); - html.append("

" + HtmlEscape.escapeHtml4(BINDING_NAME + " - " + this.account.getThing().getLabel())); - if (!title.isEmpty()) { - html.append(" - "); - html.append(HtmlEscape.escapeHtml4(title)); - } - html.append("

"); - return html; - } - - private void createPageEndAndSent(HttpServletResponse resp, StringBuilder html) { - // account overview link - html.append("
"); - html.append(HtmlEscape.escapeHtml4("Account overview")); - html.append("
"); - - html.append(""); - resp.addHeader("content-type", "text/html;charset=UTF-8"); - try { - resp.getWriter().write(html.toString()); - } catch (IOException e) { - logger.warn("return html failed with IO error", e); - } - } - - private void handleIds(HttpServletResponse resp, Connection connection, Device device, @Nullable Thing thing) - throws IOException, URISyntaxException { - StringBuilder html; - if (thing != null) { - html = createPageStart("Channel Options - " + thing.getLabel()); - } else { - html = createPageStart("Device Information - No thing defined"); - } - renderBluetoothMacChannel(connection, device, html); - renderAmazonMusicPlaylistIdChannel(connection, device, html); - renderPlayAlarmSoundChannel(connection, device, html); - renderMusicProviderIdChannel(connection, html); - renderCapabilities(connection, device, html); - createPageEndAndSent(resp, html); - } - - private void renderCapabilities(Connection connection, Device device, StringBuilder html) { - html.append("

Capabilities

"); - html.append(""); - device.getCapabilities().forEach( - capability -> html.append("")); - html.append("
Name
").append(HtmlEscape.escapeHtml4(capability)).append("
"); - } - - private void renderMusicProviderIdChannel(Connection connection, StringBuilder html) { - html.append("

").append(HtmlEscape.escapeHtml4("Channel " + CHANNEL_MUSIC_PROVIDER_ID)).append("

"); - html.append(""); - List musicProviders = connection.getMusicProviders(); - for (JsonMusicProvider musicProvider : musicProviders) { - List properties = musicProvider.supportedProperties; - String providerId = musicProvider.id; - String displayName = musicProvider.displayName; - if (properties != null && properties.contains("Alexa.Music.PlaySearchPhrase") && providerId != null - && !providerId.isEmpty() && "AVAILABLE".equals(musicProvider.availability) && displayName != null - && !displayName.isEmpty()) { - html.append(""); - } - } - html.append("
NameValue
"); - html.append(HtmlEscape.escapeHtml4(displayName)); - html.append(""); - html.append(HtmlEscape.escapeHtml4(providerId)); - html.append("
"); - } - - private void renderPlayAlarmSoundChannel(Connection connection, Device device, StringBuilder html) { - html.append("

").append(HtmlEscape.escapeHtml4("Channel " + CHANNEL_PLAY_ALARM_SOUND)).append("

"); - List notificationSounds = List.of(); - String errorMessage = "No notifications sounds found"; - try { - notificationSounds = connection.getNotificationSounds(device); - } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException - | InterruptedException e) { - errorMessage = e.getLocalizedMessage(); - } - if (!notificationSounds.isEmpty()) { - html.append(""); - for (JsonNotificationSound notificationSound : notificationSounds) { - if (notificationSound.folder == null && notificationSound.providerId != null - && notificationSound.id != null && notificationSound.displayName != null) { - String providerSoundId = notificationSound.providerId + ":" + notificationSound.id; - - html.append(""); - } - } - html.append("
NameValue
"); - html.append(HtmlEscape.escapeHtml4(notificationSound.displayName)); - html.append(""); - html.append(HtmlEscape.escapeHtml4(providerSoundId)); - html.append("
"); - } else { - html.append(HtmlEscape.escapeHtml4(errorMessage)); - } - } - - private void renderAmazonMusicPlaylistIdChannel(Connection connection, Device device, StringBuilder html) { - html.append("

").append(HtmlEscape.escapeHtml4("Channel " + CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID)) - .append("

"); - - JsonPlaylists playLists = null; - String errorMessage = "No playlists found"; - try { - playLists = connection.getPlaylists(device); - } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException - | InterruptedException e) { - errorMessage = e.getLocalizedMessage(); - } - - if (playLists != null) { - Map playlistMap = playLists.playlists; - if (playlistMap != null && !playlistMap.isEmpty()) { - html.append(""); - - for (PlayList[] innerLists : playlistMap.values()) { - { - if (innerLists != null && innerLists.length > 0) { - PlayList playList = innerLists[0]; - if (playList != null && playList.playlistId != null && playList.title != null) { - html.append(""); - } - } - } - } - html.append("
NameValue
"); - html.append(HtmlEscape.escapeHtml4(nullReplacement(playList.title))); - html.append(""); - html.append(HtmlEscape.escapeHtml4(nullReplacement(playList.playlistId))); - html.append("
"); - } else { - html.append(HtmlEscape.escapeHtml4(errorMessage)); - } - } - } - - private void renderBluetoothMacChannel(Connection connection, Device device, StringBuilder html) { - html.append("

").append(HtmlEscape.escapeHtml4("Channel " + CHANNEL_BLUETOOTH_MAC)).append("

"); - JsonBluetoothStates bluetoothStates = connection.getBluetoothConnectionStates(); - if (bluetoothStates == null) { - return; - } - BluetoothState[] innerStates = bluetoothStates.bluetoothStates; - if (innerStates == null) { - return; - } - for (BluetoothState state : innerStates) { - if (state == null) { - continue; - } - String stateDeviceSerialNumber = state.deviceSerialNumber; - if ((stateDeviceSerialNumber == null && device.serialNumber == null) - || (stateDeviceSerialNumber != null && stateDeviceSerialNumber.equals(device.serialNumber))) { - List pairedDeviceList = state.getPairedDeviceList(); - if (!pairedDeviceList.isEmpty()) { - html.append(""); - for (PairedDevice pairedDevice : pairedDeviceList) { - html.append(""); - } - html.append("
NameValue
"); - html.append(HtmlEscape.escapeHtml4(nullReplacement(pairedDevice.friendlyName))); - html.append(""); - html.append(HtmlEscape.escapeHtml4(nullReplacement(pairedDevice.address))); - html.append("
"); - } else { - html.append(HtmlEscape.escapeHtml4("No bluetooth devices paired")); - } - } - } - } - - void handleProxyRequest(Connection connection, HttpServletResponse resp, String verb, String url, - @Nullable String referer, @Nullable String postData, boolean json, String site) throws IOException { - HttpsURLConnection urlConnection; - try { - Map headers = null; - if (referer != null) { - headers = new HashMap<>(); - headers.put("Referer", referer); - } - - urlConnection = connection.makeRequest(verb, url, postData, json, false, headers, 0); - if (urlConnection.getResponseCode() == 302) { - { - String location = urlConnection.getHeaderField("location"); - if (location.contains("/ap/maplanding")) { - try { - connection.registerConnectionAsApp(location); - account.setConnection(connection); - handleDefaultPageResult(resp, "Login succeeded", connection); - this.connectionToInitialize = null; - return; - } catch (URISyntaxException | ConnectionException e) { - returnError(resp, - "Login to '" + connection.getAmazonSite() + "' failed: " + e.getLocalizedMessage()); - this.connectionToInitialize = null; - return; - } - } - - String startString = "https://www." + connection.getAmazonSite() + "/"; - String newLocation = null; - if (location.startsWith(startString) && connection.getIsLoggedIn()) { - newLocation = servletUrl + PROXY_URI_PART + location.substring(startString.length()); - } else if (location.startsWith(startString)) { - newLocation = servletUrl + FORWARD_URI_PART + location.substring(startString.length()); - } else { - startString = "/"; - if (location.startsWith(startString)) { - newLocation = servletUrl + FORWARD_URI_PART + location.substring(startString.length()); - } - } - if (newLocation != null) { - logger.debug("Redirect mapped from {} to {}", location, newLocation); - - resp.sendRedirect(newLocation); - return; - } - returnError(resp, "Invalid redirect to '" + location + "'"); - return; - } - } - } catch (URISyntaxException | ConnectionException | InterruptedException e) { - returnError(resp, e.getLocalizedMessage()); - return; - } - String response = connection.convertStream(urlConnection); - returnHtml(connection, resp, response, site); - } - - private void returnHtml(Connection connection, HttpServletResponse resp, String html) { - returnHtml(connection, resp, html, connection.getAmazonSite()); - } - - private void returnHtml(Connection connection, HttpServletResponse resp, String html, String amazonSite) { - String resultHtml = html.replace("action=\"/", "action=\"" + servletUrl + "/") - .replace("action=\"/", "action=\"" + servletUrl + "/") - .replace("https://www." + amazonSite + "/", servletUrl + "/") - .replace("https://www." + amazonSite + ":443" + "/", servletUrl + "/") - .replace("https://www." + amazonSite + "/", servletUrl + "/") - .replace("https://www." + amazonSite + ":443" + "/", servletUrl + "/") - .replace("http://www." + amazonSite + "/", servletUrl + "/") - .replace("http://www." + amazonSite + "/", servletUrl + "/"); - - resp.addHeader("content-type", "text/html;charset=UTF-8"); - try { - resp.getWriter().write(resultHtml); - } catch (IOException e) { - logger.warn("return html failed with IO error", e); - } - } - - void returnError(HttpServletResponse resp, @Nullable String errorMessage) { - try { - String message = errorMessage != null ? errorMessage : "null"; - resp.getWriter().write("" + HtmlEscape.escapeHtml4(message) + "
Try again"); - } catch (IOException e) { - logger.info("Returning error message failed", e); - } - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java index 10e6fc8597181..40057c47e9e56 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java @@ -12,13 +12,20 @@ */ package org.openhab.binding.amazonechocontrol.internal; -import java.util.Arrays; -import java.util.HashSet; +import java.io.InputStreamReader; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.amazonechocontrol.internal.smarthome.AlexaColor; +import org.openhab.binding.amazonechocontrol.internal.util.ResourceUtil; +import org.openhab.core.library.types.HSBType; import org.openhab.core.thing.ThingTypeUID; -import org.openhab.core.thing.type.ChannelTypeUID; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; /** * The {@link AmazonEchoControlBindingConstants} class defines common constants, which are @@ -37,30 +44,28 @@ public class AmazonEchoControlBindingConstants { public static final ThingTypeUID THING_TYPE_ECHO_SPOT = new ThingTypeUID(BINDING_ID, "echospot"); public static final ThingTypeUID THING_TYPE_ECHO_SHOW = new ThingTypeUID(BINDING_ID, "echoshow"); public static final ThingTypeUID THING_TYPE_ECHO_WHA = new ThingTypeUID(BINDING_ID, "wha"); - public static final ThingTypeUID THING_TYPE_FLASH_BRIEFING_PROFILE = new ThingTypeUID(BINDING_ID, "flashbriefingprofile"); - public static final ThingTypeUID THING_TYPE_SMART_HOME_DEVICE = new ThingTypeUID(BINDING_ID, "smartHomeDevice"); public static final ThingTypeUID THING_TYPE_SMART_HOME_DEVICE_GROUP = new ThingTypeUID(BINDING_ID, "smartHomeDeviceGroup"); - public static final Set SUPPORTED_ECHO_THING_TYPES_UIDS = new HashSet<>( - Arrays.asList(THING_TYPE_ACCOUNT, THING_TYPE_ECHO, THING_TYPE_ECHO_SPOT, THING_TYPE_ECHO_SHOW, - THING_TYPE_ECHO_WHA, THING_TYPE_FLASH_BRIEFING_PROFILE)); + public static final Set SUPPORTED_ECHO_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_ECHO, + THING_TYPE_ECHO_SPOT, THING_TYPE_ECHO_SHOW, THING_TYPE_ECHO_WHA, THING_TYPE_FLASH_BRIEFING_PROFILE); - public static final Set SUPPORTED_SMART_HOME_THING_TYPES_UIDS = new HashSet<>( - Arrays.asList(THING_TYPE_SMART_HOME_DEVICE, THING_TYPE_SMART_HOME_DEVICE_GROUP)); + public static final Set SUPPORTED_SMART_HOME_THING_TYPES_UIDS = Set.of(THING_TYPE_SMART_HOME_DEVICE, + THING_TYPE_SMART_HOME_DEVICE_GROUP); // List of all Channel ids + public static final String CHANNEL_ANNOUNCEMENT = "announcement"; + public static final String CHANNEL_SEND_MESSAGE = "sendMessage"; + public static final String CHANNEL_REFRESH_ACTIVITY = "refreshActivity"; public static final String CHANNEL_PLAYER = "player"; public static final String CHANNEL_VOLUME = "volume"; public static final String CHANNEL_EQUALIZER_TREBLE = "equalizerTreble"; public static final String CHANNEL_EQUALIZER_MIDRANGE = "equalizerMidrange"; public static final String CHANNEL_EQUALIZER_BASS = "equalizerBass"; - public static final String CHANNEL_ERROR = "error"; public static final String CHANNEL_SHUFFLE = "shuffle"; - public static final String CHANNEL_LOOP = "loop"; public static final String CHANNEL_IMAGE_URL = "imageUrl"; public static final String CHANNEL_TITLE = "title"; public static final String CHANNEL_SUBTITLE1 = "subtitle1"; @@ -69,11 +74,6 @@ public class AmazonEchoControlBindingConstants { public static final String CHANNEL_BLUETOOTH_MAC = "bluetoothMAC"; public static final String CHANNEL_BLUETOOTH = "bluetooth"; public static final String CHANNEL_BLUETOOTH_DEVICE_NAME = "bluetoothDeviceName"; - public static final String CHANNEL_RADIO_STATION_ID = "radioStationId"; - public static final String CHANNEL_RADIO = "radio"; - public static final String CHANNEL_AMAZON_MUSIC_TRACK_ID = "amazonMusicTrackId"; - public static final String CHANNEL_AMAZON_MUSIC = "amazonMusic"; - public static final String CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID = "amazonMusicPlayListId"; public static final String CHANNEL_TEXT_TO_SPEECH = "textToSpeech"; public static final String CHANNEL_TEXT_TO_SPEECH_VOLUME = "textToSpeechVolume"; public static final String CHANNEL_TEXT_COMMAND = "textCommand"; @@ -84,37 +84,45 @@ public class AmazonEchoControlBindingConstants { public static final String CHANNEL_PLAY_MUSIC_VOICE_COMMAND = "playMusicVoiceCommand"; public static final String CHANNEL_START_COMMAND = "startCommand"; public static final String CHANNEL_LAST_VOICE_COMMAND = "lastVoiceCommand"; + public static final String CHANNEL_LAST_SPOKEN_TEXT = "lastSpokenText"; public static final String CHANNEL_MEDIA_PROGRESS = "mediaProgress"; public static final String CHANNEL_MEDIA_LENGTH = "mediaLength"; public static final String CHANNEL_MEDIA_PROGRESS_TIME = "mediaProgressTime"; public static final String CHANNEL_ASCENDING_ALARM = "ascendingAlarm"; + public static final String CHANNEL_DO_NOT_DISTURB = "doNotDisturb"; public static final String CHANNEL_NOTIFICATION_VOLUME = "notificationVolume"; public static final String CHANNEL_NEXT_REMINDER = "nextReminder"; public static final String CHANNEL_NEXT_ALARM = "nextAlarm"; public static final String CHANNEL_NEXT_MUSIC_ALARM = "nextMusicAlarm"; public static final String CHANNEL_NEXT_TIMER = "nextTimer"; - public static final String CHANNEL_SAVE = "save"; public static final String CHANNEL_ACTIVE = "active"; public static final String CHANNEL_PLAY_ON_DEVICE = "playOnDevice"; - // List of channel Type UIDs - public static final ChannelTypeUID CHANNEL_TYPE_BLUETHOOTH_MAC = new ChannelTypeUID(BINDING_ID, "bluetoothMAC"); - public static final ChannelTypeUID CHANNEL_TYPE_AMAZON_MUSIC_PLAY_LIST_ID = new ChannelTypeUID(BINDING_ID, - "amazonMusicPlayListId"); - public static final ChannelTypeUID CHANNEL_TYPE_PLAY_ALARM_SOUND = new ChannelTypeUID(BINDING_ID, "playAlarmSound"); - public static final ChannelTypeUID CHANNEL_TYPE_CHANNEL_PLAY_ON_DEVICE = new ChannelTypeUID(BINDING_ID, - "playOnDevice"); - public static final ChannelTypeUID CHANNEL_TYPE_MUSIC_PROVIDER_ID = new ChannelTypeUID(BINDING_ID, - "musicProviderId"); - public static final ChannelTypeUID CHANNEL_TYPE_START_COMMAND = new ChannelTypeUID(BINDING_ID, "startCommand"); - // List of all Properties public static final String DEVICE_PROPERTY_SERIAL_NUMBER = "serialNumber"; public static final String DEVICE_PROPERTY_FAMILY = "deviceFamily"; + public static final String DEVICE_PROPERTY_DEVICE_TYPE_ID = "deviceTypeId"; + public static final String DEVICE_PROPERTY_MANUFACTURER_NAME = "manufacturerName"; + public static final String DEVICE_PROPERTY_DEVICE_IDENTIFIER_LIST = "deviceIdentifierList"; public static final String DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE = "configurationJson"; public static final String DEVICE_PROPERTY_ID = "id"; // Other public static final String FLASH_BRIEFING_COMMAND_PREFIX = "FlashBriefing."; + + public static final String API_VERSION = "2.2.556530.0"; + public static final String DI_OS_VERSION = "16.6"; + public static final String DI_SDK_VERSION = "6.12.4"; + + public static final Map DEVICE_TYPES = ResourceUtil + .readProperties(AmazonEchoControlBindingConstants.class, "device_type.properties"); + + public static final JsonObject CAPABILITY_REGISTRATION = Objects.requireNonNull( + ResourceUtil.getResourceStream(AmazonEchoControlBindingConstants.class, "registration_capabilities.json") + .map(inputStream -> new Gson().fromJson(new InputStreamReader(inputStream), JsonObject.class)) + .orElseThrow(() -> new IllegalStateException("resource not found"))); + public static final List ALEXA_COLORS = ResourceUtil + .readProperties(AlexaColor.class, "color.properties").entrySet().stream() + .map(e -> new AlexaColor(e.getKey(), new HSBType(e.getValue()))).toList(); } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlCommandDescriptionProvider.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlCommandDescriptionProvider.java new file mode 100644 index 0000000000000..951defdb934c6 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlCommandDescriptionProvider.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal; + +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceTO; +import org.openhab.binding.amazonechocontrol.internal.dto.NotificationSoundTO; +import org.openhab.binding.amazonechocontrol.internal.handler.EchoHandler; +import org.openhab.binding.amazonechocontrol.internal.handler.FlashBriefingProfileHandler; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider; +import org.openhab.core.thing.type.DynamicCommandDescriptionProvider; +import org.openhab.core.types.CommandOption; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AmazonEchoControlCommandDescriptionProvider} implements dynamic command description provider for the + * amazonechocontrol binding + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(service = { DynamicCommandDescriptionProvider.class, AmazonEchoControlCommandDescriptionProvider.class }) +public class AmazonEchoControlCommandDescriptionProvider extends BaseDynamicCommandDescriptionProvider { + private final Logger logger = LoggerFactory.getLogger(AmazonEchoControlCommandDescriptionProvider.class); + + public void setFlashBriefingTargets(Collection flashBriefingProfileHandlers, + Collection targets) { + List options = new ArrayList<>(); + options.add(new CommandOption("", "")); + for (DeviceTO device : targets) { + final String value = device.serialNumber; + if (value != null && device.capabilities.contains("FLASH_BRIEFING")) { + options.add(new CommandOption(value, device.accountName)); + } + } + + for (FlashBriefingProfileHandler flashBriefingProfileHandler : flashBriefingProfileHandlers) { + ChannelUID channelUID = new ChannelUID(flashBriefingProfileHandler.getThing().getUID(), + CHANNEL_PLAY_ON_DEVICE); + if (options.isEmpty()) { + channelOptionsMap.remove(channelUID); + } else { + channelOptionsMap.put(channelUID, options); + } + } + } + + public void setEchoHandlerStartCommands(Collection echoHandlers, + Collection flashBriefingProfileHandlers) { + List options = new ArrayList<>(); + options.add(new CommandOption("Weather", "Weather")); + options.add(new CommandOption("Traffic", "Traffic")); + options.add(new CommandOption("GoodMorning", "Good morning")); + options.add(new CommandOption("SingASong", "Song")); + options.add(new CommandOption("TellStory", "Story")); + options.add(new CommandOption("FlashBriefing", "Flash briefing")); + + for (FlashBriefingProfileHandler flashBriefing : flashBriefingProfileHandlers) { + String value = FLASH_BRIEFING_COMMAND_PREFIX + flashBriefing.getThing().getUID().getId(); + String displayName = flashBriefing.getThing().getLabel(); + options.add(new CommandOption(value, displayName)); + } + + for (EchoHandler echoHandler : echoHandlers) { + ChannelUID channelUID = new ChannelUID(echoHandler.getThing().getUID(), CHANNEL_START_COMMAND); + if (options.isEmpty()) { + channelOptionsMap.remove(channelUID); + } else { + channelOptionsMap.put(channelUID, options); + } + } + } + + public void setEchoHandlerAlarmSounds(EchoHandler echoHandler, List alarmSounds) { + List options = new ArrayList<>(); + for (NotificationSoundTO notificationSound : alarmSounds) { + if (notificationSound.folder == null && notificationSound.providerId != null && notificationSound.id != null + && notificationSound.displayName != null) { + String providerSoundId = notificationSound.providerId + ":" + notificationSound.id; + options.add(new CommandOption(providerSoundId, notificationSound.displayName)); + } + } + + ChannelUID channelUID = new ChannelUID(echoHandler.getThing().getUID(), CHANNEL_PLAY_ALARM_SOUND); + if (options.isEmpty()) { + channelOptionsMap.remove(channelUID); + } else { + channelOptionsMap.put(channelUID, options); + } + } + + public void removeCommandDescriptionForThing(ThingUID thingUID) { + logger.trace("removing state description for thing {}", thingUID); + channelOptionsMap.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID)); + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index f4ad5253c4bb2..d72889322661b 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -14,44 +14,35 @@ import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; -import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; -import org.openhab.binding.amazonechocontrol.internal.discovery.SmartHomeDevicesDiscovery; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http2.client.HTTP2Client; import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler; import org.openhab.binding.amazonechocontrol.internal.handler.EchoHandler; import org.openhab.binding.amazonechocontrol.internal.handler.FlashBriefingProfileHandler; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.core.config.discovery.AbstractDiscoveryService; -import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.binding.amazonechocontrol.internal.util.NonNullListTypeAdapterFactory; +import org.openhab.binding.amazonechocontrol.internal.util.SerializeNullTypeAdapterFactory; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.storage.Storage; import org.openhab.core.storage.StorageService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; -import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; -import org.osgi.framework.ServiceRegistration; -import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; -import org.osgi.service.http.HttpService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; /** * The {@link AmazonEchoControlHandlerFactory} is responsible for creating things and thing @@ -63,22 +54,42 @@ AmazonEchoControlHandlerFactory.class }, configurationPid = "binding.amazonechocontrol") @NonNullByDefault public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { - private final Logger logger = LoggerFactory.getLogger(AmazonEchoControlHandlerFactory.class); - private final Map>> discoveryServiceRegistrations = new HashMap<>(); - private final Set accountHandlers = new HashSet<>(); - private final HttpService httpService; private final StorageService storageService; - private final BindingServlet bindingServlet; + private final Gson gson; + private final HttpClient httpClient; + private final HTTP2Client http2Client; + + private final AmazonEchoControlStateDescriptionProvider amazonEchoControlStateDescriptionProvider; + private final AmazonEchoControlCommandDescriptionProvider amazonEchoControlCommandDescriptionProvider; @Activate - public AmazonEchoControlHandlerFactory(@Reference HttpService httpService, - @Reference StorageService storageService) { + public AmazonEchoControlHandlerFactory(@Reference StorageService storageService, + @Reference AmazonEchoControlStateDescriptionProvider dynamicStateDescriptionProvider, + @Reference HttpClientFactory httpClientFactory, + @Reference AmazonEchoControlCommandDescriptionProvider amazonEchoControlCommandDescriptionProvider) + throws Exception { this.storageService = storageService; - this.httpService = httpService; - this.gson = new Gson(); - this.bindingServlet = new BindingServlet(httpService); + this.gson = new GsonBuilder().registerTypeAdapterFactory(new NonNullListTypeAdapterFactory()) + .registerTypeAdapterFactory(new SerializeNullTypeAdapterFactory()).create(); + this.amazonEchoControlStateDescriptionProvider = dynamicStateDescriptionProvider; + this.amazonEchoControlCommandDescriptionProvider = amazonEchoControlCommandDescriptionProvider; + + this.httpClient = httpClientFactory.createHttpClient("openhab-aec"); + this.http2Client = httpClientFactory.createHttp2Client("openhab-aec", httpClient.getSslContextFactory()); + http2Client.setConnectTimeout(10000); + http2Client.setIdleTimeout(-1); + + httpClient.start(); + http2Client.start(); + } + + @Deactivate + @SuppressWarnings("unused") + public void deactivate() throws Exception { + http2Client.stop(); + httpClient.stop(); } @Override @@ -87,75 +98,33 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { || SUPPORTED_SMART_HOME_THING_TYPES_UIDS.contains(thingTypeUID); } - @Override - protected void deactivate(ComponentContext componentContext) { - bindingServlet.dispose(); - super.deactivate(componentContext); - } - @Override protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { Storage storage = storageService.getStorage(thing.getUID().toString(), String.class.getClassLoader()); - AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService, storage, gson); + AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, storage, gson, httpClient, http2Client, + amazonEchoControlCommandDescriptionProvider); accountHandlers.add(bridgeHandler); - registerDiscoveryService(bridgeHandler); - bindingServlet.addAccountThing(thing); return bridgeHandler; } else if (thingTypeUID.equals(THING_TYPE_FLASH_BRIEFING_PROFILE)) { - Storage storage = storageService.getStorage(thing.getUID().toString(), - String.class.getClassLoader()); - return new FlashBriefingProfileHandler(thing, storage); + Storage storage = storageService.getStorage(thing.getUID().toString()); + return new FlashBriefingProfileHandler(thing, storage, gson); } else if (SUPPORTED_ECHO_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new EchoHandler(thing, gson); + return new EchoHandler(thing, gson, amazonEchoControlStateDescriptionProvider); } else if (SUPPORTED_SMART_HOME_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new SmartHomeDeviceHandler(thing, gson); + return new SmartHomeDeviceHandler(thing, gson, amazonEchoControlCommandDescriptionProvider, + amazonEchoControlStateDescriptionProvider); } return null; } - private synchronized void registerDiscoveryService(AccountHandler bridgeHandler) { - List> discoveryServiceRegistration = Objects.requireNonNull(discoveryServiceRegistrations - .computeIfAbsent(bridgeHandler.getThing().getUID(), k -> new ArrayList<>())); - SmartHomeDevicesDiscovery smartHomeDevicesDiscovery = new SmartHomeDevicesDiscovery(bridgeHandler); - smartHomeDevicesDiscovery.activate(); - discoveryServiceRegistration.add(bundleContext.registerService(DiscoveryService.class.getName(), - smartHomeDevicesDiscovery, new Hashtable<>())); - - AmazonEchoDiscovery discoveryService = new AmazonEchoDiscovery(bridgeHandler); - discoveryService.activate(); - discoveryServiceRegistration.add( - bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>())); - } - @Override protected synchronized void removeHandler(ThingHandler thingHandler) { + amazonEchoControlCommandDescriptionProvider.removeCommandDescriptionForThing(thingHandler.getThing().getUID()); if (thingHandler instanceof AccountHandler) { accountHandlers.remove(thingHandler); - BindingServlet bindingServlet = this.bindingServlet; - bindingServlet.removeAccountThing(thingHandler.getThing()); - - List> discoveryServiceRegistration = discoveryServiceRegistrations - .remove(thingHandler.getThing().getUID()); - if (discoveryServiceRegistration != null) { - discoveryServiceRegistration.forEach(serviceReg -> { - AbstractDiscoveryService service = (AbstractDiscoveryService) bundleContext - .getService(serviceReg.getReference()); - serviceReg.unregister(); - if (service != null) { - if (service instanceof AmazonEchoDiscovery discovery) { - discovery.deactivate(); - } else if (service instanceof SmartHomeDevicesDiscovery discovery) { - discovery.deactivate(); - } else { - logger.warn("Found unknown discovery-service instance: {}", service); - } - } - }); - } } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlServlet.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlServlet.java new file mode 100644 index 0000000000000..09e87188a0094 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlServlet.java @@ -0,0 +1,516 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal; + +import static org.eclipse.jetty.util.StringUtil.isNotBlank; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlServlet.SERVLET_PATH; +import static org.openhab.binding.amazonechocontrol.internal.util.Util.findIn; +import static org.unbescape.html.HtmlEscape.escapeHtml4; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.servlet.Servlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.util.introspection.UberspectImpl; +import org.apache.velocity.util.introspection.UberspectPublicFields; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.MimeTypes; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceTO; +import org.openhab.binding.amazonechocontrol.internal.dto.NotificationSoundTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.BluetoothStateTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.MusicProviderTO; +import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler; +import org.openhab.binding.amazonechocontrol.internal.util.HttpRequestBuilder; +import org.openhab.core.thing.Thing; +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; +import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletName; +import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletPattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AmazonEchoControlServlet} allows to log in to Amazon accounts using a proxy and shows information about + * configured accounts and devices + * + * @author Michael Geramb - Initial Contribution + * @author Jan N. Klug - Refactored to whiteboard, merged both servlets, use Velocity templates + */ +@Component(service = Servlet.class, immediate = true) +@HttpWhiteboardServletName(SERVLET_PATH) +@HttpWhiteboardServletPattern({ SERVLET_PATH, SERVLET_PATH + "/*" }) +@NonNullByDefault +public class AmazonEchoControlServlet extends HttpServlet { + public static final String SERVLET_PATH = "/" + BINDING_ID; + private static final long serialVersionUID = -9158865063627039237L; + private static final String FORWARD_URI_PART = "/FORWARD/"; + private static final String PROXY_URI_PART = "/PROXY/"; + + private final Logger logger = LoggerFactory.getLogger(AmazonEchoControlServlet.class); + private final VelocityEngine velocityEngine = new VelocityEngine(); + + private final AmazonEchoControlHandlerFactory handlerFactory; + + @Activate + public AmazonEchoControlServlet(@Reference AmazonEchoControlHandlerFactory handlerFactory) { + this.handlerFactory = handlerFactory; + + velocityEngine.setProperty("introspector.uberspect.class", + UberspectImpl.class.getName() + ", " + UberspectPublicFields.class.getName()); + velocityEngine.init(); + } + + private @Nullable AccountHandler getAccountHandler(String accountUid) { + ThingUID thingUID = new ThingUID(THING_TYPE_ACCOUNT, URLDecoder.decode(accountUid, StandardCharsets.UTF_8)); + return handlerFactory.getAccountHandlers().stream().filter(h -> thingUID.equals(h.getThing().getUID())) + .findAny().orElse(null); + } + + @Override + protected void doPut(@NonNullByDefault({}) HttpServletRequest req, @NonNullByDefault({}) HttpServletResponse resp) + throws IOException { + preProcess(HttpMethod.PUT, req, resp); + } + + @Override + protected void doDelete(@NonNullByDefault({}) HttpServletRequest req, + @NonNullByDefault({}) HttpServletResponse resp) throws IOException { + preProcess(HttpMethod.DELETE, req, resp); + } + + @Override + protected void doPost(@NonNullByDefault({}) HttpServletRequest req, @NonNullByDefault({}) HttpServletResponse resp) + throws IOException { + preProcess(HttpMethod.POST, req, resp); + } + + @Override + protected void doGet(@NonNullByDefault({}) HttpServletRequest req, @NonNullByDefault({}) HttpServletResponse resp) + throws IOException { + preProcess(HttpMethod.GET, req, resp); + } + + private void preProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws IOException { + ServletUri servletUri = ServletUri.fromFullUri(req.getRequestURI()); + if (servletUri == null) { + returnError(resp, null, "Could not parse URI for " + method + "/" + req.getRequestURI()); + return; + } + if ("static".equals(servletUri.account())) { + serveStatic(resp, servletUri.request()); + } else if (!servletUri.account().isBlank()) { + switch (method) { + case DELETE, POST, PUT -> doAccountDeletePostPut(method, servletUri, req, resp); + case GET -> doAccountGet(servletUri, req, resp); + default -> returnError(resp, servletUri, "Can't handle " + method + " request for accounts."); + } + } else { + if (HttpMethod.GET.equals(method)) { + doBindingGet(resp); + } else { + returnError(resp, servletUri, "Can't handle " + method + " requests for the binding."); + } + } + } + + private void doBindingGet(HttpServletResponse resp) throws IOException { + VelocityContext ctx = new VelocityContext(); + ctx.put("servletPath", SERVLET_PATH); + ctx.put("accounts", handlerFactory.getAccountHandlers().stream() + .sorted(Comparator.comparing(h -> h.getThing().getUID().toString())).toList()); + + StringWriter stringWriter = evaluateTemplate("WEB-INF/binding.vm", ctx); + + resp.addHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.TEXT_HTML_UTF_8.asString()); + resp.getWriter().write(stringWriter.toString()); + } + + private void doAccountDeletePostPut(HttpMethod method, ServletUri uriParts, HttpServletRequest req, + HttpServletResponse resp) throws IOException { + String uri = uriParts.request(); + String queryString = req.getQueryString(); + if (queryString != null && !queryString.isEmpty()) { + uri += "?" + queryString; + } + + AccountHandler accountHandler = getAccountHandler(uriParts.account()); + if (accountHandler == null) { + returnError(resp, uriParts, "Could not find account handler"); + return; + } + Connection connection = accountHandler.getConnection(); + if (uri.startsWith(PROXY_URI_PART)) { + // handle proxy request + String proxyUrl = connection.getAlexaServer() + "/" + uri.substring(PROXY_URI_PART.length()); + + Object postData = null; + if (HttpMethod.PUT.equals(method) || HttpMethod.POST.equals(method)) { + postData = req.getReader().lines().collect(Collectors.joining()); + } + + this.handleProxyRequest(accountHandler, connection, resp, uriParts, method, proxyUrl, null, postData, + postData != null, connection.getRetailDomain()); + return; + } + + // handle post of login page + if (connection.isLoggedIn()) { + returnError(resp, uriParts, "Connection not in initialize mode."); + return; + } + + resp.addHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.TEXT_HTML_UTF_8.asString()); + + Map map = req.getParameterMap(); + StringBuilder postDataBuilder = new StringBuilder(); + for (String name : map.keySet()) { + if (!postDataBuilder.isEmpty()) { + postDataBuilder.append('&'); + } + + postDataBuilder.append(name); + postDataBuilder.append('='); + String value = ""; + if ("failedSignInCount".equals(name)) { + value = "ape:AA=="; + } else { + String[] strings = map.get(name); + if (strings != null && strings.length > 0) { + value = strings[0]; + } + } + postDataBuilder.append(URLEncoder.encode(value, StandardCharsets.UTF_8)); + } + + String relativeUrl = uriParts.request().replace(FORWARD_URI_PART, "/"); + + String retailDomain = relativeUrl.startsWith("/ap/signin") ? "amazon.com" : connection.getRetailDomain(); + String postUrl = "https://www." + retailDomain + relativeUrl; + queryString = req.getQueryString(); + if (isNotBlank(queryString)) { + postUrl += "?" + queryString; + } + String referer = "https://www." + retailDomain; + String postData = postDataBuilder.toString(); + handleProxyRequest(accountHandler, connection, resp, uriParts, method, postUrl, referer, postData, false, + retailDomain); + } + + private void doAccountGet(ServletUri uriParts, HttpServletRequest req, HttpServletResponse resp) + throws IOException { + String uri = uriParts.request(); + String queryString = req.getQueryString(); + if (isNotBlank(queryString)) { + uri += "?" + queryString; + } + try { + AccountHandler accountHandler = getAccountHandler(uriParts.account()); + if (accountHandler == null) { + returnError(resp, uriParts, "Could not find account handler."); + return; + } + + Connection connection = accountHandler.getConnection(); + if (uri.startsWith(FORWARD_URI_PART)) { + String getUrl = connection.getRetailUrl() + "/" + uri.substring(FORWARD_URI_PART.length()); + + this.handleProxyRequest(accountHandler, connection, resp, uriParts, HttpMethod.GET, getUrl, null, null, + false, connection.getRetailDomain()); + return; + } + + if (uri.startsWith(PROXY_URI_PART)) { + // handle proxy request + String proxyUrl = connection.getAlexaServer() + "/" + uri.substring(PROXY_URI_PART.length()); + + this.handleProxyRequest(accountHandler, connection, resp, uriParts, HttpMethod.GET, proxyUrl, null, + null, false, connection.getRetailDomain()); + return; + } + + if (connection.verifyLogin()) { + // handle commands + if ("/logout".equals(uriParts.request()) || "/logout/".equals(uriParts.request())) { + accountHandler.resetConnection(false); + resp.sendRedirect(uriParts.buildFor("/")); + return; + } + // handle commands + if ("/newdevice".equals(uriParts.request()) || "/newdevice/".equals(uriParts.request())) { + accountHandler.resetConnection(true); + resp.sendRedirect(uriParts.buildFor("/")); + return; + } + if ("/ids".equals(uriParts.request()) || "/ids/".equals(uriParts.request())) { + String serialNumber = getQueryMap(queryString).get("serialNumber"); + DeviceTO device = accountHandler.findDevice(serialNumber); + if (device != null) { + Thing thing = accountHandler.getThingBySerialNumber(device.serialNumber); + if (thing == null) { + returnError(resp, uriParts, "No thing defined for " + serialNumber); + } else { + createDeviceDetailsResponse(resp, uriParts, connection, device, thing); + } + return; + } + } + // return hint that everything is ok + createAccountPage(resp, uriParts, accountHandler, connection); + return; + } + + if (!uriParts.request().isBlank()) { + resp.sendRedirect(SERVLET_PATH + "/" + uriParts.account()); + return; + } + + String html = connection.getLoginPage(); + returnHtml(resp, uriParts, html, "amazon.com"); + } catch (ConnectionException e) { + logger.warn("get failed with uri syntax error", e); + } + } + + private void createAccountPage(HttpServletResponse resp, ServletUri uriParts, AccountHandler accountHandler, + Connection connection) throws IOException { + VelocityContext ctx = new VelocityContext(); + ctx.put("servletPath", SERVLET_PATH); + ctx.put("accountPath", uriParts.buildFor("/")); + ctx.put("account", accountHandler); + ctx.put("connection", connection); + ctx.put("devices", accountHandler.getLastKnownDevices().stream() + .sorted(Comparator.comparing(d -> d.serialNumber)).toList()); + ctx.put("DEVICE_TYPES", DEVICE_TYPES); + + StringWriter stringWriter = evaluateTemplate("WEB-INF/account-detail.vm", ctx); + resp.addHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.TEXT_HTML_UTF_8.asString()); + resp.getWriter().write(stringWriter.toString()); + } + + private void createDeviceDetailsResponse(HttpServletResponse resp, ServletUri uriParts, Connection connection, + DeviceTO device, Thing thing) throws IOException { + Map> channels = new HashMap<>(); + List musicProviders = connection.getMusicProviders().stream().filter(this::isValidMusicProvider) + .map(p -> new ChannelOption(p.id, p.displayName)).sorted(Comparator.comparing(o -> o.value)).toList(); + channels.put(CHANNEL_MUSIC_PROVIDER_ID, musicProviders); + + List alarmSounds = connection.getNotificationSounds(device).stream() + .filter(this::isValidAlarmSound).map(p -> new ChannelOption(p.providerId + ":" + p.id, p.displayName)) + .sorted(Comparator.comparing(o -> o.value)).toList(); + channels.put(CHANNEL_PLAY_ALARM_SOUND, alarmSounds); + + List states = connection.getBluetoothConnectionStates(); + List pairedDevices = findIn(states, k -> k.deviceSerialNumber, device.serialNumber) + .map(state -> state.pairedDeviceList) + .map(list -> list.stream().map(p -> new ChannelOption(p.address, p.friendlyName)) + .sorted(Comparator.comparing(o -> o.value)).toList()) + .orElse(List.of()); + channels.put(CHANNEL_BLUETOOTH_MAC, Objects.requireNonNull(pairedDevices)); + + VelocityContext ctx = new VelocityContext(); + ctx.put("thing", thing); + ctx.put("servletPath", SERVLET_PATH); + ctx.put("accountPath", uriParts.buildFor("/")); + ctx.put("channels", channels); + ctx.put("capabilities", device.capabilities.stream().sorted().toList()); + + StringWriter stringWriter = evaluateTemplate("WEB-INF/device-detail.vm", ctx); + resp.addHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.TEXT_HTML_UTF_8.asString()); + resp.getWriter().write(stringWriter.toString()); + } + + private boolean isValidMusicProvider(MusicProviderTO provider) { + return provider.supportedProperties.contains("Alexa.Music.PlaySearchPhrase") + && "AVAILABLE".equals(provider.availability) && isNotBlank(provider.displayName); + } + + private boolean isValidAlarmSound(NotificationSoundTO sound) { + return sound.folder == null && sound.providerId != null && sound.id != null && sound.displayName != null; + } + + private void handleProxyRequest(AccountHandler accountHandler, Connection connection, HttpServletResponse resp, + ServletUri uriParts, HttpMethod method, String url, @Nullable String referer, @Nullable Object postData, + boolean isJson, String retailDomain) throws IOException { + try { + Map headers = new HashMap<>(); + if (referer != null) { + headers.put(HttpHeader.REFERER.asString(), referer); + } + + HttpRequestBuilder.HttpResponse response = connection.getRequestBuilder().builder(method, url) + .withContent(postData).withJson(isJson).withHeaders(headers).retry(false).redirect(false) + .syncSend(); + if (response.statusCode() == HttpStatus.FOUND_302) { + String location = response.headers().get("location"); + if (location.contains("/ap/maplanding")) { + try { + URI oAuthRedirectUri = new URI(location); + String accessToken = getQueryMap(oAuthRedirectUri.getQuery()).get("openid.oa2.access_token"); + if (accessToken == null) { + returnError(resp, uriParts, + "Login to '" + retailDomain + "' failed: Could not extract accessToken."); + } else if (connection.registerConnectionAsApp(accessToken)) { + accountHandler.setConnection(connection); + resp.sendRedirect(SERVLET_PATH + "/" + uriParts.account()); + // createAccountPage(resp, uriParts, accountHandler, connection); + } else { + returnError(resp, uriParts, + "Login to '" + retailDomain + "' failed: Could not register as app."); + } + return; + } catch (URISyntaxException e) { + returnError(resp, uriParts, + "Login to '" + retailDomain + "' failed: " + e.getLocalizedMessage()); + accountHandler.resetConnection(false); + return; + } + } + + String startString = connection.getRetailUrl() + "/"; + String newLocation = null; + if (location.startsWith(startString) && connection.isLoggedIn()) { + newLocation = uriParts.buildFor(PROXY_URI_PART + location.substring(startString.length())); + } else if (location.startsWith(startString)) { + newLocation = uriParts.buildFor(FORWARD_URI_PART + location.substring(startString.length())); + } else { + startString = "/"; + if (location.startsWith(startString)) { + newLocation = uriParts.buildFor(FORWARD_URI_PART + location.substring(startString.length())); + } + } + if (newLocation != null) { + logger.debug("Redirect mapped from {} to {}", location, newLocation); + resp.sendRedirect(newLocation); + return; + } + returnError(resp, uriParts, "Invalid redirect to '" + location + "'"); + return; + } + returnHtml(resp, uriParts, response.content(), retailDomain); + } catch (ConnectionException e) { + returnError(resp, uriParts, e.getLocalizedMessage()); + } + } + + private void returnHtml(HttpServletResponse resp, ServletUri uriParts, String html, String retailDomain) + throws IOException { + String servletUrl = uriParts.buildFor("/"); + String resultHtml = html.replace("action=\"/", "action=\"" + servletUrl) + .replace("action=\"/", "action=\"" + servletUrl) + .replace("https://www." + retailDomain + "/", servletUrl) + .replace("https://www." + retailDomain + ":443" + "/", servletUrl) + .replace("https://www." + retailDomain + "/", servletUrl) + .replace("https://www." + retailDomain + ":443" + "/", servletUrl) + .replace("http://www." + retailDomain + "/", servletUrl) + .replace("http://www." + retailDomain + "/", servletUrl); + resp.addHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.TEXT_HTML_UTF_8.asString()); + resp.getWriter().write(resultHtml); + } + + void returnError(HttpServletResponse resp, @Nullable ServletUri uriParts, @Nullable String errorMessage) + throws IOException { + String message = errorMessage != null ? errorMessage : "null"; + String tryAgainUri = uriParts == null ? SERVLET_PATH + "/" : uriParts.buildFor("/"); + resp.getWriter() + .write("" + escapeHtml4(message) + "
Try again"); + } + + private Map getQueryMap(@Nullable String query) { + Map map = new HashMap<>(); + if (query != null) { + String[] params = query.split("&"); + for (String param : params) { + String[] elements = param.split("="); + if (elements.length == 2) { + String name = elements[0]; + String value = URLDecoder.decode(elements[1], StandardCharsets.UTF_8); + map.put(name, value); + } + } + } + return map; + } + + private StringWriter evaluateTemplate(String template, VelocityContext ctx) { + StringWriter stringWriter = new StringWriter(); + ClassLoader classLoader = AmazonEchoControlServlet.class.getClassLoader(); + if (classLoader == null) { + return stringWriter; + } + try (InputStream inputStream = classLoader.getResourceAsStream(template)) { + if (inputStream != null) { + Reader reader = new InputStreamReader(inputStream); + velocityEngine.evaluate(ctx, stringWriter, "VTL", reader); + } + } catch (IOException ignored) { + } + return stringWriter; + } + + private void serveStatic(HttpServletResponse resp, String file) throws IOException { + ClassLoader classLoader = AmazonEchoControlServlet.class.getClassLoader(); + if (classLoader == null) { + resp.sendError(500); + return; + } + try (InputStream inputStream = classLoader.getResourceAsStream("WEB-INF" + file)) { + if (inputStream != null) { + String content = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines() + .collect(Collectors.joining("\n")); + resp.getWriter().write(content); + return; + } + } catch (IOException ignored) { + } + resp.sendError(404); + } + + public static class ChannelOption { + public String value; + public String displayName; + + public ChannelOption(String value, String displayName) { + this.value = value; + this.displayName = displayName; + } + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlStateDescriptionProvider.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlStateDescriptionProvider.java new file mode 100644 index 0000000000000..e30234d0faff4 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlStateDescriptionProvider.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.amazonechocontrol.internal; + +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.openhab.core.types.StateDescription; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dynamic channel state description provider. + * Overrides the state description for the controls, which receive its configuration in the runtime. + * + * @author Jan N. Klug - Initial contribution + */ +@Component(service = { DynamicStateDescriptionProvider.class, AmazonEchoControlStateDescriptionProvider.class }) +@NonNullByDefault +public class AmazonEchoControlStateDescriptionProvider implements DynamicStateDescriptionProvider { + private final Map descriptions = new ConcurrentHashMap<>(); + private final Logger logger = LoggerFactory.getLogger(AmazonEchoControlStateDescriptionProvider.class); + + /** + * Set a state description for a channel. This description will be used when preparing the channel state by + * the framework for presentation. A previous description, if existed, will be replaced. + * + * @param channelUID channel UID + * @param description state description for the channel + */ + public void setDescription(ChannelUID channelUID, StateDescription description) { + logger.trace("adding state description for channel {}", channelUID); + descriptions.put(channelUID, description); + } + + /** + * remove all descriptions for a given thing + * + * @param thingUID the thing's UID + */ + public void removeDescriptionsForThing(ThingUID thingUID) { + logger.trace("removing state description for thing {}", thingUID); + descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID)); + } + + @Override + public @Nullable StateDescription getStateDescription(Channel channel, + @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { + if (descriptions.containsKey(channel.getUID())) { + logger.trace("returning new stateDescription for {}", channel.getUID()); + return descriptions.get(channel.getUID()); + } else { + return null; + } + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoDynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoDynamicStateDescriptionProvider.java deleted file mode 100644 index 9a4cbc4f83a3d..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoDynamicStateDescriptionProvider.java +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal; - -import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler; -import org.openhab.binding.amazonechocontrol.internal.handler.EchoHandler; -import org.openhab.binding.amazonechocontrol.internal.handler.FlashBriefingProfileHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.PairedDevice; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists.PlayList; -import org.openhab.core.thing.Channel; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingRegistry; -import org.openhab.core.thing.ThingUID; -import org.openhab.core.thing.binding.ThingHandler; -import org.openhab.core.thing.type.ChannelTypeUID; -import org.openhab.core.thing.type.DynamicStateDescriptionProvider; -import org.openhab.core.types.StateDescription; -import org.openhab.core.types.StateDescriptionFragmentBuilder; -import org.openhab.core.types.StateOption; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; - -/** - * Dynamic channel state description provider. - * Overrides the state description for the controls, which receive its configuration in the runtime. - * - * @author Michael Geramb - Initial contribution - */ -@Component(service = { DynamicStateDescriptionProvider.class, AmazonEchoDynamicStateDescriptionProvider.class }) -@NonNullByDefault -public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDescriptionProvider { - private final ThingRegistry thingRegistry; - - @Activate - public AmazonEchoDynamicStateDescriptionProvider(@Reference ThingRegistry thingRegistry) { - this.thingRegistry = thingRegistry; - } - - public @Nullable ThingHandler findHandler(Channel channel) { - Thing thing = thingRegistry.get(channel.getUID().getThingUID()); - if (thing == null) { - return null; - } - ThingUID accountThingId = thing.getBridgeUID(); - if (accountThingId == null) { - return null; - } - Thing accountThing = thingRegistry.get(accountThingId); - if (accountThing == null) { - return null; - } - AccountHandler accountHandler = (AccountHandler) accountThing.getHandler(); - if (accountHandler == null) { - return null; - } - Connection connection = accountHandler.findConnection(); - if (connection == null || !connection.getIsLoggedIn()) { - return null; - } - return thing.getHandler(); - } - - @Override - public @Nullable StateDescription getStateDescription(Channel channel, - @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { - ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); - if (channelTypeUID == null || !BINDING_ID.equals(channelTypeUID.getBindingId())) { - return null; - } - if (originalStateDescription == null) { - return null; - } - - if (CHANNEL_TYPE_BLUETHOOTH_MAC.equals(channel.getChannelTypeUID())) { - EchoHandler handler = (EchoHandler) findHandler(channel); - if (handler == null) { - return null; - } - BluetoothState bluetoothState = handler.findBluetoothState(); - if (bluetoothState == null) { - return null; - } - List pairedDeviceList = bluetoothState.getPairedDeviceList(); - if (pairedDeviceList.isEmpty()) { - return null; - } - List options = new ArrayList<>(); - options.add(new StateOption("", "")); - for (PairedDevice device : pairedDeviceList) { - final String value = device.address; - if (value != null && device.friendlyName != null) { - options.add(new StateOption(value, device.friendlyName)); - } - } - StateDescription result = StateDescriptionFragmentBuilder.create(originalStateDescription) - .withOptions(options).build().toStateDescription(); - return result; - } else if (CHANNEL_TYPE_AMAZON_MUSIC_PLAY_LIST_ID.equals(channel.getChannelTypeUID())) { - EchoHandler handler = (EchoHandler) findHandler(channel); - if (handler == null) { - return null; - } - - JsonPlaylists playLists = handler.findPlaylists(); - if (playLists == null) { - return null; - } - - List options = new ArrayList<>(); - options.add(new StateOption("", "")); - Map playlistMap = playLists.playlists; - if (playlistMap != null) { - for (PlayList[] innerLists : playlistMap.values()) { - if (innerLists != null && innerLists.length > 0) { - PlayList playList = innerLists[0]; - final String value = playList.playlistId; - if (value != null && playList.title != null) { - options.add(new StateOption(value, - String.format("%s (%d)", playList.title, playList.trackCount))); - } - } - } - } - StateDescription result = StateDescriptionFragmentBuilder.create(originalStateDescription) - .withOptions(options).build().toStateDescription(); - return result; - } else if (CHANNEL_TYPE_PLAY_ALARM_SOUND.equals(channel.getChannelTypeUID())) { - EchoHandler handler = (EchoHandler) findHandler(channel); - if (handler == null) { - return null; - } - - List notificationSounds = handler.findAlarmSounds(); - if (notificationSounds.isEmpty()) { - return null; - } - - List options = new ArrayList<>(); - options.add(new StateOption("", "")); - - for (JsonNotificationSound notificationSound : notificationSounds) { - if (notificationSound.folder == null && notificationSound.providerId != null - && notificationSound.id != null && notificationSound.displayName != null) { - String providerSoundId = notificationSound.providerId + ":" + notificationSound.id; - options.add(new StateOption(providerSoundId, notificationSound.displayName)); - } - } - StateDescription result = StateDescriptionFragmentBuilder.create(originalStateDescription) - .withOptions(options).build().toStateDescription(); - return result; - } else if (CHANNEL_TYPE_CHANNEL_PLAY_ON_DEVICE.equals(channel.getChannelTypeUID())) { - FlashBriefingProfileHandler handler = (FlashBriefingProfileHandler) findHandler(channel); - if (handler == null) { - return null; - } - AccountHandler accountHandler = handler.findAccountHandler(); - if (accountHandler == null) { - return null; - } - List devices = accountHandler.getLastKnownDevices(); - if (devices.isEmpty()) { - return null; - } - - List options = new ArrayList<>(); - options.add(new StateOption("", "")); - for (Device device : devices) { - final String value = device.serialNumber; - if (value != null && device.getCapabilities().contains("FLASH_BRIEFING")) { - options.add(new StateOption(value, device.accountName)); - } - } - return StateDescriptionFragmentBuilder.create(originalStateDescription).withOptions(options).build() - .toStateDescription(); - } else if (CHANNEL_TYPE_MUSIC_PROVIDER_ID.equals(channel.getChannelTypeUID())) { - EchoHandler handler = (EchoHandler) findHandler(channel); - if (handler == null) { - return null; - } - List musicProviders = handler.findMusicProviders(); - if (musicProviders.isEmpty()) { - return null; - } - - List options = new ArrayList<>(); - for (JsonMusicProvider musicProvider : musicProviders) { - List properties = musicProvider.supportedProperties; - String providerId = musicProvider.id; - String displayName = musicProvider.displayName; - if (properties != null && properties.contains("Alexa.Music.PlaySearchPhrase") && providerId != null - && !providerId.isEmpty() && "AVAILABLE".equals(musicProvider.availability) - && displayName != null && !displayName.isEmpty()) { - options.add(new StateOption(providerId, displayName)); - } - } - return StateDescriptionFragmentBuilder.create(originalStateDescription).withOptions(options).build() - .toStateDescription(); - } else if (CHANNEL_TYPE_START_COMMAND.equals(channel.getChannelTypeUID())) { - EchoHandler handler = (EchoHandler) findHandler(channel); - if (handler == null) { - return null; - } - AccountHandler account = handler.findAccount(); - if (account == null) { - return null; - } - List flashbriefings = account.getFlashBriefingProfileHandlers(); - if (flashbriefings.isEmpty()) { - return null; - } - - List options = new ArrayList<>(); - options.addAll(originalStateDescription.getOptions()); - - for (FlashBriefingProfileHandler flashBriefing : flashbriefings) { - String value = FLASH_BRIEFING_COMMAND_PREFIX + flashBriefing.getThing().getUID().getId(); - String displayName = flashBriefing.getThing().getLabel(); - options.add(new StateOption(value, displayName)); - } - return StateDescriptionFragmentBuilder.create(originalStateDescription).withOptions(options).build() - .toStateDescription(); - } - return null; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java deleted file mode 100644 index 77a37158bbe2c..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal; - -import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.BINDING_NAME; - -import java.io.IOException; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.List; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.thing.Thing; -import org.osgi.service.http.HttpService; -import org.osgi.service.http.NamespaceException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.unbescape.html.HtmlEscape; - -/** - * This servlet provides the base navigation page, with hyperlinks for the defined account things - * - * @author Michael Geramb - Initial Contribution - */ -@NonNullByDefault -public class BindingServlet extends HttpServlet { - private static final long serialVersionUID = -1453738923337413163L; - - private final Logger logger = LoggerFactory.getLogger(BindingServlet.class); - - String servletUrlWithoutRoot; - String servletUrl; - HttpService httpService; - - List accountHandlers = new ArrayList<>(); - - public BindingServlet(HttpService httpService) { - this.httpService = httpService; - servletUrlWithoutRoot = "amazonechocontrol"; - servletUrl = "/" + servletUrlWithoutRoot; - try { - httpService.registerServlet(servletUrl, this, null, httpService.createDefaultHttpContext()); - } catch (NamespaceException | ServletException e) { - logger.warn("Register servlet fails", e); - } - } - - public void addAccountThing(Thing accountThing) { - synchronized (accountHandlers) { - accountHandlers.add(accountThing); - } - } - - public void removeAccountThing(Thing accountThing) { - synchronized (accountHandlers) { - accountHandlers.remove(accountThing); - } - } - - public void dispose() { - httpService.unregister(servletUrl); - } - - @Override - protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) - throws ServletException, IOException { - if (req == null) { - return; - } - if (resp == null) { - return; - } - String requestUri = req.getRequestURI(); - if (requestUri == null) { - return; - } - String uri = requestUri.substring(servletUrl.length()); - String queryString = req.getQueryString(); - if (queryString != null && queryString.length() > 0) { - uri += "?" + queryString; - } - logger.debug("doGet {}", uri); - - if (!"/".equals(uri)) { - String newUri = req.getServletPath() + "/"; - resp.sendRedirect(newUri); - return; - } - - StringBuilder html = new StringBuilder(); - html.append("" + HtmlEscape.escapeHtml4(BINDING_NAME) + ""); - html.append("

" + HtmlEscape.escapeHtml4(BINDING_NAME) + "

"); - - synchronized (accountHandlers) { - if (accountHandlers.isEmpty()) { - html.append("No Account thing created."); - } else { - for (Thing accountHandler : accountHandlers) { - String url = URLEncoder.encode(accountHandler.getUID().getId(), "UTF8"); - html.append("" + HtmlEscape.escapeHtml4(accountHandler.getLabel()) - + "
"); - } - } - } - html.append(""); - - resp.addHeader("content-type", "text/html;charset=UTF-8"); - try { - resp.getWriter().write(html.toString()); - } catch (IOException e) { - logger.warn("return html failed with uri syntax error", e); - } - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java deleted file mode 100644 index 0d5478a51e939..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ /dev/null @@ -1,2186 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal; - -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.*; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.io.OutputStream; -import java.net.CookieManager; -import java.net.CookieStore; -import java.net.HttpCookie; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.Random; -import java.util.Scanner; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; -import java.util.zip.GZIPInputStream; - -import javax.net.ssl.HttpsURLConnection; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAnnouncementContent; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAnnouncementTarget; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAutomation; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAutomation.Payload; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBootstrapResult; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBootstrapResult.Authentication; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonEnabledFeeds; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonEqualizer; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonExchangeTokenResponse; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonExchangeTokenResponse.Cookie; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNetworkDetails; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationRequest; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSounds; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationsResponse; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaySearchPhraseOperationPayload; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayValidationResult; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRegisterAppRequest; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Bearer; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.DeviceInfo; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Extensions; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Response; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Success; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Tokens; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonRenewTokenResponse; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroups.SmartHomeGroup; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonStartRoutineRequest; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonUsersMeResponse; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWakeWords; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWakeWords.WakeWord; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWebSiteCookie; -import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice; -import org.openhab.core.common.ThreadPoolManager; -import org.openhab.core.library.types.QuantityType; -import org.openhab.core.library.types.StringType; -import org.openhab.core.library.unit.SIUnits; -import org.openhab.core.util.HexUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonSyntaxException; - -/** - * The {@link Connection} is responsible for the connection to the amazon server - * and handling of the commands - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class Connection { - private static final String THING_THREADPOOL_NAME = "thingHandler"; - private static final long EXPIRES_IN = 432000; // five days - private static final Pattern CHARSET_PATTERN = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)"); - private static final String DEVICE_TYPE = "A2IVLV5VM2W81"; - - private final Logger logger = LoggerFactory.getLogger(Connection.class); - - protected final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(THING_THREADPOOL_NAME); - - private final Random rand = new Random(); - private final CookieManager cookieManager = new CookieManager(); - private final Gson gson; - private final Gson gsonWithNullSerialization; - - private String amazonSite = "amazon.com"; - private String alexaServer = "https://alexa.amazon.com"; - private final String userAgent; - private String frc; - private String serial; - private String deviceId; - - private @Nullable String refreshToken; - private @Nullable Date loginTime; - private @Nullable Date verifyTime; - private long renewTime = 0; - private @Nullable String deviceName; - private @Nullable String accountCustomerId; - private @Nullable String customerName; - - private Map announcements = Collections.synchronizedMap(new LinkedHashMap<>()); - private Map textToSpeeches = Collections.synchronizedMap(new LinkedHashMap<>()); - private Map textCommands = Collections.synchronizedMap(new LinkedHashMap<>()); - - private Map volumes = Collections.synchronizedMap(new LinkedHashMap<>()); - private Map> devices = Collections.synchronizedMap(new LinkedHashMap<>()); - - private final Map> timers = new ConcurrentHashMap<>(); - private final Map locks = new ConcurrentHashMap<>(); - - private enum TimerType { - ANNOUNCEMENT, - TTS, - VOLUME, - DEVICES, - TEXT_COMMAND - } - - public Connection(@Nullable Connection oldConnection, Gson gson) { - this.gson = gson; - String frc = null; - String serial = null; - String deviceId = null; - if (oldConnection != null) { - deviceId = oldConnection.getDeviceId(); - frc = oldConnection.getFrc(); - serial = oldConnection.getSerial(); - } - if (frc != null) { - this.frc = frc; - } else { - // generate frc - byte[] frcBinary = new byte[313]; - rand.nextBytes(frcBinary); - this.frc = Base64.getEncoder().encodeToString(frcBinary); - } - if (serial != null) { - this.serial = serial; - } else { - // generate serial - byte[] serialBinary = new byte[16]; - rand.nextBytes(serialBinary); - this.serial = HexUtils.bytesToHex(serialBinary); - } - if (deviceId != null) { - this.deviceId = deviceId; - } else { - this.deviceId = generateDeviceId(); - } - - // build user agent - this.userAgent = "AmazonWebView/Amazon Alexa/2.2.223830.0/iOS/11.4.1/iPhone"; - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonWithNullSerialization = gsonBuilder.create(); - - replaceTimer(TimerType.DEVICES, - scheduler.scheduleWithFixedDelay(this::handleExecuteSequenceNode, 0, 500, TimeUnit.MILLISECONDS)); - } - - /** - * Generate a new device id - *

- * The device id consists of 16 random bytes in upper-case hex format, a # as separator and a fixed DEVICE_TYPE - * - * @return a string containing the new device-id - */ - private String generateDeviceId() { - byte[] bytes = new byte[16]; - rand.nextBytes(bytes); - String hexStr = HexUtils.bytesToHex(bytes).toUpperCase() + "#" + DEVICE_TYPE; - return HexUtils.bytesToHex(hexStr.getBytes()); - } - - /** - * Check if deviceId is valid (consisting of hex(hex(16 random bytes)) + "#" + DEVICE_TYPE) - * - * @param deviceId the deviceId - * @return true if valid, false if invalid - */ - private boolean checkDeviceIdIsValid(@Nullable String deviceId) { - if (deviceId != null && deviceId.matches("^[0-9a-fA-F]{92}$")) { - String hexString = new String(HexUtils.hexToBytes(deviceId)); - if (hexString.matches("^[0-9A-F]{32}#" + DEVICE_TYPE + "$")) { - return true; - } - } - return false; - } - - private void setAmazonSite(@Nullable String amazonSite) { - String correctedAmazonSite = amazonSite != null ? amazonSite : "amazon.com"; - if (correctedAmazonSite.toLowerCase().startsWith("http://")) { - correctedAmazonSite = correctedAmazonSite.substring(7); - } - if (correctedAmazonSite.toLowerCase().startsWith("https://")) { - correctedAmazonSite = correctedAmazonSite.substring(8); - } - if (correctedAmazonSite.toLowerCase().startsWith("www.")) { - correctedAmazonSite = correctedAmazonSite.substring(4); - } - if (correctedAmazonSite.toLowerCase().startsWith("alexa.")) { - correctedAmazonSite = correctedAmazonSite.substring(6); - } - this.amazonSite = correctedAmazonSite; - alexaServer = "https://alexa." + this.amazonSite; - } - - public @Nullable Date tryGetLoginTime() { - return loginTime; - } - - public @Nullable Date tryGetVerifyTime() { - return verifyTime; - } - - public String getFrc() { - return frc; - } - - public String getSerial() { - return serial; - } - - public String getDeviceId() { - return deviceId; - } - - public String getAmazonSite() { - return amazonSite; - } - - public String getAlexaServer() { - return alexaServer; - } - - public String getDeviceName() { - String deviceName = this.deviceName; - if (deviceName == null) { - return "Unknown"; - } - return deviceName; - } - - public String getCustomerId() { - String customerId = this.accountCustomerId; - if (customerId == null) { - return "Unknown"; - } - return customerId; - } - - public String getCustomerName() { - String customerName = this.customerName; - if (customerName == null) { - return "Unknown"; - } - return customerName; - } - - public boolean isSequenceNodeQueueRunning() { - return devices.values().stream().anyMatch( - (queueObjects) -> (queueObjects.stream().anyMatch(queueObject -> queueObject.future != null))); - } - - public String serializeLoginData() { - Date loginTime = this.loginTime; - if (refreshToken == null || loginTime == null) { - return ""; - } - StringBuilder builder = new StringBuilder(); - builder.append("7\n"); // version - builder.append(frc); - builder.append("\n"); - builder.append(serial); - builder.append("\n"); - builder.append(deviceId); - builder.append("\n"); - builder.append(refreshToken); - builder.append("\n"); - builder.append(amazonSite); - builder.append("\n"); - builder.append(deviceName); - builder.append("\n"); - builder.append(accountCustomerId); - builder.append("\n"); - builder.append(loginTime.getTime()); - builder.append("\n"); - List cookies = cookieManager.getCookieStore().getCookies(); - builder.append(cookies.size()); - builder.append("\n"); - for (HttpCookie cookie : cookies) { - writeValue(builder, cookie.getName()); - writeValue(builder, cookie.getValue()); - writeValue(builder, cookie.getComment()); - writeValue(builder, cookie.getCommentURL()); - writeValue(builder, cookie.getDomain()); - writeValue(builder, cookie.getMaxAge()); - writeValue(builder, cookie.getPath()); - writeValue(builder, cookie.getPortlist()); - writeValue(builder, cookie.getVersion()); - writeValue(builder, cookie.getSecure()); - writeValue(builder, cookie.getDiscard()); - } - return builder.toString(); - } - - private void writeValue(StringBuilder builder, @Nullable Object value) { - if (value == null) { - builder.append('0'); - } else { - builder.append('1'); - builder.append("\n"); - builder.append(value.toString()); - } - builder.append("\n"); - } - - private String readValue(Scanner scanner) { - if (scanner.nextLine().equals("1")) { - String result = scanner.nextLine(); - if (result != null) { - return result; - } - } - return ""; - } - - public boolean tryRestoreLogin(@Nullable String data, @Nullable String overloadedDomain) { - Date loginTime = tryRestoreSessionData(data, overloadedDomain); - if (loginTime != null) { - try { - if (verifyLogin()) { - this.loginTime = loginTime; - return true; - } - } catch (IOException e) { - return false; - } catch (URISyntaxException | InterruptedException e) { - } - } - return false; - } - - private @Nullable Date tryRestoreSessionData(@Nullable String data, @Nullable String overloadedDomain) { - // verify store data - if (data == null || data.isEmpty()) { - return null; - } - Scanner scanner = new Scanner(data); - String version = scanner.nextLine(); - // check if serialize version is supported - if (!"5".equals(version) && !"6".equals(version) && !"7".equals(version)) { - scanner.close(); - return null; - } - int intVersion = Integer.parseInt(version); - - frc = scanner.nextLine(); - serial = scanner.nextLine(); - deviceId = scanner.nextLine(); - - // Recreate session and cookies - refreshToken = scanner.nextLine(); - String domain = scanner.nextLine(); - if (overloadedDomain != null) { - domain = overloadedDomain; - } - setAmazonSite(domain); - - deviceName = scanner.nextLine(); - - if (intVersion > 5) { - String accountCustomerId = scanner.nextLine(); - // Note: version 5 have wrong customer id serialized. - // Only use it, if it at least version 6 of serialization - if (intVersion > 6) { - if (!"null".equals(accountCustomerId)) { - this.accountCustomerId = accountCustomerId; - } - } - } - - Date loginTime = new Date(Long.parseLong(scanner.nextLine())); - CookieStore cookieStore = cookieManager.getCookieStore(); - cookieStore.removeAll(); - - Integer numberOfCookies = Integer.parseInt(scanner.nextLine()); - for (Integer i = 0; i < numberOfCookies; i++) { - String name = readValue(scanner); - String value = readValue(scanner); - - HttpCookie clientCookie = new HttpCookie(name, value); - clientCookie.setComment(readValue(scanner)); - clientCookie.setCommentURL(readValue(scanner)); - clientCookie.setDomain(readValue(scanner)); - clientCookie.setMaxAge(Long.parseLong(readValue(scanner))); - clientCookie.setPath(readValue(scanner)); - clientCookie.setPortlist(readValue(scanner)); - clientCookie.setVersion(Integer.parseInt(readValue(scanner))); - clientCookie.setSecure(Boolean.parseBoolean(readValue(scanner))); - clientCookie.setDiscard(Boolean.parseBoolean(readValue(scanner))); - - cookieStore.add(null, clientCookie); - } - scanner.close(); - try { - checkRenewSession(); - - String accountCustomerId = this.accountCustomerId; - if (accountCustomerId == null || accountCustomerId.isEmpty()) { - List devices = this.getDeviceList(); - accountCustomerId = devices.stream().filter(device -> serial.equals(device.serialNumber)).findAny() - .map(device -> device.deviceOwnerCustomerId).orElse(null); - if (accountCustomerId == null || accountCustomerId.isEmpty()) { - accountCustomerId = devices.stream().filter(device -> "This Device".equals(device.accountName)) - .findAny().map(device -> { - serial = Objects.requireNonNullElse(device.serialNumber, serial); - return device.deviceOwnerCustomerId; - }).orElse(null); - } - this.accountCustomerId = accountCustomerId; - } - } catch (URISyntaxException | IOException | InterruptedException | ConnectionException e) { - logger.debug("Getting account customer Id failed", e); - } - return loginTime; - } - - private @Nullable Authentication tryGetBootstrap() throws IOException, URISyntaxException, InterruptedException { - HttpsURLConnection connection = makeRequest("GET", alexaServer + "/api/bootstrap", null, false, false, null, 0); - String contentType = connection.getContentType(); - if (connection.getResponseCode() == 200 && contentType != null - && contentType.toLowerCase().startsWith("application/json")) { - try { - String bootstrapResultJson = convertStream(connection); - JsonBootstrapResult result = parseJson(bootstrapResultJson, JsonBootstrapResult.class); - Authentication authentication = result.authentication; - if (authentication != null && authentication.authenticated) { - this.customerName = authentication.customerName; - if (this.accountCustomerId == null) { - this.accountCustomerId = authentication.customerId; - } - return authentication; - } - } catch (JsonSyntaxException | IllegalStateException e) { - logger.info("No valid json received", e); - return null; - } - } - return null; - } - - public String convertStream(HttpsURLConnection connection) throws IOException { - InputStream input = connection.getInputStream(); - if (input == null) { - return ""; - } - - InputStream readerStream; - if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) { - readerStream = new GZIPInputStream(connection.getInputStream()); - } else { - readerStream = input; - } - String contentType = connection.getContentType(); - String charSet = null; - if (contentType != null) { - Matcher m = CHARSET_PATTERN.matcher(contentType); - if (m.find()) { - charSet = m.group(1).trim().toUpperCase(); - } - } - - Scanner inputScanner = charSet == null || charSet.isEmpty() - ? new Scanner(readerStream, StandardCharsets.UTF_8.name()) - : new Scanner(readerStream, charSet); - Scanner scannerWithoutDelimiter = inputScanner.useDelimiter("\\A"); - String result = scannerWithoutDelimiter.hasNext() ? scannerWithoutDelimiter.next() : null; - inputScanner.close(); - scannerWithoutDelimiter.close(); - input.close(); - if (result == null) { - result = ""; - } - return result; - } - - public String makeRequestAndReturnString(String url) throws IOException, URISyntaxException, InterruptedException { - return makeRequestAndReturnString("GET", url, null, false, null); - } - - public String makeRequestAndReturnString(String verb, String url, @Nullable String postData, boolean json, - @Nullable Map customHeaders) throws IOException, URISyntaxException, InterruptedException { - HttpsURLConnection connection = makeRequest(verb, url, postData, json, true, customHeaders, 3); - String result = convertStream(connection); - logger.debug("Result of {} {}:{}", verb, url, result); - return result; - } - - public HttpsURLConnection makeRequest(String verb, String url, @Nullable String postData, boolean json, - boolean autoredirect, @Nullable Map customHeaders, int badRequestRepeats) - throws IOException, URISyntaxException, InterruptedException { - String currentUrl = url; - int redirectCounter = 0; - int retryCounter = 0; - // loop for handling redirect and bad request, using automatic redirect is not - // possible, because all response headers must be catched - while (true) { - int code; - HttpsURLConnection connection = null; - try { - logger.debug("Make request to {}", url); - connection = (HttpsURLConnection) new URL(currentUrl).openConnection(); - connection.setRequestMethod(verb); - connection.setRequestProperty("Accept-Language", "en-US"); - if (customHeaders == null || !customHeaders.containsKey("User-Agent")) { - connection.setRequestProperty("User-Agent", userAgent); - } - connection.setRequestProperty("Accept-Encoding", "gzip"); - connection.setRequestProperty("DNT", "1"); - connection.setRequestProperty("Upgrade-Insecure-Requests", "1"); - if (customHeaders != null) { - for (String key : customHeaders.keySet()) { - String value = customHeaders.get(key); - if (value != null && !value.isEmpty()) { - connection.setRequestProperty(key, value); - } - } - } - connection.setInstanceFollowRedirects(false); - - // add cookies - URI uri = connection.getURL().toURI(); - - if (customHeaders == null || !customHeaders.containsKey("Cookie")) { - StringBuilder cookieHeaderBuilder = new StringBuilder(); - for (HttpCookie cookie : cookieManager.getCookieStore().get(uri)) { - if (cookieHeaderBuilder.length() > 0) { - cookieHeaderBuilder.append(";"); - } - cookieHeaderBuilder.append(cookie.getName()); - cookieHeaderBuilder.append("="); - cookieHeaderBuilder.append(cookie.getValue()); - if (cookie.getName().equals("csrf")) { - connection.setRequestProperty("csrf", cookie.getValue()); - } - - } - if (cookieHeaderBuilder.length() > 0) { - String cookies = cookieHeaderBuilder.toString(); - connection.setRequestProperty("Cookie", cookies); - } - } - if (postData != null) { - logger.debug("{}: {}", verb, postData); - // post data - byte[] postDataBytes = postData.getBytes(StandardCharsets.UTF_8); - int postDataLength = postDataBytes.length; - - connection.setFixedLengthStreamingMode(postDataLength); - - if (json) { - connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); - } else { - connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - } - connection.setRequestProperty("Content-Length", Integer.toString(postDataLength)); - if ("POST".equals(verb)) { - connection.setRequestProperty("Expect", "100-continue"); - } - - connection.setDoOutput(true); - OutputStream outputStream = connection.getOutputStream(); - outputStream.write(postDataBytes); - outputStream.close(); - } - // handle result - code = connection.getResponseCode(); - String location = null; - - // handle response headers - Map<@Nullable String, List> headerFields = connection.getHeaderFields(); - for (Map.Entry<@Nullable String, List> header : headerFields.entrySet()) { - String key = header.getKey(); - if (key != null && !key.isEmpty()) { - if ("Set-Cookie".equalsIgnoreCase(key)) { - // store cookie - for (String cookieHeader : header.getValue()) { - if (!cookieHeader.isEmpty()) { - List cookies = HttpCookie.parse(cookieHeader); - for (HttpCookie cookie : cookies) { - cookieManager.getCookieStore().add(uri, cookie); - } - } - } - } - if ("Location".equalsIgnoreCase(key)) { - // get redirect location - location = header.getValue().get(0); - if (!location.isEmpty()) { - location = uri.resolve(location).toString(); - // check for https - if (location.toLowerCase().startsWith("http://")) { - // always use https - location = "https://" + location.substring(7); - logger.debug("Redirect corrected to {}", location); - } - } - } - } - } - if (code == 200) { - logger.debug("Call to {} succeeded", url); - return connection; - } else if (code == 302 && location != null) { - logger.debug("Redirected to {}", location); - redirectCounter++; - if (redirectCounter > 30) { - throw new ConnectionException("Too many redirects"); - } - currentUrl = location; - if (autoredirect) { - continue; // repeat with new location - } - return connection; - } else { - logger.debug("Retry call to {}", url); - retryCounter++; - if (retryCounter > badRequestRepeats) { - throw new HttpException(code, - verb + " url '" + url + "' failed: " + connection.getResponseMessage()); - } - Thread.sleep(2000); - } - } catch (InterruptedException | InterruptedIOException e) { - if (connection != null) { - connection.disconnect(); - } - logger.warn("Unable to wait for next call to {}", url, e); - throw e; - } catch (IOException e) { - if (connection != null) { - connection.disconnect(); - } - logger.warn("Request to url '{}' fails with unknown error", url, e); - throw e; - } catch (Exception e) { - if (connection != null) { - connection.disconnect(); - } - throw e; - } - } - } - - public String registerConnectionAsApp(String oAutRedirectUrl) - throws ConnectionException, IOException, URISyntaxException, InterruptedException { - URI oAutRedirectUri = new URI(oAutRedirectUrl); - - Map queryParameters = new LinkedHashMap<>(); - String query = oAutRedirectUri.getQuery(); - String[] pairs = query.split("&"); - for (String pair : pairs) { - int idx = pair.indexOf("="); - queryParameters.put(URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name()), - URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name())); - } - String accessToken = queryParameters.get("openid.oa2.access_token"); - - Map cookieMap = new HashMap<>(); - - List webSiteCookies = new ArrayList<>(); - for (HttpCookie cookie : getSessionCookies("https://www.amazon.com")) { - cookieMap.put(cookie.getName(), cookie.getValue()); - webSiteCookies.add(new JsonWebSiteCookie(cookie.getName(), cookie.getValue())); - } - - JsonRegisterAppRequest registerAppRequest = new JsonRegisterAppRequest(serial, accessToken, frc, - webSiteCookies); - String registerAppRequestJson = gson.toJson(registerAppRequest); - - HashMap registerHeaders = new HashMap<>(); - registerHeaders.put("x-amzn-identity-auth-domain", "api.amazon.com"); - - String registerAppResultJson = makeRequestAndReturnString("POST", "https://api.amazon.com/auth/register", - registerAppRequestJson, true, registerHeaders); - JsonRegisterAppResponse registerAppResponse = parseJson(registerAppResultJson, JsonRegisterAppResponse.class); - - Response response = registerAppResponse.response; - if (response == null) { - throw new ConnectionException("Error: No response received from register application"); - } - Success success = response.success; - if (success == null) { - throw new ConnectionException("Error: No success received from register application"); - } - Tokens tokens = success.tokens; - if (tokens == null) { - throw new ConnectionException("Error: No tokens received from register application"); - } - Bearer bearer = tokens.bearer; - if (bearer == null) { - throw new ConnectionException("Error: No bearer received from register application"); - } - String refreshToken = bearer.refreshToken; - this.refreshToken = refreshToken; - if (refreshToken == null || refreshToken.isEmpty()) { - throw new ConnectionException("Error: No refresh token received"); - } - try { - exchangeToken(); - // Check which is the owner domain - String usersMeResponseJson = makeRequestAndReturnString("GET", - "https://alexa.amazon.com/api/users/me?platform=ios&version=2.2.223830.0", null, false, null); - JsonUsersMeResponse usersMeResponse = parseJson(usersMeResponseJson, JsonUsersMeResponse.class); - URI uri = new URI(usersMeResponse.marketPlaceDomainName); - String host = uri.getHost(); - - // Switch to owner domain - setAmazonSite(host); - exchangeToken(); - tryGetBootstrap(); - } catch (Exception e) { - logout(); - throw e; - } - String deviceName = null; - Extensions extensions = success.extensions; - if (extensions != null) { - DeviceInfo deviceInfo = extensions.deviceInfo; - if (deviceInfo != null) { - deviceName = deviceInfo.deviceName; - } - } - if (deviceName == null) { - deviceName = "Unknown"; - } - this.deviceName = deviceName; - return deviceName; - } - - private void exchangeToken() throws IOException, URISyntaxException, InterruptedException { - this.renewTime = 0; - String cookiesJson = "{\"cookies\":{\"." + getAmazonSite() + "\":[]}}"; - String cookiesBase64 = Base64.getEncoder().encodeToString(cookiesJson.getBytes()); - - String exchangePostData = "di.os.name=iOS&app_version=2.2.223830.0&domain=." + getAmazonSite() - + "&source_token=" + URLEncoder.encode(this.refreshToken, "UTF8") - + "&requested_token_type=auth_cookies&source_token_type=refresh_token&di.hw.version=iPhone&di.sdk.version=6.10.0&cookies=" - + cookiesBase64 + "&app_name=Amazon%20Alexa&di.os.version=11.4.1"; - - HashMap exchangeTokenHeader = new HashMap<>(); - exchangeTokenHeader.put("Cookie", ""); - - String exchangeTokenJson = makeRequestAndReturnString("POST", - "https://www." + getAmazonSite() + "/ap/exchangetoken", exchangePostData, false, exchangeTokenHeader); - JsonExchangeTokenResponse exchangeTokenResponse = Objects - .requireNonNull(gson.fromJson(exchangeTokenJson, JsonExchangeTokenResponse.class)); - - org.openhab.binding.amazonechocontrol.internal.jsons.JsonExchangeTokenResponse.Response response = exchangeTokenResponse.response; - if (response != null) { - org.openhab.binding.amazonechocontrol.internal.jsons.JsonExchangeTokenResponse.Tokens tokens = response.tokens; - if (tokens != null) { - Map cookiesMap = tokens.cookies; - if (cookiesMap != null) { - for (String domain : cookiesMap.keySet()) { - Cookie[] cookies = cookiesMap.get(domain); - if (cookies != null) { - for (Cookie cookie : cookies) { - if (cookie != null) { - HttpCookie httpCookie = new HttpCookie(cookie.name, cookie.value); - httpCookie.setPath(cookie.path); - httpCookie.setDomain(domain); - Boolean secure = cookie.secure; - if (secure != null) { - httpCookie.setSecure(secure); - } - this.cookieManager.getCookieStore().add(null, httpCookie); - } - } - } - } - } - } - } - if (!verifyLogin()) { - throw new ConnectionException("Verify login failed after token exchange"); - } - this.renewTime = (long) (System.currentTimeMillis() + Connection.EXPIRES_IN * 1000d / 0.8d); // start renew at - } - - public boolean checkRenewSession() throws URISyntaxException, IOException, InterruptedException { - if (System.currentTimeMillis() >= this.renewTime) { - String renewTokenPostData = "app_name=Amazon%20Alexa&app_version=2.2.223830.0&di.sdk.version=6.10.0&source_token=" - + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8.name()) - + "&package_name=com.amazon.echo&di.hw.version=iPhone&platform=iOS&requested_token_type=access_token&source_token_type=refresh_token&di.os.name=iOS&di.os.version=11.4.1¤t_version=6.10.0"; - String renewTokenResponseJson = makeRequestAndReturnString("POST", "https://api.amazon.com/auth/token", - renewTokenPostData, false, null); - parseJson(renewTokenResponseJson, JsonRenewTokenResponse.class); - - exchangeToken(); - return true; - } - return false; - } - - public boolean getIsLoggedIn() { - return loginTime != null; - } - - public String getLoginPage() throws IOException, URISyntaxException, InterruptedException { - // clear session data - logout(); - - logger.debug("Start Login to {}", alexaServer); - - if (!checkDeviceIdIsValid(deviceId)) { - deviceId = generateDeviceId(); - logger.debug("Generating new device id (old device id had invalid format)."); - } - - String mapMdJson = "{\"device_user_dictionary\":[],\"device_registration_data\":{\"software_version\":\"1\"},\"app_identifier\":{\"app_version\":\"2.2.223830\",\"bundle_id\":\"com.amazon.echo\"}}"; - String mapMdCookie = Base64.getEncoder().encodeToString(mapMdJson.getBytes()); - - cookieManager.getCookieStore().add(new URI("https://www.amazon.com"), new HttpCookie("map-md", mapMdCookie)); - cookieManager.getCookieStore().add(new URI("https://www.amazon.com"), new HttpCookie("frc", frc)); - - Map customHeaders = new HashMap<>(); - customHeaders.put("authority", "www.amazon.com"); - String loginFormHtml = makeRequestAndReturnString("GET", "https://www.amazon.com" - + "/ap/signin?openid.return_to=https://www.amazon.com/ap/maplanding&openid.assoc_handle=amzn_dp_project_dee_ios&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&pageId=amzn_dp_project_dee_ios&accountStatusPolicy=P1&openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns.oa2=http://www.amazon.com/ap/ext/oauth/2&openid.oa2.client_id=device:" - + deviceId - + "&openid.ns.pape=http://specs.openid.net/extensions/pape/1.0&openid.oa2.response_type=token&openid.ns=http://specs.openid.net/auth/2.0&openid.pape.max_auth_age=0&openid.oa2.scope=device_auth_access", - null, false, customHeaders); - - logger.debug("Received login form {}", loginFormHtml); - return loginFormHtml; - } - - public boolean verifyLogin() throws IOException, URISyntaxException, InterruptedException { - if (this.refreshToken == null) { - return false; - } - Authentication authentication = tryGetBootstrap(); - if (authentication != null && authentication.authenticated) { - verifyTime = new Date(); - if (loginTime == null) { - loginTime = verifyTime; - } - return true; - } - return false; - } - - public List getSessionCookies() { - try { - return cookieManager.getCookieStore().get(new URI(alexaServer)); - } catch (URISyntaxException e) { - return new ArrayList<>(); - } - } - - public List getSessionCookies(String server) { - try { - return cookieManager.getCookieStore().get(new URI(server)); - } catch (URISyntaxException e) { - return new ArrayList<>(); - } - } - - @SuppressWarnings("null") // current value in compute can be null - private void replaceTimer(TimerType type, @Nullable ScheduledFuture newTimer) { - timers.compute(type, (timerType, oldTimer) -> { - if (oldTimer != null) { - oldTimer.cancel(true); - } - return newTimer; - }); - } - - public void logout() { - cookieManager.getCookieStore().removeAll(); - // reset all members - refreshToken = null; - loginTime = null; - verifyTime = null; - deviceName = null; - - replaceTimer(TimerType.ANNOUNCEMENT, null); - announcements.clear(); - replaceTimer(TimerType.TTS, null); - textToSpeeches.clear(); - replaceTimer(TimerType.VOLUME, null); - volumes.clear(); - replaceTimer(TimerType.DEVICES, null); - textCommands.clear(); - replaceTimer(TimerType.TTS, null); - - devices.values().forEach((queueObjects) -> { - queueObjects.forEach((queueObject) -> { - Future future = queueObject.future; - if (future != null) { - future.cancel(true); - queueObject.future = null; - } - }); - }); - } - - // parser - private T parseJson(String json, Class type) throws JsonSyntaxException, IllegalStateException { - try { - // gson.fromJson is always non-null if json is non-null - return Objects.requireNonNull(gson.fromJson(json, type)); - } catch (JsonParseException | IllegalStateException e) { - logger.warn("Parsing json failed: {}", json, e); - throw e; - } - } - - // commands and states - public List getWakeWords() { - String json; - try { - json = makeRequestAndReturnString(alexaServer + "/api/wake-word?cached=true"); - JsonWakeWords wakeWords = parseJson(json, JsonWakeWords.class); - return Objects.requireNonNullElse(wakeWords.wakeWords, List.of()); - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.info("getting wakewords failed", e); - } - return List.of(); - } - - public List getSmarthomeDeviceList() - throws IOException, URISyntaxException, InterruptedException { - try { - String json = makeRequestAndReturnString(alexaServer + "/api/phoenix"); - logger.debug("getSmartHomeDevices result: {}", json); - - JsonNetworkDetails networkDetails = parseJson(json, JsonNetworkDetails.class); - Object jsonObject = gson.fromJson(networkDetails.networkDetail, Object.class); - List result = new ArrayList<>(); - searchSmartHomeDevicesRecursive(jsonObject, result); - - return result; - } catch (Exception e) { - logger.warn("getSmartHomeDevices fails: {}", e.getMessage()); - throw e; - } - } - - private void searchSmartHomeDevicesRecursive(@Nullable Object jsonNode, List devices) { - if (jsonNode instanceof Map) { - @SuppressWarnings("rawtypes") - Map map = (Map) jsonNode; - if (map.containsKey("entityId") && map.containsKey("friendlyName") && map.containsKey("actions")) { - // device node found, create type element and add it to the results - JsonElement element = gson.toJsonTree(jsonNode); - SmartHomeDevice shd = parseJson(element.toString(), SmartHomeDevice.class); - devices.add(shd); - } else if (map.containsKey("applianceGroupName")) { - JsonElement element = gson.toJsonTree(jsonNode); - SmartHomeGroup shg = parseJson(element.toString(), SmartHomeGroup.class); - devices.add(shg); - } else { - map.values().forEach(value -> searchSmartHomeDevicesRecursive(value, devices)); - } - } - } - - public List getDeviceList() throws IOException, URISyntaxException, InterruptedException { - JsonDevices devices = Objects.requireNonNull(parseJson(getDeviceListJson(), JsonDevices.class)); - logger.trace("Devices {}", devices.devices); - - // @Nullable because of a limitation of the null-checker, we filter null-serialNumbers before - Set<@Nullable String> serialNumbers = ConcurrentHashMap.newKeySet(); - return devices.devices.stream().filter(d -> d.serialNumber != null && serialNumbers.add(d.serialNumber)) - .collect(Collectors.toList()); - } - - public String getDeviceListJson() throws IOException, URISyntaxException, InterruptedException { - String json = makeRequestAndReturnString(alexaServer + "/api/devices-v2/device?cached=false"); - return json; - } - - public Map getSmartHomeDeviceStatesJson(Set devices) - throws IOException, URISyntaxException, InterruptedException { - JsonObject requestObject = new JsonObject(); - JsonArray stateRequests = new JsonArray(); - Map mergedApplianceMap = new HashMap<>(); - for (SmartHomeBaseDevice device : devices) { - String applianceId = device.findId(); - if (applianceId != null) { - JsonObject stateRequest; - if (device instanceof SmartHomeDevice && ((SmartHomeDevice) device).mergedApplianceIds != null) { - List mergedApplianceIds = Objects - .requireNonNullElse(((SmartHomeDevice) device).mergedApplianceIds, List.of()); - for (String idToMerge : mergedApplianceIds) { - mergedApplianceMap.put(idToMerge, applianceId); - stateRequest = new JsonObject(); - stateRequest.addProperty("entityId", idToMerge); - stateRequest.addProperty("entityType", "APPLIANCE"); - stateRequests.add(stateRequest); - } - } else { - stateRequest = new JsonObject(); - stateRequest.addProperty("entityId", applianceId); - stateRequest.addProperty("entityType", "APPLIANCE"); - stateRequests.add(stateRequest); - } - } - } - requestObject.add("stateRequests", stateRequests); - String requestBody = requestObject.toString(); - String json = makeRequestAndReturnString("POST", alexaServer + "/api/phoenix/state", requestBody, true, null); - logger.debug("Requested {} and received {}", requestBody, json); - - JsonObject responseObject = Objects.requireNonNull(gson.fromJson(json, JsonObject.class)); - JsonArray deviceStates = (JsonArray) responseObject.get("deviceStates"); - Map result = new HashMap<>(); - for (JsonElement deviceState : deviceStates) { - JsonObject deviceStateObject = deviceState.getAsJsonObject(); - JsonObject entity = deviceStateObject.get("entity").getAsJsonObject(); - String applianceId = entity.get("entityId").getAsString(); - JsonElement capabilityState = deviceStateObject.get("capabilityStates"); - if (capabilityState != null && capabilityState.isJsonArray()) { - String realApplianceId = mergedApplianceMap.get(applianceId); - if (realApplianceId != null) { - var capabilityArray = result.get(realApplianceId); - if (capabilityArray != null) { - capabilityArray.addAll(capabilityState.getAsJsonArray()); - result.put(realApplianceId, capabilityArray); - } else { - result.put(realApplianceId, capabilityState.getAsJsonArray()); - } - } else { - result.put(applianceId, capabilityState.getAsJsonArray()); - } - } - } - return result; - } - - public @Nullable JsonPlayerState getPlayer(Device device) - throws IOException, URISyntaxException, InterruptedException { - String json = makeRequestAndReturnString(alexaServer + "/api/np/player?deviceSerialNumber=" - + device.serialNumber + "&deviceType=" + device.deviceType + "&screenWidth=1440"); - JsonPlayerState playerState = parseJson(json, JsonPlayerState.class); - return playerState; - } - - public @Nullable JsonMediaState getMediaState(Device device) - throws IOException, URISyntaxException, InterruptedException { - String json = makeRequestAndReturnString(alexaServer + "/api/media/state?deviceSerialNumber=" - + device.serialNumber + "&deviceType=" + device.deviceType); - JsonMediaState mediaState = parseJson(json, JsonMediaState.class); - return mediaState; - } - - public List getActivities(int number, @Nullable Long startTime) { - try { - String json = makeRequestAndReturnString(alexaServer + "/api/activities?startTime=" - + (startTime != null ? startTime : "") + "&size=" + number + "&offset=1"); - JsonActivities activities = parseJson(json, JsonActivities.class); - return Objects.requireNonNullElse(activities.activities, List.of()); - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.info("getting activities failed", e); - } - return List.of(); - } - - public @Nullable JsonBluetoothStates getBluetoothConnectionStates() { - String json; - try { - json = makeRequestAndReturnString(alexaServer + "/api/bluetooth?cached=true"); - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.debug("failed to get bluetooth state: {}", e.getMessage()); - return new JsonBluetoothStates(); - } - JsonBluetoothStates bluetoothStates = parseJson(json, JsonBluetoothStates.class); - return bluetoothStates; - } - - public @Nullable JsonPlaylists getPlaylists(Device device) - throws IOException, URISyntaxException, InterruptedException { - String json = makeRequestAndReturnString( - alexaServer + "/api/cloudplayer/playlists?deviceSerialNumber=" + device.serialNumber + "&deviceType=" - + device.deviceType + "&mediaOwnerCustomerId=" + getCustomerId(device.deviceOwnerCustomerId)); - JsonPlaylists playlists = parseJson(json, JsonPlaylists.class); - return playlists; - } - - public void command(Device device, String command) throws IOException, URISyntaxException, InterruptedException { - String url = alexaServer + "/api/np/command?deviceSerialNumber=" + device.serialNumber + "&deviceType=" - + device.deviceType; - makeRequest("POST", url, command, true, true, null, 0); - } - - public void smartHomeCommand(String entityId, String action) throws IOException, InterruptedException { - smartHomeCommand(entityId, action, null, null); - } - - public void smartHomeCommand(String entityId, String action, @Nullable String property, @Nullable Object value) - throws IOException, InterruptedException { - String url = alexaServer + "/api/phoenix/state"; - Float lowerSetpoint = null; - Float upperSetpoint = null; - - JsonObject json = new JsonObject(); - JsonArray controlRequests = new JsonArray(); - JsonObject controlRequest = new JsonObject(); - controlRequest.addProperty("entityId", entityId); - controlRequest.addProperty("entityType", "APPLIANCE"); - JsonObject parameters = new JsonObject(); - parameters.addProperty("action", action); - if (property != null) { - if ("setThermostatMode".equals(action)) { - if (value instanceof StringType) { - parameters.addProperty(property + ".value", value.toString()); - } - } else if ("setTargetTemperature".equals(action)) { - if ("targetTemperature".equals(property)) { - if (value instanceof QuantityType) { - parameters.addProperty(property + ".value", ((QuantityType) value).floatValue()); - parameters.addProperty(property + ".scale", - ((QuantityType) value).getUnit().equals(SIUnits.CELSIUS) ? "celsius" : "fahrenheit"); - } - } else { - // Get current upper and lower setpoints to build command syntax - Map devices = null; - try { - List deviceList = getSmarthomeDeviceList().stream() - .filter(device -> entityId.equals(device.findEntityId())).collect(Collectors.toList()); - devices = getSmartHomeDeviceStatesJson(new HashSet<>(deviceList)); - } catch (URISyntaxException e) { - logger.debug("{}", e.toString()); - } - Entry entry = devices.entrySet().iterator().next(); - JsonArray states = entry.getValue(); - for (JsonElement stateElement : states) { - JsonObject stateValue = new JsonObject(); - String stateJson = stateElement.getAsString(); - if (stateJson.startsWith("{") && stateJson.endsWith("}")) { - JsonObject state = Objects.requireNonNull(gson.fromJson(stateJson, JsonObject.class)); - String interfaceName = Objects.requireNonNullElse(state.get("namespace"), JsonNull.INSTANCE) - .getAsString(); - String name = Objects.requireNonNullElse(state.get("name"), JsonNull.INSTANCE) - .getAsString(); - if ("Alexa.ThermostatController".equals(interfaceName)) { - if ("upperSetpoint".equals(name)) { - stateValue = Objects.requireNonNullElse(state.get("value"), JsonNull.INSTANCE) - .getAsJsonObject(); - upperSetpoint = Objects - .requireNonNullElse(stateValue.get("value"), JsonNull.INSTANCE) - .getAsFloat(); - } else if ("lowerSetpoint".equals(name)) { - stateValue = Objects.requireNonNullElse(state.get("value"), JsonNull.INSTANCE) - .getAsJsonObject(); - lowerSetpoint = Objects - .requireNonNullElse(stateValue.get("value"), JsonNull.INSTANCE) - .getAsFloat(); - } - } - } - } - if ("lowerSetTemperature".equals(property)) { - if (value instanceof QuantityType) { - parameters.addProperty("upperSetTemperature.value", upperSetpoint); - parameters.addProperty("upperSetTemperature.scale", - ((QuantityType) value).getUnit().equals(SIUnits.CELSIUS) ? "celsius" - : "fahrenheit"); - parameters.addProperty(property + ".value", ((QuantityType) value).floatValue()); - parameters.addProperty(property + ".scale", - ((QuantityType) value).getUnit().equals(SIUnits.CELSIUS) ? "celsius" - : "fahrenheit"); - } - } else if ("upperSetTemperature".equals(property)) { - if (value instanceof QuantityType) { - parameters.addProperty(property + ".value", ((QuantityType) value).floatValue()); - parameters.addProperty(property + ".scale", - ((QuantityType) value).getUnit().equals(SIUnits.CELSIUS) ? "celsius" - : "fahrenheit"); - parameters.addProperty("lowerSetTemperature.value", lowerSetpoint); - parameters.addProperty("lowerSetTemperature.scale", - ((QuantityType) value).getUnit().equals(SIUnits.CELSIUS) ? "celsius" - : "fahrenheit"); - } - } - } - } else { - if (value instanceof QuantityType) { - parameters.addProperty(property + ".value", ((QuantityType) value).floatValue()); - parameters.addProperty(property + ".scale", - ((QuantityType) value).getUnit().equals(SIUnits.CELSIUS) ? "celsius" : "fahrenheit"); - } else if (value instanceof Boolean) { - parameters.addProperty(property, (boolean) value); - } else if (value instanceof String) { - parameters.addProperty(property, (String) value); - } else if (value instanceof Number) { - parameters.addProperty(property, (Number) value); - } else if (value instanceof Character) { - parameters.addProperty(property, (Character) value); - } else if (value instanceof JsonElement) { - parameters.add(property, (JsonElement) value); - } - } - } - controlRequest.add("parameters", parameters); - controlRequests.add(controlRequest); - json.add("controlRequests", controlRequests); - - String requestBody = json.toString(); - try { - String resultBody = makeRequestAndReturnString("PUT", url, requestBody, true, null); - logger.trace("Request '{}' resulted in '{}", requestBody, resultBody); - JsonObject result = parseJson(resultBody, JsonObject.class); - JsonElement errors = result.get("errors"); - if (errors != null && errors.isJsonArray()) { - JsonArray errorList = errors.getAsJsonArray(); - if (errorList.size() > 0) { - logger.warn("Smart home device command failed. The request '{}' resulted in error(s): {}", - requestBody, StreamSupport.stream(errorList.spliterator(), false).map(JsonElement::toString) - .collect(Collectors.joining(" / "))); - } - } - } catch (URISyntaxException e) { - logger.warn("URL '{}' has invalid format for request '{}': {}", url, requestBody, e.getMessage()); - } - } - - public void notificationVolume(Device device, int volume) - throws IOException, URISyntaxException, InterruptedException { - String url = alexaServer + "/api/device-notification-state/" + device.deviceType + "/" + device.softwareVersion - + "/" + device.serialNumber; - String command = "{\"deviceSerialNumber\":\"" + device.serialNumber + "\",\"deviceType\":\"" + device.deviceType - + "\",\"softwareVersion\":\"" + device.softwareVersion + "\",\"volumeLevel\":" + volume + "}"; - makeRequest("PUT", url, command, true, true, null, 0); - } - - public void ascendingAlarm(Device device, boolean ascendingAlarm) - throws IOException, URISyntaxException, InterruptedException { - String url = alexaServer + "/api/ascending-alarm/" + device.serialNumber; - String command = "{\"ascendingAlarmEnabled\":" + (ascendingAlarm ? "true" : "false") - + ",\"deviceSerialNumber\":\"" + device.serialNumber + "\",\"deviceType\":\"" + device.deviceType - + "\",\"deviceAccountId\":null}"; - makeRequest("PUT", url, command, true, true, null, 0); - } - - public List getDeviceNotificationStates() { - try { - String json = makeRequestAndReturnString(alexaServer + "/api/device-notification-state"); - JsonDeviceNotificationState result = parseJson(json, JsonDeviceNotificationState.class); - return Objects.requireNonNullElse(result.deviceNotificationStates, List.of()); - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.info("Error getting device notification states", e); - } - return List.of(); - } - - public List getAscendingAlarm() { - String json; - try { - json = makeRequestAndReturnString(alexaServer + "/api/ascending-alarm"); - JsonAscendingAlarm result = parseJson(json, JsonAscendingAlarm.class); - return Objects.requireNonNullElse(result.ascendingAlarmModelList, List.of()); - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.info("Error getting device notification states", e); - } - return List.of(); - } - - public void bluetooth(Device device, @Nullable String address) - throws IOException, URISyntaxException, InterruptedException { - if (address == null || address.isEmpty()) { - // disconnect - makeRequest("POST", - alexaServer + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" + device.serialNumber, "", - true, true, null, 0); - } else { - makeRequest("POST", - alexaServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber, - "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true, true, null, 0); - } - } - - private @Nullable String getCustomerId(@Nullable String defaultId) { - String accountCustomerId = this.accountCustomerId; - return accountCustomerId == null || accountCustomerId.isEmpty() ? defaultId : accountCustomerId; - } - - public void playRadio(Device device, @Nullable String stationId) - throws IOException, URISyntaxException, InterruptedException { - if (stationId == null || stationId.isEmpty()) { - command(device, "{\"type\":\"PauseCommand\"}"); - } else { - makeRequest("POST", - alexaServer + "/api/tunein/queue-and-play?deviceSerialNumber=" + device.serialNumber - + "&deviceType=" + device.deviceType + "&guideId=" + stationId - + "&contentType=station&callSign=&mediaOwnerCustomerId=" - + getCustomerId(device.deviceOwnerCustomerId), - "", true, true, null, 0); - } - } - - public void playAmazonMusicTrack(Device device, @Nullable String trackId) - throws IOException, URISyntaxException, InterruptedException { - if (trackId == null || trackId.isEmpty()) { - command(device, "{\"type\":\"PauseCommand\"}"); - } else { - String command = "{\"trackId\":\"" + trackId + "\",\"playQueuePrime\":true}"; - makeRequest("POST", - alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber - + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" - + getCustomerId(device.deviceOwnerCustomerId) + "&shuffle=false", - command, true, true, null, 0); - } - } - - public void playAmazonMusicPlayList(Device device, @Nullable String playListId) - throws IOException, URISyntaxException, InterruptedException { - if (playListId == null || playListId.isEmpty()) { - command(device, "{\"type\":\"PauseCommand\"}"); - } else { - String command = "{\"playlistId\":\"" + playListId + "\",\"playQueuePrime\":true}"; - makeRequest("POST", - alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber - + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" - + getCustomerId(device.deviceOwnerCustomerId) + "&shuffle=false", - command, true, true, null, 0); - } - } - - public void announcement(Device device, String speak, String bodyText, @Nullable String title, - @Nullable Integer ttsVolume, @Nullable Integer standardVolume) { - String plainSpeak = speak.replaceAll("<.+?>", " ").replaceAll("\\s+", " ").trim(); - String plainBody = bodyText.replaceAll("<.+?>", " ").replaceAll("\\s+", " ").trim(); - - if (plainSpeak.isEmpty() && plainBody.isEmpty()) { - // if there is neither a bodytext nor (except tags) a speaktext, we have nothing to announce - return; - } - - // we lock announcements until we have finished adding this one - Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.ANNOUNCEMENT, k -> new ReentrantLock())); - lock.lock(); - try { - AnnouncementWrapper announcement = Objects.requireNonNull(announcements.computeIfAbsent( - Objects.hash(speak, plainBody, title), k -> new AnnouncementWrapper(speak, plainBody, title))); - announcement.devices.add(device); - announcement.ttsVolumes.add(ttsVolume); - announcement.standardVolumes.add(standardVolume); - - // schedule an announcement only if it has not been scheduled before - timers.computeIfAbsent(TimerType.ANNOUNCEMENT, - k -> scheduler.schedule(this::sendAnnouncement, 500, TimeUnit.MILLISECONDS)); - } finally { - lock.unlock(); - } - } - - private void sendAnnouncement() { - // we lock new announcements until we have dispatched everything - Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.ANNOUNCEMENT, k -> new ReentrantLock())); - lock.lock(); - try { - Iterator iterator = announcements.values().iterator(); - while (iterator.hasNext()) { - AnnouncementWrapper announcement = iterator.next(); - try { - List devices = announcement.devices; - if (!devices.isEmpty()) { - JsonAnnouncementContent content = new JsonAnnouncementContent(announcement); - - Map parameters = new HashMap<>(); - parameters.put("expireAfter", "PT5S"); - parameters.put("content", new JsonAnnouncementContent[] { content }); - parameters.put("target", new JsonAnnouncementTarget(devices)); - - String customerId = getCustomerId(devices.get(0).deviceOwnerCustomerId); - if (customerId != null) { - parameters.put("customerId", customerId); - } - executeSequenceCommandWithVolume(devices, "AlexaAnnouncement", parameters, - announcement.ttsVolumes, announcement.standardVolumes); - } - } catch (Exception e) { - logger.warn("send announcement fails with unexpected error", e); - } - iterator.remove(); - } - } finally { - // the timer is done anyway immediately after we unlock - timers.remove(TimerType.ANNOUNCEMENT); - lock.unlock(); - } - } - - public void textToSpeech(Device device, String text, @Nullable Integer ttsVolume, - @Nullable Integer standardVolume) { - if (text.replaceAll("<.+?>", "").replaceAll("\\s+", " ").trim().isEmpty()) { - return; - } - - // we lock TTS until we have finished adding this one - Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TTS, k -> new ReentrantLock())); - lock.lock(); - try { - TextToSpeech textToSpeech = Objects - .requireNonNull(textToSpeeches.computeIfAbsent(Objects.hash(text), k -> new TextToSpeech(text))); - textToSpeech.devices.add(device); - textToSpeech.ttsVolumes.add(ttsVolume); - textToSpeech.standardVolumes.add(standardVolume); - // schedule a TTS only if it has not been scheduled before - timers.computeIfAbsent(TimerType.TTS, - k -> scheduler.schedule(this::sendTextToSpeech, 500, TimeUnit.MILLISECONDS)); - } finally { - lock.unlock(); - } - } - - private void sendTextToSpeech() { - // we lock new TTS until we have dispatched everything - Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TTS, k -> new ReentrantLock())); - lock.lock(); - try { - Iterator iterator = textToSpeeches.values().iterator(); - while (iterator.hasNext()) { - TextToSpeech textToSpeech = iterator.next(); - try { - List devices = textToSpeech.devices; - if (!devices.isEmpty()) { - String text = textToSpeech.text; - Map parameters = Map.of("textToSpeak", text); - executeSequenceCommandWithVolume(devices, "Alexa.Speak", parameters, textToSpeech.ttsVolumes, - textToSpeech.standardVolumes); - } - } catch (Exception e) { - logger.warn("send textToSpeech fails with unexpected error", e); - } - iterator.remove(); - } - } finally { - // the timer is done anyway immediately after we unlock - timers.remove(TimerType.TTS); - lock.unlock(); - } - } - - public void textCommand(Device device, String text, @Nullable Integer ttsVolume, @Nullable Integer standardVolume) { - if (text.replaceAll("<.+?>", "").replaceAll("\\s+", " ").trim().isEmpty()) { - return; - } - - // we lock TextCommands until we have finished adding this one - Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TEXT_COMMAND, k -> new ReentrantLock())); - lock.lock(); - try { - TextCommand textCommand = Objects - .requireNonNull(textCommands.computeIfAbsent(Objects.hash(text), k -> new TextCommand(text))); - textCommand.devices.add(device); - textCommand.ttsVolumes.add(ttsVolume); - textCommand.standardVolumes.add(standardVolume); - // schedule a TextCommand only if it has not been scheduled before - timers.computeIfAbsent(TimerType.TEXT_COMMAND, - k -> scheduler.schedule(this::sendTextCommand, 500, TimeUnit.MILLISECONDS)); - } finally { - lock.unlock(); - } - } - - private synchronized void sendTextCommand() { - // we lock new TTS until we have dispatched everything - Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TEXT_COMMAND, k -> new ReentrantLock())); - lock.lock(); - - try { - Iterator iterator = textCommands.values().iterator(); - while (iterator.hasNext()) { - TextCommand textCommand = iterator.next(); - try { - List devices = textCommand.devices; - if (!devices.isEmpty()) { - String text = textCommand.text; - Map parameters = Map.of("text", text); - executeSequenceCommandWithVolume(devices, "Alexa.TextCommand", parameters, - textCommand.ttsVolumes, textCommand.standardVolumes); - } - } catch (Exception e) { - logger.warn("send textCommand fails with unexpected error", e); - } - iterator.remove(); - } - } finally { - // the timer is done anyway immediately after we unlock - timers.remove(TimerType.TEXT_COMMAND); - lock.unlock(); - } - } - - public void volume(Device device, int vol) { - // we lock volume until we have finished adding this one - Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.VOLUME, k -> new ReentrantLock())); - lock.lock(); - try { - Volume volume = Objects.requireNonNull(volumes.computeIfAbsent(vol, k -> new Volume(vol))); - volume.devices.add(device); - volume.volumes.add(vol); - // schedule a TTS only if it has not been scheduled before - timers.computeIfAbsent(TimerType.VOLUME, - k -> scheduler.schedule(this::sendVolume, 500, TimeUnit.MILLISECONDS)); - } finally { - lock.unlock(); - } - } - - private void sendVolume() { - // we lock new volume until we have dispatched everything - Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.VOLUME, k -> new ReentrantLock())); - lock.lock(); - try { - Iterator iterator = volumes.values().iterator(); - while (iterator.hasNext()) { - Volume volume = iterator.next(); - try { - List devices = volume.devices; - if (!devices.isEmpty()) { - executeSequenceCommandWithVolume(devices, null, Map.of(), volume.volumes, List.of()); - } - } catch (Exception e) { - logger.warn("send volume fails with unexpected error", e); - } - iterator.remove(); - } - } finally { - // the timer is done anyway immediately after we unlock - timers.remove(TimerType.VOLUME); - lock.unlock(); - } - } - - private void executeSequenceCommandWithVolume(List devices, @Nullable String command, - Map parameters, List<@Nullable Integer> ttsVolumes, - List<@Nullable Integer> standardVolumes) { - JsonArray serialNodesToExecute = new JsonArray(); - JsonArray ttsVolumeNodesToExecute = new JsonArray(); - for (int i = 0; i < devices.size(); i++) { - Integer ttsVolume = ttsVolumes.size() > i ? ttsVolumes.get(i) : null; - Integer standardVolume = standardVolumes.size() > i ? standardVolumes.get(i) : null; - if (ttsVolume != null && (standardVolume != null || !ttsVolume.equals(standardVolume))) { - ttsVolumeNodesToExecute.add( - createExecutionNode(devices.get(i), "Alexa.DeviceControls.Volume", Map.of("value", ttsVolume))); - } - } - if (ttsVolumeNodesToExecute.size() > 0) { - JsonObject parallelNodesToExecute = new JsonObject(); - parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode"); - parallelNodesToExecute.add("nodesToExecute", ttsVolumeNodesToExecute); - serialNodesToExecute.add(parallelNodesToExecute); - } - - if (command != null && !parameters.isEmpty()) { - JsonArray commandNodesToExecute = new JsonArray(); - if ("Alexa.Speak".equals(command) || "Alexa.TextCommand".equals(command)) { - for (Device device : devices) { - commandNodesToExecute.add(createExecutionNode(device, command, parameters)); - } - } else { - commandNodesToExecute.add(createExecutionNode(devices.get(0), command, parameters)); - } - if (commandNodesToExecute.size() > 0) { - JsonObject parallelNodesToExecute = new JsonObject(); - parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode"); - parallelNodesToExecute.add("nodesToExecute", commandNodesToExecute); - serialNodesToExecute.add(parallelNodesToExecute); - } - } - - JsonArray standardVolumeNodesToExecute = new JsonArray(); - for (int i = 0; i < devices.size(); i++) { - Integer ttsVolume = ttsVolumes.size() > i ? ttsVolumes.get(i) : null; - Integer standardVolume = standardVolumes.size() > i ? standardVolumes.get(i) : null; - if (ttsVolume != null && standardVolume != null && !ttsVolume.equals(standardVolume)) { - standardVolumeNodesToExecute.add(createExecutionNode(devices.get(i), "Alexa.DeviceControls.Volume", - Map.of("value", standardVolume))); - } - } - if (standardVolumeNodesToExecute.size() > 0) { - JsonObject parallelNodesToExecute = new JsonObject(); - parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode"); - parallelNodesToExecute.add("nodesToExecute", standardVolumeNodesToExecute); - serialNodesToExecute.add(parallelNodesToExecute); - } - - if (serialNodesToExecute.size() > 0) { - executeSequenceNodes(devices, serialNodesToExecute, false); - } - } - - // commands: Alexa.Weather.Play, Alexa.Traffic.Play, Alexa.FlashBriefing.Play, - // Alexa.GoodMorning.Play, - // Alexa.SingASong.Play, Alexa.TellStory.Play, Alexa.Speak (textToSpeach) - public void executeSequenceCommand(Device device, String command, Map parameters) { - JsonObject nodeToExecute = createExecutionNode(device, command, parameters); - executeSequenceNode(List.of(device), nodeToExecute); - } - - private void executeSequenceNode(List devices, JsonObject nodeToExecute) { - QueueObject queueObject = new QueueObject(); - queueObject.devices = devices; - queueObject.nodeToExecute = nodeToExecute; - String serialNumbers = ""; - for (Device device : devices) { - String serialNumber = device.serialNumber; - if (serialNumber != null) { - Objects.requireNonNull(this.devices.computeIfAbsent(serialNumber, k -> new LinkedBlockingQueue<>())) - .offer(queueObject); - serialNumbers = serialNumbers + device.serialNumber + " "; - } - } - logger.debug("added {} device {}", queueObject.hashCode(), serialNumbers); - } - - @SuppressWarnings("null") // peek can return null - private void handleExecuteSequenceNode() { - Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.DEVICES, k -> new ReentrantLock())); - if (lock.tryLock()) { - try { - for (String serialNumber : devices.keySet()) { - LinkedBlockingQueue queueObjects = devices.get(serialNumber); - if (queueObjects != null) { - QueueObject queueObject = queueObjects.peek(); - if (queueObject != null) { - Future future = queueObject.future; - if (future == null || future.isDone()) { - boolean execute = true; - String serial = ""; - for (Device tmpDevice : queueObject.devices) { - if (!serialNumber.equals(tmpDevice.serialNumber)) { - LinkedBlockingQueue tmpQueueObjects = devices - .get(tmpDevice.serialNumber); - if (tmpQueueObjects != null) { - QueueObject tmpQueueObject = tmpQueueObjects.peek(); - Future tmpFuture = tmpQueueObject.future; - if (!queueObject.equals(tmpQueueObject) - || (tmpFuture != null && !tmpFuture.isDone())) { - execute = false; - break; - } - serial = serial + tmpDevice.serialNumber + " "; - } - } - } - if (execute) { - queueObject.future = scheduler.submit(() -> queuedExecuteSequenceNode(queueObject)); - logger.debug("thread {} device {}", queueObject.hashCode(), serial); - } - } - } - } - } - } finally { - lock.unlock(); - } - } - } - - private void queuedExecuteSequenceNode(QueueObject queueObject) { - JsonObject nodeToExecute = queueObject.nodeToExecute; - ExecutionNodeObject executionNodeObject = getExecutionNodeObject(nodeToExecute); - if (executionNodeObject == null) { - logger.debug("executionNodeObject empty, removing without execution"); - removeObjectFromQueueAfterExecutionCompletion(queueObject); - return; - } - List types = executionNodeObject.types; - long delay = 0; - if (types.contains("Alexa.DeviceControls.Volume")) { - delay += 2000; - } - if (types.contains("Announcement")) { - delay += 3000; - } else { - delay += 2000; - } - try { - JsonObject sequenceJson = new JsonObject(); - sequenceJson.addProperty("@type", "com.amazon.alexa.behaviors.model.Sequence"); - sequenceJson.add("startNode", nodeToExecute); - - JsonStartRoutineRequest request = new JsonStartRoutineRequest(); - request.sequenceJson = gson.toJson(sequenceJson); - String json = gson.toJson(request); - - Map headers = new HashMap<>(); - headers.put("Routines-Version", "1.1.218665"); - - String text = executionNodeObject.text; - if (text != null) { - text = text.replaceAll("<.+?>", " ").replaceAll("\\s+", " ").trim(); - delay += text.length() * 150; - } - - makeRequest("POST", alexaServer + "/api/behaviors/preview", json, true, true, null, 3); - - Thread.sleep(delay); - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.warn("execute sequence node fails with unexpected error", e); - } finally { - removeObjectFromQueueAfterExecutionCompletion(queueObject); - } - } - - private void removeObjectFromQueueAfterExecutionCompletion(QueueObject queueObject) { - String serial = ""; - for (Device device : queueObject.devices) { - String serialNumber = device.serialNumber; - if (serialNumber != null) { - LinkedBlockingQueue queue = devices.get(serialNumber); - if (queue != null) { - queue.remove(queueObject); - } - serial = serial + serialNumber + " "; - } - } - logger.debug("removed {} device {}", queueObject.hashCode(), serial); - } - - private void executeSequenceNodes(List devices, JsonArray nodesToExecute, boolean parallel) { - JsonObject serialNode = new JsonObject(); - if (parallel) { - serialNode.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode"); - } else { - serialNode.addProperty("@type", "com.amazon.alexa.behaviors.model.SerialNode"); - } - - serialNode.add("nodesToExecute", nodesToExecute); - - executeSequenceNode(devices, serialNode); - } - - private JsonObject createExecutionNode(@Nullable Device device, String command, Map parameters) { - JsonObject operationPayload = new JsonObject(); - if (device != null) { - operationPayload.addProperty("deviceType", device.deviceType); - operationPayload.addProperty("deviceSerialNumber", device.serialNumber); - operationPayload.addProperty("locale", ""); - operationPayload.addProperty("customerId", getCustomerId(device.deviceOwnerCustomerId)); - } - for (String key : parameters.keySet()) { - Object value = parameters.get(key); - if (value instanceof String) { - operationPayload.addProperty(key, (String) value); - } else if (value instanceof Number) { - operationPayload.addProperty(key, (Number) value); - } else if (value instanceof Boolean) { - operationPayload.addProperty(key, (Boolean) value); - } else if (value instanceof Character) { - operationPayload.addProperty(key, (Character) value); - } else { - operationPayload.add(key, gson.toJsonTree(value)); - } - } - - JsonObject nodeToExecute = new JsonObject(); - nodeToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"); - nodeToExecute.addProperty("type", command); - if ("Alexa.TextCommand".equals(command)) { - nodeToExecute.addProperty("skillId", "amzn1.ask.1p.tellalexa"); - } - nodeToExecute.add("operationPayload", operationPayload); - return nodeToExecute; - } - - @Nullable - private ExecutionNodeObject getExecutionNodeObject(JsonObject nodeToExecute) { - ExecutionNodeObject executionNodeObject = new ExecutionNodeObject(); - if (nodeToExecute.has("nodesToExecute")) { - JsonArray serialNodesToExecute = nodeToExecute.getAsJsonArray("nodesToExecute"); - if (serialNodesToExecute != null && serialNodesToExecute.size() > 0) { - for (int i = 0; i < serialNodesToExecute.size(); i++) { - JsonObject serialNodesToExecuteJsonObject = serialNodesToExecute.get(i).getAsJsonObject(); - if (serialNodesToExecuteJsonObject.has("nodesToExecute")) { - JsonArray parallelNodesToExecute = serialNodesToExecuteJsonObject - .getAsJsonArray("nodesToExecute"); - if (parallelNodesToExecute != null && parallelNodesToExecute.size() > 0) { - JsonObject parallelNodesToExecuteJsonObject = parallelNodesToExecute.get(0) - .getAsJsonObject(); - if (processNodesToExecuteJsonObject(executionNodeObject, - parallelNodesToExecuteJsonObject)) { - break; - } - } - } else { - if (processNodesToExecuteJsonObject(executionNodeObject, serialNodesToExecuteJsonObject)) { - break; - } - } - } - } - } - - return executionNodeObject; - } - - private boolean processNodesToExecuteJsonObject(ExecutionNodeObject executionNodeObject, - JsonObject nodesToExecuteJsonObject) { - if (nodesToExecuteJsonObject.has("type")) { - executionNodeObject.types.add(nodesToExecuteJsonObject.get("type").getAsString()); - if (nodesToExecuteJsonObject.has("operationPayload")) { - JsonObject operationPayload = nodesToExecuteJsonObject.getAsJsonObject("operationPayload"); - if (operationPayload != null) { - if (operationPayload.has("textToSpeak")) { - executionNodeObject.text = operationPayload.get("textToSpeak").getAsString(); - return true; - } else if (operationPayload.has("text")) { - executionNodeObject.text = operationPayload.get("text").getAsString(); - return true; - } else if (operationPayload.has("content")) { - JsonArray content = operationPayload.getAsJsonArray("content"); - if (content != null && content.size() > 0) { - JsonObject contentJsonObject = content.get(0).getAsJsonObject(); - if (contentJsonObject.has("speak")) { - JsonObject speak = contentJsonObject.getAsJsonObject("speak"); - if (speak != null && speak.has("value")) { - executionNodeObject.text = speak.get("value").getAsString(); - return true; - } - } - } - } - } - } - } - return false; - } - - public void startRoutine(Device device, String utterance) - throws IOException, URISyntaxException, InterruptedException { - JsonAutomation found = null; - String deviceLocale = ""; - JsonAutomation[] routines = getRoutines(); - if (routines == null) { - return; - } - for (JsonAutomation routine : routines) { - if (routine != null) { - if (routine.sequence != null) { - List triggers = Objects.requireNonNullElse(routine.triggers, List.of()); - for (JsonAutomation.Trigger trigger : triggers) { - Payload payload = trigger.payload; - if (payload == null) { - continue; - } - String payloadUtterance = payload.utterance; - if (payloadUtterance != null && payloadUtterance.equalsIgnoreCase(utterance)) { - found = routine; - deviceLocale = payload.locale; - break; - } - } - } - } - } - if (found != null) { - String sequenceJson = gson.toJson(found.sequence); - - JsonStartRoutineRequest request = new JsonStartRoutineRequest(); - request.behaviorId = found.automationId; - - // replace tokens - // "deviceType":"ALEXA_CURRENT_DEVICE_TYPE" - String deviceType = "\"deviceType\":\"ALEXA_CURRENT_DEVICE_TYPE\""; - String newDeviceType = "\"deviceType\":\"" + device.deviceType + "\""; - sequenceJson = sequenceJson.replace(deviceType.subSequence(0, deviceType.length()), - newDeviceType.subSequence(0, newDeviceType.length())); - - // "deviceSerialNumber":"ALEXA_CURRENT_DSN" - String deviceSerial = "\"deviceSerialNumber\":\"ALEXA_CURRENT_DSN\""; - String newDeviceSerial = "\"deviceSerialNumber\":\"" + device.serialNumber + "\""; - sequenceJson = sequenceJson.replace(deviceSerial.subSequence(0, deviceSerial.length()), - newDeviceSerial.subSequence(0, newDeviceSerial.length())); - - // "customerId": "ALEXA_CUSTOMER_ID" - String customerId = "\"customerId\":\"ALEXA_CUSTOMER_ID\""; - String newCustomerId = "\"customerId\":\"" + getCustomerId(device.deviceOwnerCustomerId) + "\""; - sequenceJson = sequenceJson.replace(customerId.subSequence(0, customerId.length()), - newCustomerId.subSequence(0, newCustomerId.length())); - - // "locale": "ALEXA_CURRENT_LOCALE" - String locale = "\"locale\":\"ALEXA_CURRENT_LOCALE\""; - String newlocale = deviceLocale != null && !deviceLocale.isEmpty() ? "\"locale\":\"" + deviceLocale + "\"" - : "\"locale\":null"; - sequenceJson = sequenceJson.replace(locale.subSequence(0, locale.length()), - newlocale.subSequence(0, newlocale.length())); - - request.sequenceJson = sequenceJson; - - String requestJson = gson.toJson(request); - makeRequest("POST", alexaServer + "/api/behaviors/preview", requestJson, true, true, null, 3); - } else { - logger.warn("Routine {} not found", utterance); - } - } - - public @Nullable JsonAutomation @Nullable [] getRoutines() - throws IOException, URISyntaxException, InterruptedException { - String json = makeRequestAndReturnString(alexaServer + "/api/behaviors/v2/automations?limit=2000"); - JsonAutomation[] result = parseJson(json, JsonAutomation[].class); - return result; - } - - public List getEnabledFlashBriefings() throws IOException, URISyntaxException, InterruptedException { - String json = makeRequestAndReturnString(alexaServer + "/api/content-skills/enabled-feeds"); - JsonEnabledFeeds result = parseJson(json, JsonEnabledFeeds.class); - return Objects.requireNonNullElse(result.enabledFeeds, List.of()); - } - - public void setEnabledFlashBriefings(List enabledFlashBriefing) - throws IOException, URISyntaxException, InterruptedException { - JsonEnabledFeeds enabled = new JsonEnabledFeeds(); - enabled.enabledFeeds = enabledFlashBriefing; - String json = gsonWithNullSerialization.toJson(enabled); - makeRequest("POST", alexaServer + "/api/content-skills/enabled-feeds", json, true, true, null, 0); - } - - public List getNotificationSounds(Device device) - throws IOException, URISyntaxException, InterruptedException { - String json = makeRequestAndReturnString( - alexaServer + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber + "&deviceType=" - + device.deviceType + "&softwareVersion=" + device.softwareVersion); - JsonNotificationSounds result = parseJson(json, JsonNotificationSounds.class); - return Objects.requireNonNullElse(result.notificationSounds, List.of()); - } - - public List notifications() throws IOException, URISyntaxException, InterruptedException { - String response = makeRequestAndReturnString(alexaServer + "/api/notifications"); - JsonNotificationsResponse result = parseJson(response, JsonNotificationsResponse.class); - return Objects.requireNonNullElse(result.notifications, List.of()); - } - - public @Nullable JsonNotificationResponse notification(Device device, String type, @Nullable String label, - @Nullable JsonNotificationSound sound) throws IOException, URISyntaxException, InterruptedException { - Date date = new Date(new Date().getTime()); - long createdDate = date.getTime(); - Date alarm = new Date(createdDate + 5000); // add 5 seconds, because amazon does not except calls for times in - // the past (compared with the server time) - long alarmTime = alarm.getTime(); - - JsonNotificationRequest request = new JsonNotificationRequest(); - request.type = type; - request.deviceSerialNumber = device.serialNumber; - request.deviceType = device.deviceType; - request.createdDate = createdDate; - request.alarmTime = alarmTime; - request.reminderLabel = label; - request.sound = sound; - request.originalDate = new SimpleDateFormat("yyyy-MM-dd").format(alarm); - request.originalTime = new SimpleDateFormat("HH:mm:ss.SSSS").format(alarm); - request.type = type; - request.id = "create" + type; - - String data = gsonWithNullSerialization.toJson(request); - String response = makeRequestAndReturnString("PUT", alexaServer + "/api/notifications/createReminder", data, - true, null); - JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); - return result; - } - - public void stopNotification(JsonNotificationResponse notification) - throws IOException, URISyntaxException, InterruptedException { - makeRequestAndReturnString("DELETE", alexaServer + "/api/notifications/" + notification.id, null, true, null); - } - - public @Nullable JsonNotificationResponse getNotificationState(JsonNotificationResponse notification) - throws IOException, URISyntaxException, InterruptedException { - String response = makeRequestAndReturnString("GET", alexaServer + "/api/notifications/" + notification.id, null, - true, null); - JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); - return result; - } - - public List getMusicProviders() { - try { - Map headers = new HashMap<>(); - headers.put("Routines-Version", "1.1.218665"); - String response = makeRequestAndReturnString("GET", - alexaServer + "/api/behaviors/entities?skillId=amzn1.ask.1p.music", null, true, headers); - if (!response.isEmpty()) { - JsonMusicProvider[] musicProviders = parseJson(response, JsonMusicProvider[].class); - return Arrays.asList(musicProviders); - } - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.warn("getMusicProviders fails: {}", e.getMessage()); - } - return List.of(); - } - - public void playMusicVoiceCommand(Device device, String providerId, String voiceCommand) - throws IOException, URISyntaxException, InterruptedException { - JsonPlaySearchPhraseOperationPayload payload = new JsonPlaySearchPhraseOperationPayload(); - payload.customerId = getCustomerId(device.deviceOwnerCustomerId); - payload.locale = "ALEXA_CURRENT_LOCALE"; - payload.musicProviderId = providerId; - payload.searchPhrase = voiceCommand; - - String playloadString = gson.toJson(payload); - - JsonObject postValidationJson = new JsonObject(); - - postValidationJson.addProperty("type", "Alexa.Music.PlaySearchPhrase"); - postValidationJson.addProperty("operationPayload", playloadString); - - String postDataValidate = postValidationJson.toString(); - - String validateResultJson = makeRequestAndReturnString("POST", - alexaServer + "/api/behaviors/operation/validate", postDataValidate, true, null); - - if (!validateResultJson.isEmpty()) { - JsonPlayValidationResult validationResult = parseJson(validateResultJson, JsonPlayValidationResult.class); - JsonPlaySearchPhraseOperationPayload validatedOperationPayload = validationResult.operationPayload; - if (validatedOperationPayload != null) { - payload.sanitizedSearchPhrase = validatedOperationPayload.sanitizedSearchPhrase; - payload.searchPhrase = validatedOperationPayload.searchPhrase; - } - } - - payload.locale = null; - payload.deviceSerialNumber = device.serialNumber; - payload.deviceType = device.deviceType; - - JsonObject sequenceJson = new JsonObject(); - sequenceJson.addProperty("@type", "com.amazon.alexa.behaviors.model.Sequence"); - JsonObject startNodeJson = new JsonObject(); - startNodeJson.addProperty("@type", "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"); - startNodeJson.addProperty("type", "Alexa.Music.PlaySearchPhrase"); - startNodeJson.add("operationPayload", gson.toJsonTree(payload)); - sequenceJson.add("startNode", startNodeJson); - - JsonStartRoutineRequest startRoutineRequest = new JsonStartRoutineRequest(); - startRoutineRequest.sequenceJson = sequenceJson.toString(); - startRoutineRequest.status = null; - - String postData = gson.toJson(startRoutineRequest); - makeRequest("POST", alexaServer + "/api/behaviors/preview", postData, true, true, null, 3); - } - - public @Nullable JsonEqualizer getEqualizer(Device device) - throws IOException, URISyntaxException, InterruptedException { - String json = makeRequestAndReturnString( - alexaServer + "/api/equalizer/" + device.serialNumber + "/" + device.deviceType); - return parseJson(json, JsonEqualizer.class); - } - - public void setEqualizer(Device device, JsonEqualizer settings) - throws IOException, URISyntaxException, InterruptedException { - String postData = gson.toJson(settings); - makeRequest("POST", alexaServer + "/api/equalizer/" + device.serialNumber + "/" + device.deviceType, postData, - true, true, null, 0); - } - - public static class AnnouncementWrapper { - public List devices = new ArrayList<>(); - public String speak; - public String bodyText; - public @Nullable String title; - public List<@Nullable Integer> ttsVolumes = new ArrayList<>(); - public List<@Nullable Integer> standardVolumes = new ArrayList<>(); - - public AnnouncementWrapper(String speak, String bodyText, @Nullable String title) { - this.speak = speak; - this.bodyText = bodyText; - this.title = title; - } - } - - private static class TextToSpeech { - public List devices = new ArrayList<>(); - public String text; - public List<@Nullable Integer> ttsVolumes = new ArrayList<>(); - public List<@Nullable Integer> standardVolumes = new ArrayList<>(); - - public TextToSpeech(String text) { - this.text = text; - } - } - - private static class TextCommand { - public List devices = new ArrayList<>(); - public String text; - public List<@Nullable Integer> ttsVolumes = new ArrayList<>(); - public List<@Nullable Integer> standardVolumes = new ArrayList<>(); - - public TextCommand(String text) { - this.text = text; - } - } - - private static class Volume { - public List devices = new ArrayList<>(); - public int volume; - public List<@Nullable Integer> volumes = new ArrayList<>(); - - public Volume(int volume) { - this.volume = volume; - } - } - - private static class QueueObject { - public @Nullable Future future; - public List devices = List.of(); - public JsonObject nodeToExecute = new JsonObject(); - } - - private static class ExecutionNodeObject { - public List types = new ArrayList<>(); - @Nullable - public String text; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java index 10919bd62ca2f..d9ee25a54b7b4 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java @@ -13,6 +13,7 @@ package org.openhab.binding.amazonechocontrol.internal; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; /** * The {@link ConnectionException} is used for errors in the connection to the amazon server @@ -20,11 +21,15 @@ * @author Michael Geramb - Initial contribution */ @NonNullByDefault -public class ConnectionException extends RuntimeException { +public class ConnectionException extends Exception { private static final long serialVersionUID = 1L; public ConnectionException(String message) { super(message); } + + public ConnectionException(@Nullable String message, @Nullable Throwable cause) { + super(message, cause); + } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConsoleCommandExtension.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConsoleCommandExtension.java index 69707e2aedb87..237be0bbcd471 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConsoleCommandExtension.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConsoleCommandExtension.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.amazonechocontrol.internal; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.BINDING_ID; + import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -40,8 +42,7 @@ public class ConsoleCommandExtension extends AbstractConsoleCommandExtension { @Activate public ConsoleCommandExtension(@Reference AmazonEchoControlHandlerFactory handlerFactory) { - super("amazonechocontrol", "Manage the AmazonEchoControl account"); - + super(BINDING_ID, "Manage the AmazonEchoControl account"); this.handlerFactory = handlerFactory; } @@ -83,7 +84,7 @@ private void resetAccount(Console console, String accountId) { .filter(handler -> handler.getThing().getUID().getId().equals(accountId)).findAny(); if (accountHandler.isPresent()) { console.println("Resetting account '" + accountId + "'"); - accountHandler.get().setConnection(null); + accountHandler.get().resetConnection(true); } else { console.println("Account '" + accountId + "' not found."); } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java deleted file mode 100644 index 958d0bf87835c..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * The {@link HttpException} is used for http error codes - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class HttpException extends RuntimeException { - - private static final long serialVersionUID = 1L; - int code; - - public int getCode() { - return code; - } - - public HttpException(int code, String message) { - super(message); - this.code = code; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/IWebSocketCommandHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/IWebSocketCommandHandler.java deleted file mode 100644 index d9faa056b0749..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/IWebSocketCommandHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPushCommand; - -/** - * The {@link IWebSocketCommandHandler} is used for the web socket handler implementation - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public interface IWebSocketCommandHandler { - - void webSocketCommandReceived(JsonPushCommand pushCommand); -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ServletUri.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ServletUri.java new file mode 100644 index 0000000000000..6a6e49c8beb5a --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ServletUri.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.amazonechocontrol.internal; + +import static org.eclipse.jetty.util.StringUtil.isNotBlank; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlServlet.SERVLET_PATH; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link ServletUri} is the record for structured handling of the servlet URI + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault({}) +public record ServletUri(String account, String request) { + private static final Pattern URI_PART_PATTERN = Pattern.compile(SERVLET_PATH + "(?:/(\\w+)(/.+)?)?/?"); + + public String buildFor(String uri) { + if (uri.startsWith("/")) { + return SERVLET_PATH + "/" + account() + uri; + } else { + return SERVLET_PATH + "/" + account() + "/" + uri; + } + } + + public static @Nullable ServletUri fromFullUri(@Nullable String requestUri) throws IllegalArgumentException { + if (requestUri == null) { + return null; + } + Matcher matcher = URI_PART_PATTERN.matcher(requestUri); + if (!matcher.matches()) { + return null; + } + return new ServletUri(isNotBlank(matcher.group(1)) ? matcher.group(1) : "", + isNotBlank(matcher.group(2)) ? matcher.group(2) : ""); + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java deleted file mode 100644 index 961a91d8c6d34..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java +++ /dev/null @@ -1,608 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal; - -import java.io.IOException; -import java.net.HttpCookie; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; -import java.util.UUID; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadLocalRandom; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; -import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; -import org.eclipse.jetty.websocket.client.WebSocketClient; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPushCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -/** - * The {@link WebSocketConnection} encapsulate the Web Socket connection to the amazon server. - * The code is based on - * https://github.com/Apollon77/alexa-remote/blob/master/alexa-wsmqtt.js - * - * @author Michael Geramb - Initial contribution - * @author Ingo Fischer - (https://github.com/Apollon77/alexa-remote/blob/master/alexa-wsmqtt.js) - */ -@NonNullByDefault -public class WebSocketConnection { - private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class); - private final Gson gson = new Gson(); - private final WebSocketClient webSocketClient; - private final IWebSocketCommandHandler webSocketCommandHandler; - private final AmazonEchoControlWebSocket amazonEchoControlWebSocket; - - private @Nullable Session session; - private @Nullable Timer pingTimer; - private @Nullable Timer pongTimeoutTimer; - private @Nullable Future sessionFuture; - - private boolean closed; - - public WebSocketConnection(String amazonSite, List sessionCookies, - IWebSocketCommandHandler webSocketCommandHandler) throws IOException { - this.webSocketCommandHandler = webSocketCommandHandler; - amazonEchoControlWebSocket = new AmazonEchoControlWebSocket(); - HttpClient httpClient = new HttpClient(new SslContextFactory.Client()); - webSocketClient = new WebSocketClient(httpClient); - try { - String host; - if ("amazon.com".equalsIgnoreCase(amazonSite)) { - host = "dp-gw-na-js." + amazonSite; - } else { - host = "dp-gw-na." + amazonSite; - } - - String deviceSerial = ""; - List cookiesForWs = new ArrayList<>(); - for (HttpCookie cookie : sessionCookies) { - if (cookie.getName().equals("ubid-acbde")) { - deviceSerial = cookie.getValue(); - } - // Clone the cookie without the security attribute, because the web socket implementation ignore secure - // cookies - String value = cookie.getValue().replaceAll("^\"|\"$", ""); - HttpCookie cookieForWs = new HttpCookie(cookie.getName(), value); - cookiesForWs.add(cookieForWs); - } - deviceSerial += "-" + new Date().getTime(); - URI uri; - - uri = new URI("wss://" + host + "/?x-amz-device-type=ALEGCNGL9K0HM&x-amz-device-serial=" + deviceSerial); - - try { - webSocketClient.start(); - } catch (Exception e) { - logger.warn("Web socket start failed", e); - throw new IOException("Web socket start failed"); - } - - ClientUpgradeRequest request = new ClientUpgradeRequest(); - request.setHeader("Host", host); - request.setHeader("Origin", "alexa." + amazonSite); - request.setCookies(cookiesForWs); - - initPongTimeoutTimer(); - - sessionFuture = webSocketClient.connect(amazonEchoControlWebSocket, uri, request); - } catch (URISyntaxException e) { - logger.debug("Initialize web socket failed", e); - } - } - - private void setSession(Session session) { - this.session = session; - logger.debug("Web Socket session started"); - Timer pingTimer = new Timer(); - this.pingTimer = pingTimer; - pingTimer.schedule(new TimerTask() { - - @Override - public void run() { - amazonEchoControlWebSocket.sendPing(); - } - }, 180000, 180000); - } - - public boolean isClosed() { - return closed; - } - - public void close() { - closed = true; - Timer pingTimer = this.pingTimer; - if (pingTimer != null) { - pingTimer.cancel(); - } - clearPongTimeoutTimer(); - Session session = this.session; - this.session = null; - if (session != null) { - try { - session.close(); - } catch (Exception e) { - logger.debug("Closing session failed", e); - } - } - logger.trace("Connect future = {}", sessionFuture); - final Future sessionFuture = this.sessionFuture; - if (sessionFuture != null && !sessionFuture.isDone()) { - sessionFuture.cancel(true); - } - try { - webSocketClient.stop(); - } catch (InterruptedException e) { - // Just ignore - } catch (Exception e) { - logger.debug("Stopping websocket failed", e); - } - webSocketClient.destroy(); - } - - void clearPongTimeoutTimer() { - Timer pongTimeoutTimer = this.pongTimeoutTimer; - this.pongTimeoutTimer = null; - if (pongTimeoutTimer != null) { - logger.trace("Cancelling pong timeout"); - pongTimeoutTimer.cancel(); - } - } - - void initPongTimeoutTimer() { - clearPongTimeoutTimer(); - Timer pongTimeoutTimer = new Timer(); - this.pongTimeoutTimer = pongTimeoutTimer; - logger.trace("Scheduling pong timeout"); - pongTimeoutTimer.schedule(new TimerTask() { - - @Override - public void run() { - logger.trace("Pong timeout reached. Closing connection."); - close(); - } - }, 60000); - } - - @WebSocket(maxTextMessageSize = 64 * 1024, maxBinaryMessageSize = 64 * 1024) - public class AmazonEchoControlWebSocket { - int msgCounter = -1; - int messageId; - - AmazonEchoControlWebSocket() { - this.messageId = ThreadLocalRandom.current().nextInt(0, Short.MAX_VALUE); - } - - void sendMessage(String message) { - sendMessage(message.getBytes(StandardCharsets.UTF_8)); - } - - void sendMessageHex(String message) { - sendMessage(hexStringToByteArray(message)); - } - - void sendMessage(byte[] buffer) { - try { - logger.debug("Send message with length {}", buffer.length); - Session session = WebSocketConnection.this.session; - if (session != null) { - session.getRemote().sendBytes(ByteBuffer.wrap(buffer)); - } - } catch (IOException e) { - logger.debug("Send message failed", e); - WebSocketConnection.this.close(); - } - } - - byte[] hexStringToByteArray(String str) { - byte[] bytes = new byte[str.length() / 2]; - for (int i = 0; i < bytes.length; i++) { - String strValue = str.substring(2 * i, 2 * i + 2); - bytes[i] = (byte) Integer.parseInt(strValue, 16); - } - return bytes; - } - - long readHex(byte[] data, int index, int length) { - String str = readString(data, index, length); - if (str.startsWith("0x")) { - str = str.substring(2); - } - return Long.parseLong(str, 16); - } - - String readString(byte[] data, int index, int length) { - return new String(data, index, length, StandardCharsets.UTF_8); - } - - class Message { - String service = ""; - Content content = new Content(); - String contentTune = ""; - String messageType = ""; - long channel; - long checksum; - long messageId; - String moreFlag = ""; - long seq; - } - - class Content { - String messageType = ""; - String protocolVersion = ""; - String connectionUUID = ""; - long established; - long timestampINI; - long timestampACK; - String subMessageType = ""; - long channel; - String destinationIdentityUrn = ""; - String deviceIdentityUrn = ""; - @Nullable - String payload; - byte[] payloadData = new byte[0]; - @Nullable - JsonPushCommand pushCommand; - } - - Message parseIncomingMessage(byte[] data) { - int idx = 0; - Message message = new Message(); - message.service = readString(data, data.length - 4, 4); - - if (message.service.equals("TUNE")) { - message.checksum = readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - int contentLength = (int) readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - message.contentTune = readString(data, idx, contentLength - 4 - idx); - } else if (message.service.equals("FABE")) { - message.messageType = readString(data, idx, 3); - idx += 4; - message.channel = readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - message.messageId = readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - message.moreFlag = readString(data, idx, 1); - idx += 2; // 1 + delimiter; - message.seq = readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - message.checksum = readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - - // currently not used: long contentLength = readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - - message.content.messageType = readString(data, idx, 3); - idx += 4; - - if (message.channel == 0x361) { // GW_HANDSHAKE_CHANNEL - if (message.content.messageType.equals("ACK")) { - int length = (int) readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - message.content.protocolVersion = readString(data, idx, length); - idx += length + 1; - length = (int) readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - message.content.connectionUUID = readString(data, idx, length); - idx += length + 1; - message.content.established = readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - message.content.timestampINI = readHex(data, idx, 18); - idx += 19; // 18 + delimiter; - message.content.timestampACK = readHex(data, idx, 18); - idx += 19; // 18 + delimiter; - } - } else if (message.channel == 0x362) { // GW_CHANNEL - if (message.content.messageType.equals("GWM")) { - message.content.subMessageType = readString(data, idx, 3); - idx += 4; - message.content.channel = readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - - if (message.content.channel == 0xb479) { // DEE_WEBSITE_MESSAGING - int length = (int) readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - message.content.destinationIdentityUrn = readString(data, idx, length); - idx += length + 1; - - length = (int) readHex(data, idx, 10); - idx += 11; // 10 + delimiter; - String idData = readString(data, idx, length); - idx += length + 1; - - String[] idDataElements = idData.split(" ", 2); - message.content.deviceIdentityUrn = idDataElements[0]; - String payload = null; - if (idDataElements.length == 2) { - payload = idDataElements[1]; - } - if (payload == null) { - payload = readString(data, idx, data.length - 4 - idx); - } - if (!payload.isEmpty()) { - try { - message.content.pushCommand = gson.fromJson(payload, JsonPushCommand.class); - } catch (JsonSyntaxException e) { - logger.info("Parsing json failed, illegal JSON: {}", payload, e); - } - } - message.content.payload = payload; - } - } - } else if (message.channel == 0x65) { // CHANNEL_FOR_HEARTBEAT - idx -= 1; // no delimiter! - message.content.payloadData = Arrays.copyOfRange(data, idx, data.length - 4); - } - } - return message; - } - - @OnWebSocketConnect - public void onWebSocketConnect(@Nullable Session session) { - if (session != null) { - this.msgCounter = -1; - setSession(session); - sendMessage("0x99d4f71a 0x0000001d A:HTUNE"); - } else { - logger.debug("Web Socket connect without session"); - } - } - - @OnWebSocketMessage - public void onWebSocketBinary(byte @Nullable [] data, int offset, int len) { - if (data == null) { - return; - } - this.msgCounter++; - if (this.msgCounter == 0) { - sendMessage( - "0xa6f6a951 0x0000009c {\"protocolName\":\"A:H\",\"parameters\":{\"AlphaProtocolHandler.receiveWindowSize\":\"16\",\"AlphaProtocolHandler.maxFragmentSize\":\"16000\"}}TUNE"); - sendMessage(encodeGWHandshake()); - } else if (this.msgCounter == 1) { - sendMessage(encodeGWRegister()); - sendPing(); - } else { - byte[] buffer = data; - if (offset > 0 || len != buffer.length) { - buffer = Arrays.copyOfRange(data, offset, offset + len); - } - try { - Message message = parseIncomingMessage(buffer); - if (message.service.equals("FABE") && message.content.messageType.equals("PON") - && message.content.payloadData.length > 0) { - logger.debug("Pong received"); - WebSocketConnection.this.clearPongTimeoutTimer(); - return; - } else { - JsonPushCommand pushCommand = message.content.pushCommand; - logger.debug("Message received: {}", message.content.payload); - if (pushCommand != null) { - webSocketCommandHandler.webSocketCommandReceived(pushCommand); - } - return; - } - } catch (Exception e) { - logger.debug("Handling of push notification failed", e); - } - } - } - - @OnWebSocketMessage - public void onWebSocketText(@Nullable String message) { - logger.trace("Received text message: '{}'", message); - } - - @OnWebSocketClose - public void onWebSocketClose(int code, @Nullable String reason) { - logger.info("Web Socket close {}. Reason: {}", code, reason); - WebSocketConnection.this.close(); - } - - @OnWebSocketError - public void onWebSocketError(@Nullable Throwable error) { - logger.info("Web Socket error", error); - if (!closed) { - WebSocketConnection.this.close(); - } - } - - public void sendPing() { - logger.debug("Send Ping"); - WebSocketConnection.this.initPongTimeoutTimer(); - sendMessage(encodePing()); - } - - String encodeNumber(long val) { - return encodeNumber(val, 8); - } - - String encodeNumber(long val, int len) { - String str = Long.toHexString(val); - if (str.length() > len) { - str = str.substring(str.length() - len); - } - while (str.length() < len) { - str = '0' + str; - } - return "0x" + str; - } - - long computeBits(long input, long len) { - long lenCounter = len; - long value; - for (value = toUnsignedInt(input); 0 != lenCounter && 0 != value;) { - value = (long) Math.floor(value / 2); - lenCounter--; - } - return value; - } - - long toUnsignedInt(long value) { - long result = value; - if (0 > value) { - result = 4294967295L + value + 1; - } - return result; - } - - int computeChecksum(byte[] data, int exclusionStart, int exclusionEnd) { - if (exclusionEnd < exclusionStart) { - return 0; - } - long overflow; - long sum; - int index; - for (overflow = 0, sum = 0, index = 0; index < data.length; index++) { - if (index != exclusionStart) { - sum += toUnsignedInt((data[index] & 0xFF) << ((index & 3 ^ 3) << 3)); - overflow += computeBits(sum, 32); - sum = toUnsignedInt((int) sum & (int) 4294967295L); - } else { - index = exclusionEnd - 1; - } - } - while (overflow != 0) { - sum += overflow; - overflow = computeBits(sum, 32); - sum = (int) sum & (int) 4294967295L; - } - long value = toUnsignedInt(sum); - return (int) value; - } - - byte[] encodeGWHandshake() { - // pubrelBuf = new Buffer('MSG 0x00000361 0x0e414e45 f 0x00000001 0xd7c62f29 0x0000009b INI 0x00000003 1.0 - // 0x00000024 ff1c4525-c036-4942-bf6c-a098755ac82f 0x00000164d106ce6b END FABE'); - this.messageId++; - String msg = "MSG 0x00000361 "; // Message-type and Channel = GW_HANDSHAKE_CHANNEL; - msg += this.encodeNumber(this.messageId) + " f 0x00000001 "; - int checkSumStart = msg.length(); - msg += "0x00000000 "; // Checksum! - int checkSumEnd = msg.length(); - msg += "0x0000009b "; // length content - msg += "INI 0x00000003 1.0 0x00000024 "; // content part 1 - msg += UUID.randomUUID().toString(); - msg += ' '; - msg += this.encodeNumber(new Date().getTime(), 16); - msg += " END FABE"; - // msg = "MSG 0x00000361 0x0e414e45 f 0x00000001 0xd7c62f29 0x0000009b INI 0x00000003 1.0 0x00000024 - // ff1c4525-c036-4942-bf6c-a098755ac82f 0x00000164d106ce6b END FABE"; - byte[] completeBuffer = msg.getBytes(StandardCharsets.US_ASCII); - - int checksum = this.computeChecksum(completeBuffer, checkSumStart, checkSumEnd); - String checksumHex = encodeNumber(checksum); - byte[] checksumBuf = checksumHex.getBytes(StandardCharsets.US_ASCII); - System.arraycopy(checksumBuf, 0, completeBuffer, checkSumStart, checksumBuf.length); - - return completeBuffer; - } - - byte[] encodeGWRegister() { - // pubrelBuf = new Buffer('MSG 0x00000362 0x0e414e46 f 0x00000001 0xf904b9f5 0x00000109 GWM MSG 0x0000b479 - // 0x0000003b urn:tcomm-endpoint:device:deviceType:0:deviceSerialNumber:0 0x00000041 - // urn:tcomm-endpoint:service:serviceName:DeeWebsiteMessagingService - // {"command":"REGISTER_CONNECTION"}FABE'); - this.messageId++; - String msg = "MSG 0x00000362 "; // Message-type and Channel = GW_CHANNEL; - msg += this.encodeNumber(this.messageId) + " f 0x00000001 "; - int checkSumStart = msg.length(); - msg += "0x00000000 "; // Checksum! - int checkSumEnd = msg.length(); - msg += "0x00000109 "; // length content - msg += "GWM MSG 0x0000b479 0x0000003b urn:tcomm-endpoint:device:deviceType:0:deviceSerialNumber:0 0x00000041 urn:tcomm-endpoint:service:serviceName:DeeWebsiteMessagingService {\"command\":\"REGISTER_CONNECTION\"}FABE"; - - byte[] completeBuffer = msg.getBytes(StandardCharsets.US_ASCII); - - int checksum = this.computeChecksum(completeBuffer, checkSumStart, checkSumEnd); - - String checksumHex = encodeNumber(checksum); - byte[] checksumBuf = checksumHex.getBytes(StandardCharsets.US_ASCII); - System.arraycopy(checksumBuf, 0, completeBuffer, checkSumStart, checksumBuf.length); - - String test = readString(completeBuffer, 0, completeBuffer.length); - test.toString(); - return completeBuffer; - } - - void encode(byte[] data, long b, int offset, int len) { - for (int index = 0; index < len; index++) { - data[index + offset] = (byte) (b >> 8 * (len - 1 - index) & 255); - } - } - - byte[] encodePing() { - // MSG 0x00000065 0x0e414e47 f 0x00000001 0xbc2fbb5f 0x00000062 - this.messageId++; - String msg = "MSG 0x00000065 "; // Message-type and Channel = CHANNEL_FOR_HEARTBEAT; - msg += this.encodeNumber(this.messageId) + " f 0x00000001 "; - int checkSumStart = msg.length(); - msg += "0x00000000 "; // Checksum! - int checkSumEnd = msg.length(); - msg += "0x00000062 "; // length content - - byte[] completeBuffer = new byte[0x62]; - byte[] startBuffer = msg.getBytes(StandardCharsets.US_ASCII); - - System.arraycopy(startBuffer, 0, completeBuffer, 0, startBuffer.length); - - byte[] header = "PIN".getBytes(StandardCharsets.US_ASCII); - byte[] payload = "Regular".getBytes(StandardCharsets.US_ASCII); // g = h.length - byte[] bufferPing = new byte[header.length + 4 + 8 + 4 + 2 * payload.length]; - int idx = 0; - System.arraycopy(header, 0, bufferPing, 0, header.length); - idx += header.length; - encode(bufferPing, 0, idx, 4); - idx += 4; - encode(bufferPing, new Date().getTime(), idx, 8); - idx += 8; - encode(bufferPing, payload.length, idx, 4); - idx += 4; - - for (int q = 0; q < payload.length; q++) { - bufferPing[idx + q * 2] = (byte) 0; - bufferPing[idx + q * 2 + 1] = payload[q]; - } - System.arraycopy(bufferPing, 0, completeBuffer, startBuffer.length, bufferPing.length); - - byte[] buf2End = "FABE".getBytes(StandardCharsets.US_ASCII); - System.arraycopy(buf2End, 0, completeBuffer, startBuffer.length + bufferPing.length, buf2End.length); - - int checksum = this.computeChecksum(completeBuffer, checkSumStart, checkSumEnd); - String checksumHex = encodeNumber(checksum); - byte[] checksumBuf = checksumHex.getBytes(StandardCharsets.US_ASCII); - System.arraycopy(checksumBuf, 0, completeBuffer, checkSumStart, checksumBuf.length); - return completeBuffer; - } - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java deleted file mode 100644 index ab1adca34c7fe..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.channelhandler; - -import java.io.IOException; -import java.net.URISyntaxException; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; -import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -/** - * The {@link ChannelHandler} is the base class for all channel handlers - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public abstract class ChannelHandler { - - public abstract boolean tryHandleCommand(Device device, Connection connection, String channelId, Command command) - throws IOException, URISyntaxException, InterruptedException; - - protected final IAmazonThingHandler thingHandler; - protected final Gson gson; - private final Logger logger; - - protected ChannelHandler(IAmazonThingHandler thingHandler, Gson gson) { - this.logger = LoggerFactory.getLogger(this.getClass()); - this.thingHandler = thingHandler; - this.gson = gson; - } - - protected @Nullable T tryParseJson(String json, Class type) { - try { - return gson.fromJson(json, type); - } catch (JsonSyntaxException e) { - logger.debug("Json parse error", e); - return null; - } - } - - protected @Nullable T parseJson(String json, Class type) throws JsonSyntaxException { - return gson.fromJson(json, type); - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerAnnouncement.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerAnnouncement.java deleted file mode 100644 index 99bac4eff3f00..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerAnnouncement.java +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.channelhandler; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.Objects; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; -import org.openhab.core.library.types.StringType; -import org.openhab.core.types.Command; -import org.unbescape.xml.XmlEscape; - -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -/** - * The {@link ChannelHandlerAnnouncement} is responsible for the announcement - * channel - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class ChannelHandlerAnnouncement extends ChannelHandler { - - private static final String CHANNEL_NAME = "announcement"; - - protected final IEchoThingHandler thingHandler; - - public ChannelHandlerAnnouncement(IEchoThingHandler thingHandler, Gson gson) { - super(thingHandler, gson); - this.thingHandler = thingHandler; - } - - @Override - public boolean tryHandleCommand(Device device, Connection connection, String channelId, Command command) - throws IOException, URISyntaxException { - if (channelId.equals(CHANNEL_NAME)) { - if (command instanceof StringType stringCommand) { - String commandValue = stringCommand.toFullString(); - String body = commandValue; - String title = null; - String speak = commandValue; - Integer volume = null; - if (commandValue.startsWith("{") && commandValue.endsWith("}")) { - try { - AnnouncementRequestJson request = parseJson(commandValue, AnnouncementRequestJson.class); - if (request != null) { - speak = request.speak; - if (speak == null || speak.length() == 0) { - speak = " "; // blank generates a beep - } - volume = request.volume; - title = request.title; - body = request.body; - if (body == null) { - body = speak; - } - Boolean sound = request.sound; - if (sound != null) { - if (!sound && !speak.startsWith("")) { - speak = "" + XmlEscape.escapeXml10(speak) + ""; - } - if (sound && speak.startsWith("")) { - body = "Error: The combination of sound and speak in SSML syntax is not allowed"; - title = "Error"; - speak = "Error: The combination of sound and speak in SSML syntax is not allowed"; - } - } - if (" ".equals(speak)) { - volume = -1; // Do not change volume - } - } - } catch (JsonSyntaxException e) { - body = "Invalid Json." + e.getLocalizedMessage(); - title = "Error"; - speak = "" + XmlEscape.escapeXml10(body) + ""; - body = e.getLocalizedMessage(); - } - } - thingHandler.startAnnouncement(device, speak, Objects.requireNonNullElse(body, ""), title, volume); - } - refreshChannel(); - } - return false; - } - - private void refreshChannel() { - thingHandler.updateChannelState(CHANNEL_NAME, new StringType("")); - } - - private static class AnnouncementRequestJson { - public @Nullable Boolean sound; - public @Nullable String title; - public @Nullable String body; - public @Nullable String speak; - public @Nullable Integer volume; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java deleted file mode 100644 index f49482addd483..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.channelhandler; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.time.LocalDateTime; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; -import org.openhab.core.library.types.StringType; -import org.openhab.core.types.Command; - -import com.google.gson.Gson; - -/** - * The {@link ChannelHandlerSendMessage} is responsible for the announcement - * channel - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class ChannelHandlerSendMessage extends ChannelHandler { - - private static final String CHANNEL_NAME = "sendMessage"; - private @Nullable AccountJson accountJson; - private int lastMessageId = 1000; - - public ChannelHandlerSendMessage(IAmazonThingHandler thingHandler, Gson gson) { - super(thingHandler, gson); - } - - @Override - public boolean tryHandleCommand(Device device, Connection connection, String channelId, Command command) - throws IOException, URISyntaxException, InterruptedException { - if (channelId.equals(CHANNEL_NAME)) { - if (command instanceof StringType) { - String commandValue = ((StringType) command).toFullString(); - String baseUrl = "https://alexa-comms-mobile-service." + connection.getAmazonSite(); - - AccountJson currentAccountJson = this.accountJson; - if (currentAccountJson == null) { - String accountResult = connection.makeRequestAndReturnString(baseUrl + "/accounts"); - AccountJson @Nullable [] accountsJson = gson.fromJson(accountResult, AccountJson[].class); - if (accountsJson == null) { - return false; - } - for (AccountJson accountJson : accountsJson) { - Boolean signedInUser = accountJson.signedInUser; - if (signedInUser != null && signedInUser) { - this.accountJson = accountJson; - currentAccountJson = accountJson; - break; - } - } - } - if (currentAccountJson == null) { - return false; - } - String commsId = currentAccountJson.commsId; - if (commsId == null) { - return false; - } - String senderCommsId = commsId; - String receiverCommsId = commsId; - - SendConversationJson conversationJson = new SendConversationJson(); - conversationJson.conversationId = "amzn1.comms.messaging.id.conversationV2~31e6fe8f-8b0c-4e84-a1e4-80030a09009b"; - conversationJson.clientMessageId = java.util.UUID.randomUUID().toString(); - conversationJson.messageId = lastMessageId++; - conversationJson.sender = senderCommsId; - conversationJson.time = LocalDateTime.now().toString(); - conversationJson.payload.text = commandValue; - - String sendConversationBody = this.gson.toJson(new SendConversationJson[] { conversationJson }); - String sendUrl = baseUrl + "/users/" + senderCommsId + "/conversations/" + receiverCommsId - + "/messages"; - connection.makeRequestAndReturnString("POST", sendUrl, sendConversationBody, true, null); - } - refreshChannel(); - } - return false; - } - - private void refreshChannel() { - thingHandler.updateChannelState(CHANNEL_NAME, new StringType("")); - } - - @SuppressWarnings("unused") - private static class AccountJson { - public @Nullable String commsId; - public @Nullable String directedId; - public @Nullable String phoneCountryCode; - public @Nullable String phoneNumber; - public @Nullable String firstName; - public @Nullable String lastName; - public @Nullable String phoneticFirstName; - public @Nullable String phoneticLastName; - public @Nullable String commsProvisionStatus; - public @Nullable Boolean isChild; - public @Nullable Boolean signedInUser; - public @Nullable Boolean commsProvisioned; - public @Nullable Boolean speakerProvisioned; - } - - @SuppressWarnings("unused") - private static class SendConversationJson { - public @Nullable String conversationId; - public @Nullable String clientMessageId; - public @Nullable Integer messageId; - public @Nullable String time; - public @Nullable String sender; - public String type = "message/text"; - public Payload payload = new Payload(); - public Integer status = 1; - - private static class Payload { - public @Nullable String text; - } - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/IAmazonThingHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/IAmazonThingHandler.java deleted file mode 100644 index e4f133dd6dde8..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/IAmazonThingHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.channelhandler; - -import java.io.IOException; -import java.net.URISyntaxException; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; -import org.openhab.core.types.State; - -/** - * The {@link IAmazonThingHandler} is used from ChannelHandlers to communicate - * with the thing - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public interface IAmazonThingHandler { - - void updateChannelState(String channelId, State state); - - void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title, - @Nullable Integer volume) throws IOException, URISyntaxException; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/IEchoThingHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/IEchoThingHandler.java deleted file mode 100644 index af2bbde2be6e6..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/IEchoThingHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.channelhandler; - -import java.io.IOException; -import java.net.URISyntaxException; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; - -/** - * The {@link IEchoThingHandler} is used from ChannelHandlers to communicate with the thing - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public interface IEchoThingHandler extends IAmazonThingHandler { - @Override - void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title, - @Nullable Integer volume) throws IOException, URISyntaxException; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/AnnouncementWrapper.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/AnnouncementWrapper.java new file mode 100644 index 0000000000000..91a86997d747b --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/AnnouncementWrapper.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.connection; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceTO; +import org.openhab.binding.amazonechocontrol.internal.dto.request.AnnouncementContentTO; + +/** + * The {@link AnnouncementWrapper} is a wrapper for announcement instructions + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class AnnouncementWrapper { + private final List devices = new ArrayList<>(); + private final List<@Nullable Integer> ttsVolumes = new ArrayList<>(); + private final List<@Nullable Integer> standardVolumes = new ArrayList<>(); + + private final String speak; + private final String bodyText; + private final @Nullable String title; + + public AnnouncementWrapper(String speak, String bodyText, @Nullable String title) { + this.speak = speak; + this.bodyText = bodyText; + this.title = title; + } + + public void add(DeviceTO device, @Nullable Integer ttsVolume, @Nullable Integer standardVolume) { + devices.add(device); + ttsVolumes.add(ttsVolume); + standardVolumes.add(standardVolume); + } + + public List getDevices() { + return devices; + } + + public String getSpeak() { + return speak; + } + + public String getBodyText() { + return bodyText; + } + + public @Nullable String getTitle() { + return title; + } + + public List<@Nullable Integer> getTtsVolumes() { + return ttsVolumes; + } + + public List<@Nullable Integer> getStandardVolumes() { + return standardVolumes; + } + + public AnnouncementContentTO toAnnouncementTO() { + AnnouncementContentTO announcement = new AnnouncementContentTO(); + announcement.display.body = bodyText; + String title = this.title; + announcement.display.title = (title == null || title.isBlank()) ? "openHAB" : title; + announcement.speak.value = speak; + announcement.speak.type = (speak.startsWith("") && speak.endsWith("")) ? "ssml" : "text"; + return announcement; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/Connection.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/Connection.java new file mode 100644 index 0000000000000..72df44531c5d7 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/Connection.java @@ -0,0 +1,1490 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.connection; + +import static org.eclipse.jetty.http.HttpStatus.NO_CONTENT_204; +import static org.eclipse.jetty.http.HttpStatus.OK_200; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.CAPABILITY_REGISTRATION; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpHeader; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; +import org.openhab.binding.amazonechocontrol.internal.ConnectionException; +import org.openhab.binding.amazonechocontrol.internal.dto.AscendingAlarmModelTO; +import org.openhab.binding.amazonechocontrol.internal.dto.CookieTO; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceNotificationStateTO; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceTO; +import org.openhab.binding.amazonechocontrol.internal.dto.DoNotDisturbDeviceStatusTO; +import org.openhab.binding.amazonechocontrol.internal.dto.EnabledFeedTO; +import org.openhab.binding.amazonechocontrol.internal.dto.EnabledFeedsTO; +import org.openhab.binding.amazonechocontrol.internal.dto.EqualizerTO; +import org.openhab.binding.amazonechocontrol.internal.dto.NotificationSoundTO; +import org.openhab.binding.amazonechocontrol.internal.dto.NotificationStateTO; +import org.openhab.binding.amazonechocontrol.internal.dto.NotificationTO; +import org.openhab.binding.amazonechocontrol.internal.dto.PlaySearchPhraseTO; +import org.openhab.binding.amazonechocontrol.internal.dto.TOMapper; +import org.openhab.binding.amazonechocontrol.internal.dto.request.AnnouncementTO; +import org.openhab.binding.amazonechocontrol.internal.dto.request.AuthRegisterTO; +import org.openhab.binding.amazonechocontrol.internal.dto.request.BehaviorOperationValidateTO; +import org.openhab.binding.amazonechocontrol.internal.dto.request.ExchangeTokenTO; +import org.openhab.binding.amazonechocontrol.internal.dto.request.StartRoutineTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.AscendingAlarmModelsTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.AuthRegisterResponseTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.AuthRegisterTokensTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.AuthTokenTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.AutomationPayloadTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.AutomationTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.AutomationTriggerTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.BluetoothStateTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.BluetoothStatesTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.BootstrapAuthenticationTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.BootstrapTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.CustomerHistoryRecordTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.CustomerHistoryRecordsTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.DeviceListTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.DeviceNotificationStatesTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.DoNotDisturbDeviceStatusesTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.EndpointTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.ListItemTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.ListMediaSessionTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.MediaSessionTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.MusicProviderTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.NamedListsInfoTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.NamedListsItemsTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.NotificationListResponseTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.NotificationSoundResponseTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.PlayerStateTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.UsersMeTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.WakeWordTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.WakeWordsTO; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.SmartHomeBaseDevice; +import org.openhab.binding.amazonechocontrol.internal.util.HttpRequestBuilder; +import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.unbescape.json.JsonEscape; +import org.unbescape.json.JsonEscapeLevel; +import org.unbescape.json.JsonEscapeType; +import org.unbescape.xml.XmlEscape; +import org.unbescape.xml.XmlEscapeLevel; +import org.unbescape.xml.XmlEscapeType; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * The {@link Connection} is responsible for the connection to the amazon server and handling of the commands + * + * @author Michael Geramb - Initial contribution + * @author Jan N. Klug - Refactored to use jetty client, add {@link HttpRequestBuilder} + */ +@NonNullByDefault +public class Connection { + private static final String THING_THREADPOOL_NAME = "thingHandler"; + private static final long EXPIRES_IN = 432000; // five days + + private final Logger logger = LoggerFactory.getLogger(Connection.class); + + protected final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(THING_THREADPOOL_NAME); + + private final Gson gson; + + private LoginData loginData; + private CookieManager cookieManager = new CookieManager(); + private final HttpRequestBuilder requestBuilder; + private @Nullable Date verifyTime; + private long connectionExpiryTime = 0; + private long accessTokenExpiryTime = 0; + private @Nullable String customerName; + private @Nullable String accessToken; + + private final Map announcements = Collections.synchronizedMap(new LinkedHashMap<>()); + private final Map textToSpeeches = Collections.synchronizedMap(new LinkedHashMap<>()); + private final Map textCommands = Collections.synchronizedMap(new LinkedHashMap<>()); + + private final Map volumes = Collections.synchronizedMap(new LinkedHashMap<>()); + private final Map> devices = Collections + .synchronizedMap(new LinkedHashMap<>()); + + private final Map> timers = new ConcurrentHashMap<>(); + private final Map locks = new ConcurrentHashMap<>(); + + private enum TimerType { + ANNOUNCEMENT, + TTS, + VOLUME, + DEVICES, + TEXT_COMMAND + } + + public Connection(@Nullable Connection oldConnection, Gson gson, HttpClient httpClient) { + this.gson = gson; + + this.requestBuilder = new HttpRequestBuilder(httpClient, cookieManager, gson); + if (oldConnection != null) { + LoginData oldLoginData = oldConnection.getLoginData(); + this.loginData = new LoginData(cookieManager, oldLoginData.getDeviceId(), oldLoginData.getFrc(), + oldLoginData.getSerial()); + } else { + this.loginData = new LoginData(cookieManager); + } + + replaceTimer(TimerType.DEVICES, + scheduler.scheduleWithFixedDelay(this::handleExecuteSequenceNode, 0, 500, TimeUnit.MILLISECONDS)); + } + + public HttpRequestBuilder getRequestBuilder() { + return requestBuilder; + } + + public LoginData getLoginData() { + // update cookies + return loginData; + } + + public @Nullable Date getVerifyTime() { + return verifyTime; + } + + public String getRetailDomain() { + return loginData.getRetailDomain(); + } + + public String getRetailUrl() { + return loginData.getRetailUrl(); + } + + public String getAlexaServer() { + return this.loginData.getWebsiteApiUrl(); + } + + public String getCustomerName() { + return Objects.requireNonNullElse(customerName, "Unknown"); + } + + public boolean isSequenceNodeQueueRunning() { + return devices.values().stream().anyMatch( + (queueObjects) -> (queueObjects.stream().anyMatch(queueObject -> queueObject.future != null))); + } + + public boolean restoreLogin(@Nullable String data, @Nullable String overloadedDomain) { + try { + // verify stored data + if (data != null && !data.isEmpty() && loginData.deserialize(data)) { + if (overloadedDomain != null) { + loginData.setRetailDomain(overloadedDomain); + } + renewTokens(); + if (verifyLogin()) { + return true; + } + } + } catch (ConnectionException e) { + // no action + } + this.loginData.setLoginTime(null); + return false; + } + + private boolean tryGetBootstrap() { + try { + BootstrapTO result = requestBuilder.get(getAlexaServer() + "/api/bootstrap").retry(false).redirect(false) + .syncSend(BootstrapTO.class); + BootstrapAuthenticationTO authentication = result.authentication; + if (authentication != null && authentication.authenticated) { + this.customerName = authentication.customerName; + this.loginData.setAccountCustomerId(authentication.customerId); + return authentication.authenticated; + } + } catch (ConnectionException e) { + logger.debug("Bootstrapping failed", e); + } + return false; + } + + public boolean registerConnectionAsApp(String accessToken) { + try { + List webSiteCookies = cookieManager.getCookieStore().get(URI.create("https://www.amazon.com")) + .stream().map(TOMapper::mapCookie).toList(); + + AuthRegisterTO registerAppRequest = new AuthRegisterTO(); + registerAppRequest.registrationData.deviceSerial = loginData.getSerial(); + registerAppRequest.authData.accessToken = accessToken; + registerAppRequest.userContextMap = Map.of("frc", loginData.getFrc()); + registerAppRequest.cookies.webSiteCookies = webSiteCookies; + + AuthRegisterResponseTO registerAppResponse = requestBuilder.post("https://api.amazon.com/auth/register") + .withContent(registerAppRequest) + .withHeaders(Map.of("x-amzn-identity-auth-domain", "api.amazon.com")).syncSend( + org.openhab.binding.amazonechocontrol.internal.dto.response.AuthRegisterTO.class).response; + + AuthRegisterTokensTO tokens = registerAppResponse.success.tokens; + String refreshToken = tokens.bearer.refreshToken; + + this.loginData.setRefreshToken(refreshToken); + + if (refreshToken == null || refreshToken.isBlank()) { + logger.warn("Could not determine refreshToken while trying to register as app."); + return false; + } + + exchangeToken(getRetailDomain()); + // Check which is the owner domain + UsersMeTO usersMeResponse = requestBuilder.get("https://alexa.amazon.com/api/users/me?platform=ios&version=" + + AmazonEchoControlBindingConstants.API_VERSION).syncSend(UsersMeTO.class); + + // Switch to owner domain + exchangeToken(usersMeResponse.marketPlaceDomainName); + EndpointTO endpoints = requestBuilder.get("https://alexa.amazon.com/api/endpoints") + .syncSend(EndpointTO.class); + this.loginData.setRetailDomain(endpoints.retailDomain); + this.loginData.setWebsiteApiUrl(endpoints.websiteApiUrl); + this.loginData.setRetailUrl(endpoints.retailUrl); + + HttpRequestBuilder.HttpResponse response = requestBuilder + .put("https://api.amazonalexa.com/v1/devices/@self/capabilities") + .withContent(CAPABILITY_REGISTRATION) + .withHeaders(Map.of(HttpHeader.AUTHORIZATION.toString(), "Bearer " + tokens.bearer.accessToken)) + .retry(false).syncSend(); + if (response.statusCode() != NO_CONTENT_204 && response.statusCode() != OK_200) { + logger.warn("Registering capabilities failed, HTTP/2 stream will not work"); + } + + tryGetBootstrap(); + this.loginData.setDeviceName(registerAppResponse.success.extensions.deviceInfo.deviceName); + + return true; + } catch (Exception e) { + logger.warn("Registering as app failed: {}", e.getMessage()); + logout(false); + return false; + } + } + + private void exchangeToken(String cookieDomain) throws ConnectionException { + this.connectionExpiryTime = 0; + String cookiesJson = "{\"cookies\":{\"." + cookieDomain + "\":[]}}"; + String cookiesBase64 = Base64.getEncoder().encodeToString(cookiesJson.getBytes()); + + String exchangePostData = "di.os.name=iOS" // + + "&app_version=" + AmazonEchoControlBindingConstants.API_VERSION // + + "&domain=." + getRetailDomain() // + + "&source_token=" + URLEncoder.encode(this.loginData.getRefreshToken(), StandardCharsets.UTF_8) // + + "&requested_token_type=auth_cookies" // + + "&source_token_type=refresh_token" // + + "&di.hw.version=iPhone" // + + "&di.sdk.version=" + AmazonEchoControlBindingConstants.DI_SDK_VERSION // + + "&cookies=" + cookiesBase64 // + + "&app_name=Amazon%20Alexa" // + + "&di.os.version=" + AmazonEchoControlBindingConstants.DI_OS_VERSION; + + String url = getRetailUrl() + "/ap/exchangetoken"; + + ExchangeTokenTO exchangeToken = requestBuilder.post(url).withContent(exchangePostData).withHeader("Cookie", "") + .syncSend(ExchangeTokenTO.class); + + CookieStore cookieStore = cookieManager.getCookieStore(); + exchangeToken.response.tokens.cookies + .forEach((domain, cookies) -> cookies.stream().map(cookie -> TOMapper.mapCookie(cookie, domain)) + .forEach(httpCookie -> cookieStore.add(null, httpCookie))); + + if (!verifyLogin()) { + throw new ConnectionException("Verify login failed after token exchange"); + } + + // renew at 80% expired + this.connectionExpiryTime = System.currentTimeMillis() + (long) (Connection.EXPIRES_IN * 1000d / 0.8d); + } + + public String getAccessToken() throws ConnectionException { + String accessToken = this.accessToken; + if (accessToken == null) { + throw new ConnectionException("accessToken not set"); + } + return accessToken; + } + + /** + * Check if tokens need to be renewed + *

+ * The {@link #accessToken} is renewed when the current nextAlarmTime is above + * {@link #accessTokenExpiryTime}, additionally the session tokens/cookies are renewed when the current + * nextAlarmTime is + * above {@link #connectionExpiryTime} + * + * @return {@code true} when the session tokens have been renewed, {@code false} otherwise + * @throws ConnectionException when an error occurred + */ + public boolean renewTokens() throws ConnectionException { + if (System.currentTimeMillis() >= this.accessTokenExpiryTime) { + String renewTokenPostData = "app_name=Amazon%20Alexa" // + + "&app_version=" + AmazonEchoControlBindingConstants.API_VERSION // + + "&di.sdk.version=" + AmazonEchoControlBindingConstants.DI_SDK_VERSION // + + "&source_token=" + URLEncoder.encode(loginData.getRefreshToken(), StandardCharsets.UTF_8) // + + "&package_name=com.amazon.echo" // + + "&di.hw.version=iPhone" // + + "&platform=iOS" // + + "&requested_token_type=access_token"// + + "&source_token_type=refresh_token" // + + "&di.os.name=iOS" // + + "&di.os.version=" + AmazonEchoControlBindingConstants.DI_OS_VERSION // + + "¤t_version=6.12.4"; + + AuthTokenTO tokenResponse = requestBuilder.post("https://api.amazon.com/auth/token") + .withContent(renewTokenPostData).syncSend(AuthTokenTO.class); + + String accessToken = tokenResponse.accessToken; + this.accessToken = accessToken; + if (accessToken == null) { + throw new ConnectionException("Failed to renew access token, no token received."); + } + + // renew at 80% expired + this.accessTokenExpiryTime = System.currentTimeMillis() + (long) ((tokenResponse.expiresIn * 1000.0) / 0.8); + + if (System.currentTimeMillis() > this.connectionExpiryTime) { + exchangeToken(loginData.getRetailDomain()); + } + } + return false; + } + + public boolean isLoggedIn() { + return loginData.getLoginTime() != null; + } + + public String getLoginPage() throws ConnectionException { + // clear session data + logout(false); + + logger.debug("Start Login to {}", getAlexaServer()); + + String mapMdJson = "{\"device_user_dictionary\":[],\"device_registration_data\":{\"software_version\":\"1\"},\"app_identifier\":{\"app_version\":\"2.2.443692\",\"bundle_id\":\"com.amazon.echo\"}}"; + String mapMdCookie = Base64.getEncoder().encodeToString(mapMdJson.getBytes()); + + cookieManager.getCookieStore().add(URI.create("https://www.amazon.com"), new HttpCookie("map-md", mapMdCookie)); + cookieManager.getCookieStore().add(URI.create("https://www.amazon.com"), + new HttpCookie("frc", loginData.getFrc())); + + String url = "https://www.amazon.com/ap/signin" // + + "?openid.return_to=https://www.amazon.com/ap/maplanding" // + + "&openid.assoc_handle=amzn_dp_project_dee_ios" // + + "&openid.identity=http://specs.openid.net/auth/2.0/identifier_select" // + + "&pageId=amzn_dp_project_dee_ios" // + + "&accountStatusPolicy=P1" // + + "&openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select" // + + "&openid.mode=checkid_setup" // + + "&openid.ns.oa2=http://www.amazon.com/ap/ext/oauth/2" // + + "&openid.oa2.client_id=device:" + loginData.getDeviceId() // + + "&openid.ns.pape=http://specs.openid.net/extensions/pape/1.0" // + + "&openid.oa2.response_type=token" // + + "&openid.ns=http://specs.openid.net/auth/2.0&openid.pape.max_auth_age=0" // + + "&openid.oa2.scope=device_auth_access"; + + return requestBuilder.get(url).withHeader("authority", "www.amazon.com").syncSend(String.class); + } + + public boolean verifyLogin() throws ConnectionException { + if (this.loginData.getRefreshToken() == null || !tryGetBootstrap()) { + verifyTime = null; + return false; + } + verifyTime = new Date(); + if (loginData.getLoginTime() == null) { + loginData.setLoginTime(verifyTime); + } + return true; + } + + // current value in compute can be null + private void replaceTimer(TimerType type, @Nullable ScheduledFuture newTimer) { + timers.compute(type, (timerType, oldTimer) -> { + if (oldTimer != null) { + oldTimer.cancel(true); + } + return newTimer; + }); + } + + public void logout(boolean reset) { + if (reset) { + cookieManager = new CookieManager(); + loginData = new LoginData(cookieManager); + } else { + cookieManager.getCookieStore().removeAll(); + // reset all members + loginData.setRefreshToken(null); + loginData.setLoginTime(null); + } + + verifyTime = null; + connectionExpiryTime = 0; + accessTokenExpiryTime = 0; + customerName = null; + accessToken = null; + + replaceTimer(TimerType.ANNOUNCEMENT, null); + announcements.clear(); + replaceTimer(TimerType.TTS, null); + textToSpeeches.clear(); + replaceTimer(TimerType.VOLUME, null); + volumes.clear(); + replaceTimer(TimerType.DEVICES, null); + textCommands.clear(); + replaceTimer(TimerType.TTS, null); + + devices.values().forEach((queueObjects) -> queueObjects.forEach((queueObject) -> { + Future future = queueObject.future; + if (future != null) { + future.cancel(true); + queueObject.future = null; + } + })); + } + + // commands and states + public List getWakeWords() { + try { + return requestBuilder.get(getAlexaServer() + "/api/wake-word?cached=true") + .syncSend(WakeWordsTO.class).wakeWords; + } catch (ConnectionException e) { + logger.info("Getting wake words failed", e); + } + return List.of(); + } + + public List getDeviceList() throws ConnectionException { + DeviceListTO devices = requestBuilder.get(getAlexaServer() + "/api/devices-v2/device?cached=false") + .syncSend(DeviceListTO.class); + // @Nullable because of a limitation of the null-checker, we filter null-serialNumbers before + Set<@Nullable String> serialNumbers = ConcurrentHashMap.newKeySet(); + return devices.devices.stream().filter(d -> d.serialNumber != null && serialNumbers.add(d.serialNumber)) + .toList(); + } + + public Map getSmartHomeDeviceStatesJson(Set devices) + throws ConnectionException { + JsonObject requestObject = new JsonObject(); + JsonArray stateRequests = new JsonArray(); + Map mergedApplianceMap = new HashMap<>(); + for (SmartHomeBaseDevice device : devices) { + String applianceId = device.findId(); + if (applianceId != null) { + JsonObject stateRequest; + if (device instanceof JsonSmartHomeDevice + && ((JsonSmartHomeDevice) device).mergedApplianceIds != null) { + List mergedApplianceIds = Objects + .requireNonNullElse(((JsonSmartHomeDevice) device).mergedApplianceIds, List.of()); + for (String idToMerge : mergedApplianceIds) { + mergedApplianceMap.put(idToMerge, applianceId); + stateRequest = new JsonObject(); + stateRequest.addProperty("entityId", idToMerge); + stateRequest.addProperty("entityType", "APPLIANCE"); + stateRequests.add(stateRequest); + } + } else { + stateRequest = new JsonObject(); + stateRequest.addProperty("entityId", applianceId); + stateRequest.addProperty("entityType", "APPLIANCE"); + stateRequests.add(stateRequest); + } + } + } + requestObject.add("stateRequests", stateRequests); + JsonObject responseObject = requestBuilder.post(getAlexaServer() + "/api/phoenix/state") + .withContent(requestObject).syncSend(JsonObject.class); + + JsonArray deviceStates = (JsonArray) responseObject.get("deviceStates"); + Map result = new HashMap<>(); + for (JsonElement deviceState : deviceStates) { + JsonObject deviceStateObject = deviceState.getAsJsonObject(); + JsonObject entity = deviceStateObject.get("entity").getAsJsonObject(); + String applianceId = entity.get("entityId").getAsString(); + JsonElement capabilityState = deviceStateObject.get("capabilityStates"); + if (capabilityState != null && capabilityState.isJsonArray()) { + String realApplianceId = mergedApplianceMap.get(applianceId); + if (realApplianceId != null) { + var capabilityArray = result.get(realApplianceId); + if (capabilityArray != null) { + capabilityArray.addAll(capabilityState.getAsJsonArray()); + result.put(realApplianceId, capabilityArray); + } else { + result.put(realApplianceId, capabilityState.getAsJsonArray()); + } + } else { + result.put(applianceId, capabilityState.getAsJsonArray()); + } + } + } + return result; + } + + public PlayerStateTO getPlayerState(DeviceTO device) throws ConnectionException { + return requestBuilder.get(getAlexaServer() + "/api/np/player?deviceSerialNumber=" + device.serialNumber + + "&deviceType=" + device.deviceType + "&screenWidth=1440").syncSend(PlayerStateTO.class); + } + + public List getMediaSessions(DeviceTO device) { + try { + String url = getAlexaServer() + "/api/np/list-media-sessions?deviceSerialNumber=" + device.serialNumber + + "&deviceType=" + device.deviceType; + return requestBuilder.get(url).syncSend(ListMediaSessionTO.class).mediaSessionList; + } catch (ConnectionException e) { + logger.warn("Failed to update media sessions for {}: {}", device.serialNumber, e.getMessage()); + } + return List.of(); + } + + public List getActivities(long startTime, long endTime) { + try { + String url = getRetailUrl() + "/alexa-privacy/apd/rvh/customer-history-records?startTime=" + startTime + + "&endTime=" + endTime + "&maxRecordSize=1"; + CustomerHistoryRecordsTO customerHistoryRecords = requestBuilder.get(url) + .syncSend(CustomerHistoryRecordsTO.class); + return customerHistoryRecords.customerHistoryRecords.stream() + .filter(r -> !"DEVICE_ARBITRATION".equals(r.utteranceType)) + .sorted(Comparator.comparing(r -> r.timestamp)).toList(); + } catch (ConnectionException e) { + logger.info("getting activities failed", e); + } + return List.of(); + } + + public @Nullable NamedListsInfoTO getNamedListInfo(String listId) { + try { + String url = getAlexaServer() + "/api/namedLists/" + listId + "?_=" + System.currentTimeMillis(); + return requestBuilder.get(url).syncSend(NamedListsInfoTO.class); + } catch (ConnectionException e) { + logger.info("getting information for list {} failed", listId, e); + } + return null; + } + + public List getNamedListItems(String listId) { + try { + String url = getAlexaServer() + "/api/namedLists/" + listId + "/items?_=" + System.currentTimeMillis(); + return requestBuilder.get(url).syncSend(NamedListsItemsTO.class).list; + } catch (ConnectionException e) { + logger.info("getting items from list '{}' failed", listId, e); + } + return List.of(); + } + + public List getBluetoothConnectionStates() { + try { + String url = getAlexaServer() + "/api/bluetooth?cached=true"; + return requestBuilder.get(url).syncSend(BluetoothStatesTO.class).bluetoothStates; + } catch (ConnectionException e) { + logger.debug("Failed to get bluetooth state: {}", e.getMessage()); + return List.of(); + } + } + + public void command(DeviceTO device, Object command) throws ConnectionException { + String url = getAlexaServer() + "/api/np/command?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + + device.deviceType; + requestBuilder.post(url).withContent(command).retry(false).syncSend(); + } + + public void smartHomeCommand(String entityId, String action, Map values) + throws IOException, InterruptedException { + String url = getAlexaServer() + "/api/phoenix/state"; + + JsonObject json = new JsonObject(); + JsonArray controlRequests = new JsonArray(); + JsonObject controlRequest = new JsonObject(); + controlRequest.addProperty("entityId", entityId); + controlRequest.addProperty("entityType", "APPLIANCE"); + JsonObject parameters = new JsonObject(); + parameters.addProperty("action", action); + if (!values.isEmpty()) { + values.forEach((property, value) -> { + if (value instanceof QuantityType) { + JsonObject propertyObj = new JsonObject(); + propertyObj.addProperty("value", Double.toString(((QuantityType) value).doubleValue())); + propertyObj.addProperty("scale", + ((QuantityType) value).getUnit().equals(SIUnits.CELSIUS) ? "celsius" : "fahrenheit"); + parameters.add(property, propertyObj); + } else if (value instanceof Boolean) { + parameters.addProperty(property, (boolean) value); + } else if (value instanceof String) { + parameters.addProperty(property, (String) value); + } else if (value instanceof StringType) { + JsonObject propertyObj = new JsonObject(); + propertyObj.addProperty("value", value.toString()); + parameters.add(property, propertyObj); + } else if (value instanceof Number) { + parameters.addProperty(property, (Number) value); + } else if (value instanceof Character) { + parameters.addProperty(property, (Character) value); + } else if (value instanceof JsonElement) { + parameters.add(property, (JsonElement) value); + } + }); + } + controlRequest.add("parameters", parameters); + controlRequests.add(controlRequest); + json.add("controlRequests", controlRequests); + + try { + JsonObject result = requestBuilder.put(url).withContent(json).syncSend(JsonObject.class); + if (result == null) { + return; + } + JsonElement errors = result.get("errors"); + if (errors != null && errors.isJsonArray()) { + JsonArray errorList = errors.getAsJsonArray(); + if (!errorList.isEmpty()) { + logger.warn("Smart home device command failed. {}", + StreamSupport.stream(errorList.spliterator(), false).map(JsonElement::toString) + .collect(Collectors.joining(" / "))); + } + } + } catch (ConnectionException e) { + logger.warn("Request to URL '{}' failed: {}", url, e.getMessage()); + } + } + + public void setNotificationVolume(DeviceTO device, int volume) throws ConnectionException { + String url = getAlexaServer() + "/api/device-notification-state/" + device.deviceType + "/" + + device.softwareVersion + "/" + device.serialNumber; + NotificationStateTO command = new NotificationStateTO(); + command.deviceSerialNumber = device.serialNumber; + command.deviceType = device.deviceType; + command.deviceSerialNumber = device.softwareVersion; + command.volumeLevel = volume; + requestBuilder.put(url).withContent(command).retry(false).syncSend(); + } + + public void setAscendingAlarm(DeviceTO device, boolean ascendingAlarm) throws ConnectionException { + String url = getAlexaServer() + "/api/ascending-alarm/" + device.serialNumber; + AscendingAlarmModelTO command = new AscendingAlarmModelTO(); + command.ascendingAlarmEnabled = ascendingAlarm; + command.deviceSerialNumber = device.serialNumber; + command.deviceType = device.deviceType; + requestBuilder.put(url).withContent(command).retry(false).syncSend(); + } + + public void setDoNotDisturb(DeviceTO device, boolean doNotDisturb) throws ConnectionException { + String url = getAlexaServer() + "/api/dnd/status"; + DoNotDisturbDeviceStatusTO command = new DoNotDisturbDeviceStatusTO(); + command.enabled = doNotDisturb; + command.deviceSerialNumber = device.serialNumber; + command.deviceType = device.deviceType; + requestBuilder.put(url).withContent(command).retry(false).syncSend(); + } + + public List getDeviceNotificationStates() { + try { + return requestBuilder.get(getAlexaServer() + "/api/device-notification-state") + .syncSend(DeviceNotificationStatesTO.class).deviceNotificationStates; + } catch (ConnectionException e) { + logger.info("Error getting device notification states", e); + } + return List.of(); + } + + public List getAscendingAlarms() { + try { + return requestBuilder.get(getAlexaServer() + "/api/ascending-alarm") + .syncSend(AscendingAlarmModelsTO.class).ascendingAlarmModelList; + } catch (ConnectionException e) { + logger.info("Error getting ascending alarm states", e); + } + return List.of(); + } + + public List getDoNotDisturbs() { + try { + return requestBuilder.get(getAlexaServer() + "/api/dnd/device-status-list") + .syncSend(DoNotDisturbDeviceStatusesTO.class).doNotDisturbDeviceStatusList; + } catch (ConnectionException e) { + logger.info("Error getting do not disturb status list", e); + } + return List.of(); + } + + public void bluetooth(DeviceTO device, @Nullable String address) throws ConnectionException { + if (address == null || address.isEmpty()) { + String url = getAlexaServer() + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" + + device.serialNumber; + // disconnect + requestBuilder.post(url).retry(false).syncSend(); + } else { + String url = getAlexaServer() + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber; + requestBuilder.post(url).withContent(Map.of("bluetoothDeviceAddress", address)).retry(false).syncSend(); + } + } + + public void announcement(DeviceTO device, String speak, String bodyText, @Nullable String title, + @Nullable Integer ttsVolume, @Nullable Integer standardVolume) { + String trimmedSpeak = speak.replaceAll("\\s+", " ").trim(); + String trimmedBodyText = bodyText.replaceAll("\\s+", " ").trim(); + String plainSpeak = trimmedSpeak.replaceAll("<.+?>", "").trim(); + String plainBodyText = trimmedBodyText.replaceAll("<.+?>", "").trim(); + if (plainSpeak.isEmpty() && plainBodyText.isEmpty()) { + return; + } + String escapedSpeak = trimmedSpeak.replace(plainSpeak, + XmlEscape.escapeXml10(plainSpeak, XmlEscapeType.CHARACTER_ENTITY_REFERENCES_DEFAULT_TO_HEXA, + XmlEscapeLevel.LEVEL_1_ONLY_MARKUP_SIGNIFICANT)); + // we lock announcements until we have finished adding this one + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.ANNOUNCEMENT, k -> new ReentrantLock())); + lock.lock(); + try { + AnnouncementWrapper announcement = Objects + .requireNonNull(announcements.computeIfAbsent(Objects.hash(escapedSpeak, plainBodyText, title), + k -> new AnnouncementWrapper(escapedSpeak, plainBodyText, title))); + announcement.add(device, ttsVolume, standardVolume); + + // schedule an announcement only if it has not been scheduled before + timers.computeIfAbsent(TimerType.ANNOUNCEMENT, + k -> scheduler.schedule(this::sendAnnouncement, 500, TimeUnit.MILLISECONDS)); + } finally { + lock.unlock(); + } + } + + private void sendAnnouncement() { + // we lock new announcements until we have dispatched everything + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.ANNOUNCEMENT, k -> new ReentrantLock())); + lock.lock(); + try { + Iterator iterator = announcements.values().iterator(); + while (iterator.hasNext()) { + AnnouncementWrapper announcement = iterator.next(); + try { + List devices = announcement.getDevices(); + if (!devices.isEmpty()) { + AnnouncementTO announcementTO = new AnnouncementTO(); + announcementTO.content = List.of(announcement.toAnnouncementTO()); + announcementTO.customerId = loginData.getAccountCustomerId(); + announcementTO.target.customerId = devices.get(0).deviceOwnerCustomerId; + announcementTO.target.devices = devices.stream().map(TOMapper::mapAnnouncementTargetDevice) + .toList(); + executeSequenceCommandWithVolume(devices, "AlexaAnnouncement", + TOMapper.mapToMap(gson, announcementTO), announcement.getTtsVolumes(), + announcement.getStandardVolumes()); + } + } catch (Exception e) { + logger.warn("send announcement fails with unexpected error", e); + } + iterator.remove(); + } + } finally { + // the timer is done anyway immediately after we unlock + timers.remove(TimerType.ANNOUNCEMENT); + lock.unlock(); + } + } + + public void textToSpeech(DeviceTO device, String text, @Nullable Integer ttsVolume, + @Nullable Integer standardVolume) { + String trimmedText = text.replaceAll("\\s+", " ").trim(); + String plainText = trimmedText.replaceAll("<.+?>", "").trim(); + if (plainText.isEmpty()) { + return; + } + String escapedText = trimmedText.replace(plainText, + XmlEscape.escapeXml10(plainText, XmlEscapeType.CHARACTER_ENTITY_REFERENCES_DEFAULT_TO_HEXA, + XmlEscapeLevel.LEVEL_1_ONLY_MARKUP_SIGNIFICANT)); + // we lock TTS until we have finished adding this one + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TTS, k -> new ReentrantLock())); + lock.lock(); + try { + TextWrapper textToSpeech = Objects.requireNonNull( + textToSpeeches.computeIfAbsent(Objects.hash(escapedText), k -> new TextWrapper(escapedText))); + textToSpeech.add(device, ttsVolume, standardVolume); + // schedule a TTS only if it has not been scheduled before + timers.computeIfAbsent(TimerType.TTS, + k -> scheduler.schedule(this::sendTextToSpeech, 500, TimeUnit.MILLISECONDS)); + } finally { + lock.unlock(); + } + } + + private void sendTextToSpeech() { + // we lock new TTS until we have dispatched everything + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TTS, k -> new ReentrantLock())); + lock.lock(); + try { + Iterator iterator = textToSpeeches.values().iterator(); + while (iterator.hasNext()) { + TextWrapper textToSpeech = iterator.next(); + logger.trace("Executing textToSpeech {}", textToSpeech); + try { + List devices = textToSpeech.getDevices(); + if (!devices.isEmpty()) { + executeSequenceCommandWithVolume(devices, "Alexa.Speak", + Map.of("textToSpeak", textToSpeech.getText()), textToSpeech.getTtsVolumes(), + textToSpeech.getStandardVolumes()); + } + } catch (RuntimeException e) { + logger.warn("Send textToSpeech failed with unexpected error", e); + } + iterator.remove(); + } + } finally { + // the timer is done anyway immediately after we unlock + timers.remove(TimerType.TTS); + lock.unlock(); + } + } + + public void textCommand(DeviceTO device, String text, @Nullable Integer ttsVolume, + @Nullable Integer standardVolume) { + String trimmedText = text.replaceAll("\\s+", " ").trim(); + String plainText = trimmedText.replaceAll("<.+?>", "").trim(); + if (plainText.isEmpty()) { + return; + } + String escapedText = trimmedText.replace(plainText, JsonEscape.escapeJson(plainText, + JsonEscapeType.SINGLE_ESCAPE_CHARS_DEFAULT_TO_UHEXA, JsonEscapeLevel.LEVEL_1_BASIC_ESCAPE_SET)); + + // we lock TextCommands until we have finished adding this one + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TEXT_COMMAND, k -> new ReentrantLock())); + lock.lock(); + try { + TextWrapper textWrapper = Objects.requireNonNull( + textCommands.computeIfAbsent(Objects.hash(escapedText), k -> new TextWrapper(escapedText))); + textWrapper.add(device, ttsVolume, standardVolume); + // schedule a TextCommand only if it has not been scheduled before + timers.computeIfAbsent(TimerType.TEXT_COMMAND, + k -> scheduler.schedule(this::sendTextCommand, 500, TimeUnit.MILLISECONDS)); + } finally { + lock.unlock(); + } + } + + private void sendTextCommand() { + // we lock new textCommands until we have dispatched everything + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TEXT_COMMAND, k -> new ReentrantLock())); + lock.lock(); + + try { + Iterator iterator = textCommands.values().iterator(); + while (iterator.hasNext()) { + TextWrapper textCommand = iterator.next(); + logger.trace("Executing textCommand {}", textCommand); + try { + List devices = textCommand.getDevices(); + if (!devices.isEmpty()) { + executeSequenceCommandWithVolume(devices, "Alexa.TextCommand", + Map.of("text", textCommand.getText()), textCommand.getTtsVolumes(), + textCommand.getStandardVolumes()); + } + } catch (RuntimeException e) { + logger.warn("Sending textCommand failed with unexpected error", e); + } + iterator.remove(); + } + } finally { + // the timer is done anyway immediately after we unlock + timers.remove(TimerType.TEXT_COMMAND); + lock.unlock(); + } + } + + public void setVolume(DeviceTO device, int vol) { + // we lock volume until we have finished adding this one + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.VOLUME, k -> new ReentrantLock())); + lock.lock(); + try { + Volume volume = Objects.requireNonNull(volumes.computeIfAbsent(vol, k -> new Volume())); + volume.devices.add(device); + volume.volumes.add(vol); + // schedule a TTS only if it has not been scheduled before + timers.computeIfAbsent(TimerType.VOLUME, + k -> scheduler.schedule(this::sendVolume, 500, TimeUnit.MILLISECONDS)); + } finally { + lock.unlock(); + } + } + + private void sendVolume() { + // we lock new volume until we have dispatched everything + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.VOLUME, k -> new ReentrantLock())); + lock.lock(); + try { + Iterator iterator = volumes.values().iterator(); + while (iterator.hasNext()) { + Volume volume = iterator.next(); + try { + List devices = volume.devices; + if (!devices.isEmpty()) { + executeSequenceCommandWithVolume(devices, null, Map.of(), volume.volumes, List.of()); + } + } catch (Exception e) { + logger.warn("send volume fails with unexpected error", e); + } + iterator.remove(); + } + } finally { + // the timer is done anyway immediately after we unlock + timers.remove(TimerType.VOLUME); + lock.unlock(); + } + } + + private void executeSequenceCommandWithVolume(List devices, @Nullable String command, + Map parameters, List<@Nullable Integer> ttsVolumes, + List<@Nullable Integer> standardVolumes) { + JsonArray serialNodesToExecute = new JsonArray(); + + JsonArray ttsVolumeNodesToExecute = new JsonArray(); + for (int i = 0; i < devices.size(); i++) { + Integer ttsVolume = ttsVolumes.size() > i ? ttsVolumes.get(i) : null; + Integer standardVolume = standardVolumes.size() > i ? standardVolumes.get(i) : null; + if (ttsVolume != null && !ttsVolume.equals(standardVolume)) { + ttsVolumeNodesToExecute.add(createExecutionNode(devices.get(i).deviceType, devices.get(i).serialNumber, + "Alexa.DeviceControls.Volume", Map.of("value", ttsVolume))); + } + } + if (!ttsVolumeNodesToExecute.isEmpty()) { + JsonObject parallelNodesToExecute = new JsonObject(); + parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode"); + parallelNodesToExecute.add("nodesToExecute", ttsVolumeNodesToExecute); + serialNodesToExecute.add(parallelNodesToExecute); + } + + if (command != null && !parameters.isEmpty()) { + JsonArray commandNodesToExecute = new JsonArray(); + if ("Alexa.Speak".equals(command) || "Alexa.TextCommand".equals(command)) { + for (DeviceTO device : devices) { + commandNodesToExecute + .add(createExecutionNode(device.deviceType, device.serialNumber, command, parameters)); + } + } else { + commandNodesToExecute.add(createExecutionNode(devices.get(0).deviceType, devices.get(0).serialNumber, + command, parameters)); + } + if (!commandNodesToExecute.isEmpty()) { + JsonObject parallelNodesToExecute = new JsonObject(); + parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode"); + parallelNodesToExecute.add("nodesToExecute", commandNodesToExecute); + serialNodesToExecute.add(parallelNodesToExecute); + } + } + + JsonArray standardVolumeNodesToExecute = new JsonArray(); + for (int i = 0; i < devices.size(); i++) { + Integer standardVolume = standardVolumes.size() > i ? standardVolumes.get(i) : null; + Integer ttsVolume = ttsVolumes.size() > i ? ttsVolumes.get(i) : null; + if (ttsVolume != null && standardVolume != null && !standardVolume.equals(ttsVolume)) { + standardVolumeNodesToExecute.add(createExecutionNode(devices.get(i).deviceType, + devices.get(i).serialNumber, "Alexa.DeviceControls.Volume", Map.of("value", standardVolume))); + } + } + if (!standardVolumeNodesToExecute.isEmpty() && !"AlexaAnnouncement".equals(command)) { + JsonObject parallelNodesToExecute = new JsonObject(); + parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode"); + parallelNodesToExecute.add("nodesToExecute", standardVolumeNodesToExecute); + serialNodesToExecute.add(parallelNodesToExecute); + } + + if (!serialNodesToExecute.isEmpty()) { + executeSequenceNodes(devices.stream().map(d -> d.serialNumber).toList(), serialNodesToExecute, false); + + if (!standardVolumeNodesToExecute.isEmpty() && "AlexaAnnouncement".equals(command)) { + executeSequenceNodes(devices.stream().map(d -> d.serialNumber).toList(), standardVolumeNodesToExecute, + true); + } + } + } + + // commands: Alexa.Weather.Play, Alexa.Traffic.Play, Alexa.FlashBriefing.Play, + // Alexa.GoodMorning.Play, + // Alexa.SingASong.Play, Alexa.TellStory.Play, Alexa.Speak (textToSpeach) + public void executeSequenceCommand(DeviceTO device, String command, Map parameters) { + JsonObject nodeToExecute = createExecutionNode(device.deviceType, device.serialNumber, command, parameters); + executeSequenceNode(List.of(device.serialNumber), nodeToExecute); + } + + private void executeSequenceNode(List serialNumbers, JsonObject nodeToExecute) { + QueueObject queueObject = new QueueObject(); + queueObject.deviceSerialNumbers = serialNumbers; + queueObject.nodeToExecute = nodeToExecute; + List serials = new ArrayList<>(); + for (String serialNumber : serialNumbers) { + if (serialNumber != null) { + Objects.requireNonNull(this.devices.computeIfAbsent(serialNumber, k -> new LinkedBlockingQueue<>())) + .offer(queueObject); + serials.add(serialNumber); + } + } + logger.debug("Added {} device(s) {} to queue", queueObject.hashCode(), serials); + } + + private void handleExecuteSequenceNode() { + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.DEVICES, k -> new ReentrantLock())); + if (lock.tryLock()) { + try { + for (String serialNumber : devices.keySet()) { + LinkedBlockingQueue queueObjects = devices.get(serialNumber); + if (queueObjects != null) { + QueueObject queueObject = queueObjects.peek(); + if (queueObject != null) { + Future future = queueObject.future; + if (future == null || future.isDone()) { + boolean execute = true; + List serials = new ArrayList<>(); + for (String tmpDevice : queueObject.deviceSerialNumbers) { + if (Objects.equals(tmpDevice, serialNumber)) { + LinkedBlockingQueue tmpQueueObjects = devices.get(tmpDevice); + if (tmpQueueObjects != null) { + QueueObject tmpQueueObject = tmpQueueObjects.peek(); + Future tmpFuture = null; + if (tmpQueueObject != null) { + tmpFuture = tmpQueueObject.future; + } + if (!queueObject.equals(tmpQueueObject) + || (tmpFuture != null && !tmpFuture.isDone())) { + execute = false; + break; + } + + serials.add(tmpDevice); + } + } + } + if (execute) { + queueObject.future = scheduler.submit(() -> queuedExecuteSequenceNode(queueObject)); + logger.debug("thread {} device(s) {}", queueObject.hashCode(), serials); + } + } + } + } + } + } finally { + lock.unlock(); + } + } + } + + private void queuedExecuteSequenceNode(QueueObject queueObject) { + JsonObject nodeToExecute = queueObject.nodeToExecute; + ExecutionNodeObject executionNodeObject = getExecutionNodeObject(nodeToExecute); + List types = executionNodeObject.types; + long delay = types.contains("Alexa.DeviceControls.Volume") ? 2000 : 0; + delay += types.contains("Announcement") ? 3000 : 2000; + + try { + JsonObject sequenceJson = new JsonObject(); + sequenceJson.addProperty("@type", "com.amazon.alexa.behaviors.model.Sequence"); + sequenceJson.add("startNode", nodeToExecute); + + StartRoutineTO request = new StartRoutineTO(); + request.sequenceJson = gson.toJson(sequenceJson); + + String text = executionNodeObject.text; + if (text != null) { + text = text.replaceAll("<.+?>", " ").replaceAll("\\s+", " ").trim(); + delay += text.length() * 150L; + } + requestBuilder.post(getAlexaServer() + "/api/behaviors/preview").withContent(request).syncSend(); + + Thread.sleep(delay); + } catch (ConnectionException | InterruptedException e) { + logger.warn("execute sequence node fails with unexpected error", e); + } finally { + removeObjectFromQueueAfterExecutionCompletion(queueObject); + } + } + + private void removeObjectFromQueueAfterExecutionCompletion(QueueObject queueObject) { + List serials = new ArrayList<>(); + for (String serialNumber : queueObject.deviceSerialNumbers) { + LinkedBlockingQueue queue = devices.get(serialNumber); + if (queue != null) { + queue.remove(queueObject); + } + serials.add(serialNumber); + } + logger.debug("Removed {} device(s) {} from queue", queueObject.hashCode(), serials); + } + + private void executeSequenceNodes(List serialNumbers, JsonArray nodesToExecute, boolean parallel) { + JsonObject serialNode = new JsonObject(); + if (parallel) { + serialNode.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode"); + } else { + serialNode.addProperty("@type", "com.amazon.alexa.behaviors.model.SerialNode"); + } + + serialNode.add("nodesToExecute", nodesToExecute); + + executeSequenceNode(serialNumbers, serialNode); + } + + private JsonObject createExecutionNode(String deviceType, String serialNumber, String command, + Map parameters) { + JsonObject operationPayload = new JsonObject(); + operationPayload.addProperty("deviceType", deviceType); + operationPayload.addProperty("deviceSerialNumber", serialNumber); + operationPayload.addProperty("locale", ""); + operationPayload.addProperty("customerId", loginData.getAccountCustomerId()); + for (String key : parameters.keySet()) { + Object value = parameters.get(key); + if (value instanceof String) { + operationPayload.addProperty(key, (String) value); + } else if (value instanceof Number) { + operationPayload.addProperty(key, (Number) value); + } else if (value instanceof Boolean) { + operationPayload.addProperty(key, (Boolean) value); + } else if (value instanceof Character) { + operationPayload.addProperty(key, (Character) value); + } else { + operationPayload.add(key, gson.toJsonTree(value)); + } + } + + JsonObject nodeToExecute = new JsonObject(); + nodeToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"); + nodeToExecute.addProperty("type", command); + if ("Alexa.TextCommand".equals(command)) { + nodeToExecute.addProperty("skillId", "amzn1.ask.1p.tellalexa"); + } + nodeToExecute.add("operationPayload", operationPayload); + return nodeToExecute; + } + + private ExecutionNodeObject getExecutionNodeObject(JsonObject nodeToExecute) { + ExecutionNodeObject executionNodeObject = new ExecutionNodeObject(); + if (nodeToExecute.has("nodesToExecute")) { + JsonArray serialNodesToExecute = nodeToExecute.getAsJsonArray("nodesToExecute"); + if (serialNodesToExecute != null && !serialNodesToExecute.isEmpty()) { + for (int i = 0; i < serialNodesToExecute.size(); i++) { + JsonObject serialNodesToExecuteJsonObject = serialNodesToExecute.get(i).getAsJsonObject(); + if (serialNodesToExecuteJsonObject.has("nodesToExecute")) { + JsonArray parallelNodesToExecute = serialNodesToExecuteJsonObject + .getAsJsonArray("nodesToExecute"); + if (parallelNodesToExecute != null && !parallelNodesToExecute.isEmpty()) { + JsonObject parallelNodesToExecuteJsonObject = parallelNodesToExecute.get(0) + .getAsJsonObject(); + if (processNodesToExecuteJsonObject(executionNodeObject, + parallelNodesToExecuteJsonObject)) { + break; + } + } + } else { + if (processNodesToExecuteJsonObject(executionNodeObject, serialNodesToExecuteJsonObject)) { + break; + } + } + } + } + } + + return executionNodeObject; + } + + private boolean processNodesToExecuteJsonObject(ExecutionNodeObject executionNodeObject, + JsonObject nodesToExecuteJsonObject) { + if (nodesToExecuteJsonObject.has("type")) { + executionNodeObject.types.add(nodesToExecuteJsonObject.get("type").getAsString()); + if (nodesToExecuteJsonObject.has("operationPayload")) { + JsonObject operationPayload = nodesToExecuteJsonObject.getAsJsonObject("operationPayload"); + if (operationPayload != null) { + if (operationPayload.has("textToSpeak")) { + executionNodeObject.text = operationPayload.get("textToSpeak").getAsString(); + return true; + } else if (operationPayload.has("text")) { + executionNodeObject.text = operationPayload.get("text").getAsString(); + return true; + } else if (operationPayload.has("content")) { + JsonArray content = operationPayload.getAsJsonArray("content"); + if (content != null && !content.isEmpty()) { + JsonObject contentJsonObject = content.get(0).getAsJsonObject(); + if (contentJsonObject.has("speak")) { + JsonObject speak = contentJsonObject.getAsJsonObject("speak"); + if (speak != null && speak.has("value")) { + executionNodeObject.text = speak.get("value").getAsString(); + return true; + } + } + } + } + } + } + } + return false; + } + + public void startRoutine(DeviceTO device, String utterance) throws ConnectionException { + AutomationTO found = null; + String deviceLocale = ""; + List routines = getAutomations(); + + for (AutomationTO routine : routines) { + if (routine.sequence != null) { + for (AutomationTriggerTO trigger : routine.triggers) { + AutomationPayloadTO payload = trigger.payload; + if (payload == null) { + continue; + } + String payloadUtterance = payload.utterance; + if (utterance.equalsIgnoreCase(payloadUtterance)) { + found = routine; + deviceLocale = payload.locale; + break; + } + } + } + } + if (found != null) { + String sequenceJson = gson.toJson(found.sequence); + + StartRoutineTO request = new StartRoutineTO(); + request.behaviorId = found.automationId; + + // replace tokens + // "deviceType":"ALEXA_CURRENT_DEVICE_TYPE" + String deviceType = "\"deviceType\":\"ALEXA_CURRENT_DEVICE_TYPE\""; + String newDeviceType = "\"deviceType\":\"" + device.deviceType + "\""; + sequenceJson = sequenceJson.replace(deviceType, newDeviceType); + + // "deviceSerialNumber":"ALEXA_CURRENT_DSN" + String deviceSerial = "\"deviceSerialNumber\":\"ALEXA_CURRENT_DSN\""; + String newDeviceSerial = "\"deviceSerialNumber\":\"" + device.serialNumber + "\""; + sequenceJson = sequenceJson.replace(deviceSerial, newDeviceSerial); + + // "customerId": "ALEXA_CUSTOMER_ID" + String customerId = "\"customerId\":\"ALEXA_CUSTOMER_ID\""; + String newCustomerId = "\"customerId\":\"" + loginData.getAccountCustomerId() + "\""; + sequenceJson = sequenceJson.replace(customerId, newCustomerId); + + // "locale": "ALEXA_CURRENT_LOCALE" + String locale = "\"locale\":\"ALEXA_CURRENT_LOCALE\""; + String newlocale = deviceLocale != null && !deviceLocale.isEmpty() ? "\"locale\":\"" + deviceLocale + "\"" + : "\"locale\":null"; + sequenceJson = sequenceJson.replace(locale, newlocale); + + request.sequenceJson = sequenceJson; + + requestBuilder.post(getAlexaServer() + "/api/behaviors/preview").withContent(request).syncSend(); + } else { + logger.warn("Routine {} not found", utterance); + } + } + + public List getAutomations() throws ConnectionException { + return requestBuilder.get(getAlexaServer() + "/api/behaviors/v2/automations?limit=2000") + .syncSend(AutomationTO.LIST_TYPE_TOKEN); + } + + public List getEnabledFlashBriefings() { + try { + return requestBuilder.get(getAlexaServer() + "/api/content-skills/enabled-feeds") + .syncSend(EnabledFeedsTO.class).enabledFeeds; + } catch (ConnectionException e) { + logger.warn("Failed to get enabled feeds: {}", e.getMessage()); + } + return List.of(); + } + + public void setEnabledFlashBriefings(List enabledFlashBriefing) throws ConnectionException { + EnabledFeedsTO enabled = new EnabledFeedsTO(); + enabled.enabledFeeds = enabledFlashBriefing; + requestBuilder.post(getAlexaServer() + "/api/content-skills/enabled-feeds").withContent(enabled).retry(false) + .syncSend(); + } + + public List getNotificationSounds(DeviceTO device) { + try { + return requestBuilder + .get(getAlexaServer() + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber + + "&deviceType=" + device.deviceType + "&softwareVersion=" + device.softwareVersion) + .syncSend(NotificationSoundResponseTO.class).notificationSounds; + } catch (ConnectionException e) { + return List.of(); + } + } + + public List getNotifications() { + try { + return requestBuilder.get(getAlexaServer() + "/api/notifications") + .syncSend(NotificationListResponseTO.class).notifications; + } catch (ConnectionException e) { + logger.warn("Failed to get notifications: {}", e.getMessage()); + } + return List.of(); + } + + public NotificationTO getNotification(String notificationId) throws ConnectionException { + String url = getAlexaServer() + "/api/notifications/" + notificationId; + return requestBuilder.get(url).syncSend(NotificationTO.class); + } + + public @Nullable NotificationTO createNotification(DeviceTO device, String type, @Nullable String label, + @Nullable NotificationSoundTO sound) throws ConnectionException { + Instant createdTime = Instant.now(); + // add 5 seconds, because amazon does not accept calls for times in the past (compared with the server + // nextAlarmTime) + Instant alarmTime = createdTime.plusSeconds(5); + ZonedDateTime zonedAlarmTime = alarmTime.atZone(ZoneId.systemDefault()); + + NotificationTO request = new NotificationTO(); + request.status = "ON"; + request.deviceSerialNumber = device.serialNumber; + request.deviceType = device.deviceType; + request.createdDate = createdTime.getEpochSecond() * 1000; + request.alarmTime = alarmTime.getEpochSecond() * 1000; + request.reminderLabel = label; + request.sound = sound; + request.originalDate = zonedAlarmTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + request.originalTime = zonedAlarmTime.format(DateTimeFormatter.ofPattern("HH:mm:ss.SSSS")); + request.type = type; + request.id = "create" + type; + request.isSaveInFlight = true; + request.isRecurring = false; + + String url = getAlexaServer() + "/api/notifications/createReminder"; + return requestBuilder.put(url).withContent(request).syncSend(NotificationTO.class); + } + + public void deleteNotification(String notificationId) { + try { + requestBuilder.delete(getAlexaServer() + "/api/notifications/" + notificationId).syncSend(); + } catch (ConnectionException e) { + logger.warn("Failed to delete notification {}: {}", notificationId, e.getMessage()); + } + } + + public List getMusicProviders() { + try { + return requestBuilder.get(getAlexaServer() + "/api/behaviors/entities?skillId=amzn1.ask.1p.music") + .withHeader("Routines-Version", "1.1.218665").syncSend(MusicProviderTO.LIST_TYPE_TOKEN); + } catch (ConnectionException e) { + logger.warn("Failed to get music providers: {}", e.getMessage()); + } + return List.of(); + } + + public void playMusicVoiceCommand(DeviceTO device, String providerId, String voiceCommand) + throws ConnectionException { + PlaySearchPhraseTO playSearchPhrase = new PlaySearchPhraseTO(); + playSearchPhrase.customerId = loginData.getAccountCustomerId(); + playSearchPhrase.musicProviderId = providerId; + playSearchPhrase.searchPhrase = voiceCommand; + + BehaviorOperationValidateTO validationRequest = new BehaviorOperationValidateTO(); + validationRequest.type = "Alexa.Music.PlaySearchPhrase"; + validationRequest.operationPayload = gson.toJson(playSearchPhrase); + + PlaySearchPhraseTO validatedOperationPayload = requestBuilder + .post(getAlexaServer() + "/api/behaviors/operation/validate").withContent(validationRequest) + .syncSend(PlaySearchPhraseTO.VALIDATION_RESULT_TO_TYPE_TOKEN).operationPayload; + + if (validatedOperationPayload != null) { + playSearchPhrase.sanitizedSearchPhrase = validatedOperationPayload.sanitizedSearchPhrase; + playSearchPhrase.searchPhrase = validatedOperationPayload.searchPhrase; + } + + playSearchPhrase.locale = null; + playSearchPhrase.deviceSerialNumber = device.serialNumber; + playSearchPhrase.deviceType = device.deviceType; + + JsonObject sequenceJson = new JsonObject(); + sequenceJson.addProperty("@type", "com.amazon.alexa.behaviors.model.Sequence"); + JsonObject startNodeJson = new JsonObject(); + startNodeJson.addProperty("@type", "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"); + startNodeJson.addProperty("type", "Alexa.Music.PlaySearchPhrase"); + startNodeJson.add("operationPayload", gson.toJsonTree(playSearchPhrase)); + sequenceJson.add("startNode", startNodeJson); + + StartRoutineTO startRoutineRequest = new StartRoutineTO(); + startRoutineRequest.sequenceJson = sequenceJson.toString(); + startRoutineRequest.status = null; + + requestBuilder.post(getAlexaServer() + "/api/behaviors/preview").withContent(startRoutineRequest).syncSend(); + } + + public Optional getEqualizer(DeviceTO device) { + try { + return Optional.of(requestBuilder + .get(getAlexaServer() + "/api/equalizer/" + device.serialNumber + "/" + device.deviceType) + .syncSend(EqualizerTO.class)); + } catch (ConnectionException e) { + return Optional.empty(); + } + } + + public boolean setEqualizer(DeviceTO device, EqualizerTO settings) { + try { + requestBuilder.post(getAlexaServer() + "/api/equalizer/" + device.serialNumber + "/" + device.deviceType) + .withContent(settings).retry(false).syncSend(); + return true; + } catch (ConnectionException e) { + return false; + } + } + + private static class Volume { + public List devices = new ArrayList<>(); + public List<@Nullable Integer> volumes = new ArrayList<>(); + } + + private static class QueueObject { + public @Nullable Future future; + public List deviceSerialNumbers = List.of(); + public JsonObject nodeToExecute = new JsonObject(); + } + + private static class ExecutionNodeObject { + public List types = new ArrayList<>(); + public @Nullable String text; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/LoginData.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/LoginData.java new file mode 100644 index 0000000000000..0004bacaee47f --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/LoginData.java @@ -0,0 +1,337 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.connection; + +import java.net.CookieManager; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Scanner; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.util.HexUtils; + +/** + * The {@link LoginData} holds the login data and provides the methods for serialization and deserialization + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class LoginData { + private static final String DEVICE_TYPE = "A2IVLV5VM2W81"; + + private final Random rand = new Random(); + private final CookieManager cookieManager; + + // data fields + private String frc; + private String serial; + private String deviceId; + private @Nullable String refreshToken; + private String retailDomain = "amazon.com"; + private String retailUrl = "https://www.amazon.com"; + private String websiteApiUrl = "https://alexa.amazon.com"; + + private String deviceName = "Unknown"; + private String accountCustomerId = ""; + private @Nullable Date loginTime; + private List cookies = new ArrayList<>(); + + public LoginData(CookieManager cookieManager, String deviceId, String frc, String serial) { + this.cookieManager = cookieManager; + this.frc = frc; + this.serial = serial; + this.deviceId = deviceId; + } + + public LoginData(CookieManager cookieManager) { + this.cookieManager = cookieManager; + + // FRC + byte[] frcBinary = new byte[313]; + rand.nextBytes(frcBinary); + this.frc = Base64.getEncoder().encodeToString(frcBinary); + + // Serial number + byte[] serialBinary = new byte[16]; + rand.nextBytes(serialBinary); + this.serial = HexUtils.bytesToHex(serialBinary); + + // Device id 16 random bytes in upper-case hex format, a # as separator and a fixed DEVICE_TYPE + byte[] bytes = new byte[16]; + rand.nextBytes(bytes); + String hexStr = HexUtils.bytesToHex(bytes).toUpperCase() + "#" + DEVICE_TYPE; + this.deviceId = HexUtils.bytesToHex(hexStr.getBytes()); + } + + public String getFrc() { + return frc; + } + + public String getSerial() { + return serial; + } + + public String getDeviceId() { + return deviceId; + } + + public @Nullable String getRefreshToken() { + return refreshToken; + } + + public String getRetailDomain() { + return retailDomain; + } + + public String getRetailUrl() { + return retailUrl; + } + + public String getWebsiteApiUrl() { + return websiteApiUrl; + } + + public String getDeviceName() { + return deviceName; + } + + public String getAccountCustomerId() { + return accountCustomerId; + } + + public @Nullable Date getLoginTime() { + return loginTime; + } + + public void setFrc(String frc) { + this.frc = frc; + } + + public void setSerial(String serial) { + this.serial = serial; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public void setRefreshToken(@Nullable String refreshToken) { + this.refreshToken = refreshToken; + } + + public void setRetailDomain(String retailDomain) { + this.retailDomain = retailDomain; + } + + public void setRetailUrl(String retailUrl) { + this.retailUrl = retailUrl; + } + + public void setWebsiteApiUrl(String websiteApiUrl) { + this.websiteApiUrl = websiteApiUrl; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public void setAccountCustomerId(String accountCustomerId) { + this.accountCustomerId = accountCustomerId; + } + + public void setLoginTime(@Nullable Date loginTime) { + this.loginTime = loginTime; + } + + public String serializeLoginData() { + Date loginTime = this.loginTime; + if (refreshToken == null || loginTime == null) { + return ""; + } + StringBuilder builder = new StringBuilder(); + builder.append("8\n"); // version + builder.append(frc).append("\n"); + builder.append(serial).append("\n"); + builder.append(deviceId).append("\n"); + builder.append(refreshToken).append("\n"); + builder.append(retailDomain).append("\n"); + builder.append(retailUrl).append("\n"); + builder.append(websiteApiUrl).append("\n"); + builder.append(deviceName).append("\n"); + builder.append(accountCustomerId).append("\n"); + builder.append(loginTime.getTime()).append("\n"); + cookies = cookieManager.getCookieStore().getCookies().stream().map(LoginData.Cookie::fromHttpCookie).toList(); + builder.append(cookies.size()).append("\n"); + cookies.forEach(cookie -> builder.append(cookie.serialize())); + return builder.toString(); + } + + public boolean deserialize(String data) { + Scanner scanner = new Scanner(data); + String version = scanner.nextLine(); + // check if serialize version is supported + if (!"7".equals(version) && !"8".equals(version)) { + scanner.close(); + return false; + } + + frc = scanner.nextLine(); + serial = scanner.nextLine(); + deviceId = scanner.nextLine(); + + refreshToken = scanner.nextLine(); + retailDomain = scanner.nextLine(); + if ("8".equals(version)) { + retailUrl = scanner.nextLine(); + websiteApiUrl = scanner.nextLine(); + } else { + // this maybe incorrect, but it's the same code that we used before + retailUrl = "https://www." + retailDomain; + websiteApiUrl = "https://alexa." + retailDomain; + } + deviceName = scanner.nextLine(); + accountCustomerId = scanner.nextLine(); + loginTime = new Date(Long.parseLong(scanner.nextLine())); + + int numberOfCookies = Integer.parseInt(scanner.nextLine()); + cookies = new ArrayList<>(); + for (int i = 0; i < numberOfCookies; i++) { + cookies.add(Cookie.fromScanner(scanner)); + } + scanner.close(); + + CookieStore cookieStore = cookieManager.getCookieStore(); + cookieStore.removeAll(); + cookies.forEach(cookie -> cookieStore.add(null, cookie.toHttpCookie())); + + return true; + } + + private static class Cookie { + private final String name; + private final String value; + private final String comment; + private final String commentURL; + private final String domain; + private final long maxAge; + private final String path; + private final String portlist; + private final int version; + private final boolean secure; + private final boolean discard; + + private Cookie(String name, String value, String comment, String commentURL, String domain, long maxAge, + String path, String portlist, int version, boolean secure, boolean discard) { + this.name = name; + this.value = value; + this.comment = comment; + this.commentURL = commentURL; + this.domain = domain; + this.maxAge = maxAge; + this.path = path; + this.portlist = portlist; + this.version = version; + this.secure = secure; + this.discard = discard; + } + + private static String readValue(Scanner scanner) { + if (scanner.nextLine().equals("1")) { + return Objects.requireNonNullElse(scanner.nextLine(), ""); + } + return ""; + } + + private void writeValue(StringBuilder builder, @Nullable Object value) { + if (value == null) { + builder.append("0\n"); + } else { + builder.append("1").append("\n").append(value).append("\n"); + } + } + + public static Cookie fromScanner(Scanner scanner) { + return new Cookie(readValue(scanner), readValue(scanner), readValue(scanner), readValue(scanner), + readValue(scanner), Long.parseLong(readValue(scanner)), readValue(scanner), readValue(scanner), + Integer.parseInt(readValue(scanner)), Boolean.parseBoolean(readValue(scanner)), + Boolean.parseBoolean(readValue(scanner))); + } + + public String serialize() { + StringBuilder builder = new StringBuilder(); + writeValue(builder, name); + writeValue(builder, value); + writeValue(builder, comment); + writeValue(builder, commentURL); + writeValue(builder, domain); + writeValue(builder, maxAge); + writeValue(builder, path); + writeValue(builder, portlist); + writeValue(builder, version); + writeValue(builder, secure); + writeValue(builder, discard); + + return builder.toString(); + } + + public static Cookie fromHttpCookie(HttpCookie cookie) { + return new Cookie(cookie.getName(), cookie.getValue(), cookie.getComment(), cookie.getCommentURL(), + cookie.getDomain(), cookie.getMaxAge(), cookie.getPath(), cookie.getPortlist(), cookie.getVersion(), + cookie.getSecure(), cookie.getDiscard()); + } + + public HttpCookie toHttpCookie() { + HttpCookie clientCookie = new HttpCookie(name, value); + clientCookie.setComment(comment); + clientCookie.setCommentURL(commentURL); + clientCookie.setDomain(domain); + clientCookie.setMaxAge(maxAge); + clientCookie.setPath(path); + clientCookie.setPortlist(portlist); + clientCookie.setVersion(version); + clientCookie.setSecure(secure); + clientCookie.setDiscard(discard); + return clientCookie; + } + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + LoginData loginData = (LoginData) o; + return Objects.equals(frc, loginData.frc) && Objects.equals(serial, loginData.serial) + && Objects.equals(deviceId, loginData.deviceId) && Objects.equals(refreshToken, loginData.refreshToken) + && Objects.equals(retailDomain, loginData.retailDomain) + && Objects.equals(retailUrl, loginData.retailUrl) + && Objects.equals(websiteApiUrl, loginData.websiteApiUrl) + && Objects.equals(deviceName, loginData.deviceName) + && Objects.equals(accountCustomerId, loginData.accountCustomerId) + && Objects.equals(loginTime, loginData.loginTime) && Objects.equals(cookies, loginData.cookies); + } + + @Override + public int hashCode() { + return Objects.hash(frc, serial, deviceId, refreshToken, retailDomain, retailUrl, websiteApiUrl, deviceName, + accountCustomerId, loginTime, cookies); + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/TextWrapper.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/TextWrapper.java new file mode 100644 index 0000000000000..19e04c832d674 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/connection/TextWrapper.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.connection; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceTO; + +/** + * The {@link TextWrapper} is a wrapper class for text or TTS instructions + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class TextWrapper { + private final List devices = new ArrayList<>(); + private final String text; + private final List<@Nullable Integer> ttsVolumes = new ArrayList<>(); + private final List<@Nullable Integer> standardVolumes = new ArrayList<>(); + + public TextWrapper(String text) { + this.text = text; + } + + public void add(DeviceTO device, @Nullable Integer ttsVolume, @Nullable Integer standardVolume) { + devices.add(device); + ttsVolumes.add(ttsVolume); + standardVolumes.add(standardVolume); + } + + public List getDevices() { + return devices; + } + + public List<@Nullable Integer> getTtsVolumes() { + return ttsVolumes; + } + + public List<@Nullable Integer> getStandardVolumes() { + return standardVolumes; + } + + public String getText() { + return text; + } + + @Override + public String toString() { + return "TextWrapper{" + "devices=" + devices + ", text='" + text + "'" + ", ttsVolumes=" + ttsVolumes + + ", standardVolumes=" + standardVolumes + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 445ec6a3eb59b..aa80e18e95f52 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -15,25 +15,26 @@ import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; import java.util.Date; -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.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceTO; +import org.openhab.binding.amazonechocontrol.internal.dto.EnabledFeedTO; import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; -import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; 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.ServiceScope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,29 +43,20 @@ * the amazon account specified in the binding. * * @author Michael Geramb - Initial contribution + * @author Jan N. Klug - Refactored to ThingHandlerService */ +@Component(scope = ServiceScope.PROTOTYPE, service = AmazonEchoDiscovery.class) @NonNullByDefault -public class AmazonEchoDiscovery extends AbstractDiscoveryService { - - AccountHandler accountHandler; +public class AmazonEchoDiscovery extends AbstractThingHandlerDiscoveryService { + private static final int BACKGROUND_INTERVAL = 10; // in seconds private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); - private final Set discoveredFlashBriefings = new HashSet<>(); + private final Set> discoveredFlashBriefings = new HashSet<>(); private @Nullable ScheduledFuture startScanStateJob; private @Nullable Long activateTimeStamp; - public AmazonEchoDiscovery(AccountHandler accountHandler) { - super(SUPPORTED_ECHO_THING_TYPES_UIDS, 10); - this.accountHandler = accountHandler; - } - - public void activate() { - activate(new HashMap<>()); - } - - @Override - public void deactivate() { - super.deactivate(); + public AmazonEchoDiscovery() { + super(AccountHandler.class, SUPPORTED_ECHO_THING_TYPES_UIDS, 5); } @Override @@ -74,36 +66,32 @@ protected void startScan() { if (activateTimeStamp != null) { removeOlderResults(activateTimeStamp); } - setDevices(accountHandler.updateDeviceList()); + setDevices(thingHandler.updateDeviceList()); - String currentFlashBriefingConfiguration = accountHandler.getNewCurrentFlashbriefingConfiguration(); + List currentFlashBriefingConfiguration = thingHandler.updateFlashBriefingHandlers(); discoverFlashBriefingProfiles(currentFlashBriefingConfiguration); } protected void startAutomaticScan() { - if (!this.accountHandler.getThing().getThings().isEmpty()) { + if (!thingHandler.getThing().getThings().isEmpty()) { stopScanJob(); return; } - Connection connection = this.accountHandler.findConnection(); - if (connection == null) { - return; - } - Date verifyTime = connection.tryGetVerifyTime(); - if (verifyTime == null) { - return; - } - if (new Date().getTime() - verifyTime.getTime() < 10000) { + Connection connection = thingHandler.getConnection(); + // do discovery only if logged in and last login is more than 10 s ago + Date verifyTime = connection.getVerifyTime(); + if (verifyTime == null || System.currentTimeMillis() < (verifyTime.getTime() + 10000)) { return; } + startScan(); } @Override protected void startBackgroundDiscovery() { stopScanJob(); - startScanStateJob = scheduler.scheduleWithFixedDelay(this::startAutomaticScan, 3000, 1000, - TimeUnit.MILLISECONDS); + startScanStateJob = scheduler.scheduleWithFixedDelay(this::startAutomaticScan, BACKGROUND_INTERVAL, + BACKGROUND_INTERVAL, TimeUnit.SECONDS); } @Override @@ -121,43 +109,46 @@ void stopScanJob() { } @Override - @Activate - public void activate(@Nullable Map config) { - super.activate(config); - if (config != null) { - modified(config); - } + public void initialize() { if (activateTimeStamp == null) { activateTimeStamp = new Date().getTime(); } + super.initialize(); } - synchronized void setDevices(List deviceList) { - for (Device device : deviceList) { + private synchronized void setDevices(List deviceList) { + for (DeviceTO device : deviceList) { String serialNumber = device.serialNumber; if (serialNumber != null) { String deviceFamily = device.deviceFamily; if (deviceFamily != null) { ThingTypeUID thingTypeId; - if ("ECHO".equals(deviceFamily)) { - thingTypeId = THING_TYPE_ECHO; - } else if ("ROOK".equals(deviceFamily)) { - thingTypeId = THING_TYPE_ECHO_SPOT; - } else if ("KNIGHT".equals(deviceFamily)) { - thingTypeId = THING_TYPE_ECHO_SHOW; - } else if ("WHA".equals(deviceFamily)) { - thingTypeId = THING_TYPE_ECHO_WHA; - } else { - logger.debug("Unknown thing type '{}'", deviceFamily); - continue; + switch (deviceFamily) { + case "ECHO": + thingTypeId = THING_TYPE_ECHO; + break; + case "ROOK": + thingTypeId = THING_TYPE_ECHO_SPOT; + break; + case "KNIGHT": + thingTypeId = THING_TYPE_ECHO_SHOW; + break; + case "WHA": + thingTypeId = THING_TYPE_ECHO_WHA; + break; + default: + logger.debug("Unknown thing type '{}'", deviceFamily); + continue; } - ThingUID bridgeThingUID = this.accountHandler.getThing().getUID(); + ThingUID bridgeThingUID = thingHandler.getThing().getUID(); ThingUID thingUID = new ThingUID(thingTypeId, bridgeThingUID, serialNumber); DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel(device.accountName) .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) .withProperty(DEVICE_PROPERTY_FAMILY, deviceFamily) + .withProperty(DEVICE_PROPERTY_DEVICE_TYPE_ID, + Objects.requireNonNullElse(device.deviceType, "")) .withRepresentationProperty(DEVICE_PROPERTY_SERIAL_NUMBER).withBridge(bridgeThingUID) .build(); @@ -170,27 +161,21 @@ synchronized void setDevices(List deviceList) { } } - public synchronized void discoverFlashBriefingProfiles(String currentFlashBriefingJson) { - if (currentFlashBriefingJson.isEmpty()) { + private synchronized void discoverFlashBriefingProfiles(List enabledFeeds) { + if (enabledFeeds.isEmpty()) { return; } - if (!discoveredFlashBriefings.contains(currentFlashBriefingJson)) { - ThingUID bridgeThingUID = this.accountHandler.getThing().getUID(); + if (!discoveredFlashBriefings.contains(enabledFeeds)) { + ThingUID bridgeThingUID = thingHandler.getThing().getUID(); ThingUID freeThingUID = new ThingUID(THING_TYPE_FLASH_BRIEFING_PROFILE, bridgeThingUID, - Integer.toString(currentFlashBriefingJson.hashCode())); + Integer.toString(enabledFeeds.hashCode())); DiscoveryResult result = DiscoveryResultBuilder.create(freeThingUID).withLabel("FlashBriefing") - .withProperty(DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE, currentFlashBriefingJson) - .withBridge(accountHandler.getThing().getUID()).build(); - logger.debug("Flash Briefing {} discovered", currentFlashBriefingJson); + .withProperty(DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE, enabledFeeds) + .withBridge(thingHandler.getThing().getUID()).build(); + logger.debug("Flash Briefing {} discovered", enabledFeeds); thingDiscovered(result); - discoveredFlashBriefings.add(currentFlashBriefingJson); - } - } - - public synchronized void removeExistingFlashBriefingProfile(@Nullable String currentFlashBriefingJson) { - if (currentFlashBriefingJson != null) { - discoveredFlashBriefings.remove(currentFlashBriefingJson); + discoveredFlashBriefings.add(enabledFeeds); } } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java index d1f677ef92132..cd21422a963d8 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java @@ -14,9 +14,7 @@ import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; -import java.util.Date; import java.util.HashMap; -import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; @@ -26,125 +24,80 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice.DriverIdentity; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDeviceAlias; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeGroups.SmartHomeGroup; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.SmartHomeBaseDevice; import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDeviceAlias; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.DriverIdentity; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroups.SmartHomeGroup; -import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice; import org.openhab.binding.amazonechocontrol.internal.smarthome.Constants; -import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; 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.ServiceScope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Lukas Knoeller - Initial contribution + * @author Jan N. Klug - Refactored to ThingHandlerService */ +@Component(scope = ServiceScope.PROTOTYPE, service = SmartHomeDevicesDiscovery.class) @NonNullByDefault -public class SmartHomeDevicesDiscovery extends AbstractDiscoveryService { - private AccountHandler accountHandler; - private Logger logger = LoggerFactory.getLogger(SmartHomeDevicesDiscovery.class); +public class SmartHomeDevicesDiscovery extends AbstractThingHandlerDiscoveryService { + private final Logger logger = LoggerFactory.getLogger(SmartHomeDevicesDiscovery.class); - private @Nullable ScheduledFuture startScanStateJob; - private @Nullable Long activateTimeStamp; + private @Nullable ScheduledFuture discoveryJob; - public SmartHomeDevicesDiscovery(AccountHandler accountHandler) { - super(SUPPORTED_SMART_HOME_THING_TYPES_UIDS, 10); - this.accountHandler = accountHandler; - } - - public void activate() { - activate(new Hashtable<>()); - } - - @Override - public void deactivate() { - super.deactivate(); + public SmartHomeDevicesDiscovery() { + super(AccountHandler.class, SUPPORTED_SMART_HOME_THING_TYPES_UIDS, 10); } @Override protected void startScan() { - stopScanJob(); - Long activateTimeStamp = this.activateTimeStamp; - if (activateTimeStamp != null) { - removeOlderResults(activateTimeStamp); - } - setSmartHomeDevices(accountHandler.updateSmartHomeDeviceList(false)); - } - - protected void startAutomaticScan() { - if (!this.accountHandler.getThing().getThings().isEmpty()) { - stopScanJob(); - return; - } - Connection connection = this.accountHandler.findConnection(); - if (connection == null) { - return; - } - Date verifyTime = connection.tryGetVerifyTime(); - if (verifyTime == null) { - return; - } - if (new Date().getTime() - verifyTime.getTime() < 10000) { - return; - } - startScan(); + setSmartHomeDevices(thingHandler.updateSmartHomeDeviceList(false)); } @Override - protected void startBackgroundDiscovery() { - stopScanJob(); - startScanStateJob = scheduler.scheduleWithFixedDelay(this::startAutomaticScan, 3000, 1000, - TimeUnit.MILLISECONDS); + protected void stopScan() { + removeOlderResults(getTimestampOfLastScan()); + super.stopScan(); } @Override - protected void stopBackgroundDiscovery() { - stopScanJob(); - } - - void stopScanJob() { - ScheduledFuture currentStartScanStateJob = startScanStateJob; - if (currentStartScanStateJob != null) { - currentStartScanStateJob.cancel(false); - startScanStateJob = null; + protected void startBackgroundDiscovery() { + ScheduledFuture discoveryJob = this.discoveryJob; + if (discoveryJob == null || discoveryJob.isCancelled()) { + this.discoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 1, 5, TimeUnit.MINUTES); } - super.stopScan(); } @Override - @Activate - public void activate(@Nullable Map config) { - super.activate(config); - if (config != null) { - modified(config); - } - Long activateTimeStamp = this.activateTimeStamp; - if (activateTimeStamp == null) { - this.activateTimeStamp = new Date().getTime(); + protected void stopBackgroundDiscovery() { + ScheduledFuture discoveryJob = this.discoveryJob; + if (discoveryJob != null) { + discoveryJob.cancel(true); + this.discoveryJob = null; } } - synchronized void setSmartHomeDevices(List deviceList) { - int smartHomeDeviceDiscoveryMode = accountHandler.getSmartHomeDevicesDiscoveryMode(); + private synchronized void setSmartHomeDevices(List deviceList) { + int smartHomeDeviceDiscoveryMode = thingHandler.getSmartHomeDevicesDiscoveryMode(); if (smartHomeDeviceDiscoveryMode == 0) { return; } for (Object smartHomeDevice : deviceList) { - ThingUID bridgeThingUID = this.accountHandler.getThing().getUID(); + ThingUID bridgeThingUID = thingHandler.getThing().getUID(); ThingUID thingUID = null; String deviceName = null; Map props = new HashMap<>(); - if (smartHomeDevice instanceof SmartHomeDevice shd) { - logger.trace("Found SmartHome device: {}", shd); + if (smartHomeDevice instanceof JsonSmartHomeDevice shd) { + logger.trace("Found SmartHome device: {}", shd.applianceId); String entityId = shd.entityId; if (entityId == null) { @@ -161,11 +114,11 @@ synchronized void setSmartHomeDevices(List deviceList) { isSkillDevice = driverIdentity != null && "SKILL".equals(driverIdentity.namespace); if (smartHomeDeviceDiscoveryMode == 1 && isSkillDevice) { - // Connected through skill + // Connected through skill and we want direct only continue; } - if (smartHomeDeviceDiscoveryMode != 2 && "openHAB".equalsIgnoreCase(shd.manufacturerName)) { - // OpenHAB device + if (smartHomeDeviceDiscoveryMode == 2 && "openHAB".equalsIgnoreCase(shd.manufacturerName)) { + // openHAB device and we want non-openHAB only continue; } @@ -178,30 +131,45 @@ synchronized void setSmartHomeDevices(List deviceList) { thingUID = new ThingUID(THING_TYPE_SMART_HOME_DEVICE, bridgeThingUID, entityId.replace(".", "-")); List aliases = shd.aliases; - if ("Amazon".equals(shd.manufacturerName) && driverIdentity != null - && "SonarCloudService".equals(driverIdentity.identifier)) { + String manufacturerName = shd.manufacturerName; + if (manufacturerName != null) { + props.put(DEVICE_PROPERTY_MANUFACTURER_NAME, manufacturerName); + } + if (manufacturerName != null && manufacturerName.startsWith("Amazon")) { List<@Nullable String> interfaces = shd.getCapabilities().stream().map(c -> c.interfaceName) - .collect(Collectors.toList()); - if (interfaces.contains("Alexa.AcousticEventSensor")) { + .toList(); + if (driverIdentity != null && "SonarCloudService".equals(driverIdentity.identifier)) { + if (interfaces.contains("Alexa.AcousticEventSensor")) { + deviceName = "Alexa Guard on " + shd.friendlyName; + } else if (interfaces.contains("Alexa.ColorController")) { + deviceName = "Alexa Color Controller on " + shd.friendlyName; + } else if (interfaces.contains("Alexa.PowerController")) { + deviceName = "Alexa Plug on " + shd.friendlyName; + } else { + deviceName = "Unknown Device on " + shd.friendlyName; + } + } else if (driverIdentity != null + && "OnGuardSmartHomeBridgeService".equals(driverIdentity.identifier)) { + deviceName = "Alexa Guard"; + } else if (driverIdentity != null && "AlexaBridge".equals(driverIdentity.namespace) + && interfaces.contains("Alexa.AcousticEventSensor")) { deviceName = "Alexa Guard on " + shd.friendlyName; - } else if (interfaces.contains("Alexa.ColorController")) { - deviceName = "Alexa Color Controller on " + shd.friendlyName; - } else if (interfaces.contains("Alexa.PowerController")) { - deviceName = "Alexa Plug on " + shd.friendlyName; - } else if (interfaces.contains("Alexa.ThermostatController")) { - deviceName = "Alexa Smart " + shd.friendlyName; } else { deviceName = "Unknown Device on " + shd.friendlyName; } - } else if ("Amazon".equals(shd.manufacturerName) && driverIdentity != null - && "OnGuardSmartHomeBridgeService".equals(driverIdentity.identifier)) { - deviceName = "Alexa Guard"; } else if (aliases != null && !aliases.isEmpty() && aliases.get(0).friendlyName != null) { deviceName = aliases.get(0).friendlyName; } else { deviceName = shd.friendlyName; } props.put(DEVICE_PROPERTY_ID, id); + List alexaDeviceIdentifierList = shd.alexaDeviceIdentifierList; + if (alexaDeviceIdentifierList != null && !alexaDeviceIdentifierList.isEmpty()) { + props.put(DEVICE_PROPERTY_DEVICE_IDENTIFIER_LIST, + alexaDeviceIdentifierList.stream() + .map(d -> d.dmsDeviceSerialNumber + " @ " + d.dmsDeviceTypeId) + .collect(Collectors.joining(", "))); + } } else if (smartHomeDevice instanceof SmartHomeGroup shg) { logger.trace("Found SmartHome device: {}", shg); @@ -210,7 +178,7 @@ synchronized void setSmartHomeDevices(List deviceList) { // No id continue; } - Set supportedChildren = SmartHomeDeviceHandler.getSupportedSmartHomeDevices(shg, + Set supportedChildren = SmartHomeDeviceHandler.getSupportedSmartHomeDevices(shg, deviceList); if (supportedChildren.isEmpty()) { // No children with a supported interface diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/AscendingAlarmModelTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/AscendingAlarmModelTO.java new file mode 100644 index 0000000000000..ceb25775f1db4 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/AscendingAlarmModelTO.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.util.SerializeNull; + +/** + * The {@link AscendingAlarmModelTO} encapsulates the ascending alarm status of a device + * + * @author Jan N. Klug - Initial contribution + */ +public class AscendingAlarmModelTO { + public boolean ascendingAlarmEnabled; + public String deviceSerialNumber; + public String deviceType; + @SerializeNull + public Object deviceAccountId = null; + + @Override + public @NonNull String toString() { + return "AscendingAlarmModelTO{ascendingAlarmEnabled=" + ascendingAlarmEnabled + ", deviceSerialNumber='" + + deviceSerialNumber + "', deviceType='" + deviceType + "', deviceAccountId=" + deviceAccountId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPushCommand.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/BehaviorOperationValidationResultTO.java similarity index 55% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPushCommand.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/BehaviorOperationValidationResultTO.java index de580c37d1c68..0fec9543f7bd0 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPushCommand.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/BehaviorOperationValidationResultTO.java @@ -10,18 +10,13 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; +package org.openhab.binding.amazonechocontrol.internal.dto; /** - * The {@link JsonPushCommand} encapsulate the GSON data of automation query + * The {@link BehaviorOperationValidationResultTO} encapsulate the GSON for validation result * * @author Michael Geramb - Initial contribution */ -@NonNullByDefault -public class JsonPushCommand { - public @Nullable String command; - public @Nullable String payload; +public class BehaviorOperationValidationResultTO { + public T operationPayload; } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/BluetoothPairedDeviceTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/BluetoothPairedDeviceTO.java new file mode 100644 index 0000000000000..7f4a843b002eb --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/BluetoothPairedDeviceTO.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.response.BluetoothStateTO; + +/** + * The {@link BluetoothPairedDeviceTO} encapsulate a part of {@link BluetoothStateTO} + * + * @author Jan N. Klug - Initial contribution + */ +public class BluetoothPairedDeviceTO { + public String address; + public boolean connected; + public String deviceClass; + public String friendlyName; + public List profiles = List.of(); + + @Override + public @NonNull String toString() { + return "BluetoothPairedDeviceTO{address='" + address + "', connected=" + connected + ", deviceClass='" + + deviceClass + "', friendlyName='" + friendlyName + "', profiles=" + profiles + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/CookieTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/CookieTO.java new file mode 100644 index 0000000000000..fc4c945670474 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/CookieTO.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link CookieTO} encapsulates a cookie + * + * @author Jan N. Klug - Initial contribution + */ +public class CookieTO { + @SerializedName("Path") + public String path; + @SerializedName("Secure") + public String secure; + @SerializedName("Value") + public String value; + @SerializedName("Expires") + public String expires; + @SerializedName("HttpOnly") + public String httpOnly; + @SerializedName("Name") + public String name; + + @Override + public @NonNull String toString() { + return "CookieTO{path='" + path + "', secure=" + secure + ", value='" + value + "', expires='" + expires + + "', httpOnly=" + httpOnly + ", name='" + name + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DeviceIdTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DeviceIdTO.java new file mode 100644 index 0000000000000..89e40643f25e7 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DeviceIdTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link DeviceIdTO} encapsulates a target device for an announcement target + * + * @author Jan N. Klug - Initial contribution + */ +public class DeviceIdTO { + public String deviceSerialNumber; + public String deviceTypeId; + + @Override + public @NonNull String toString() { + return "AnnouncementTargetDeviceTO{deviceSerialNumber='" + deviceSerialNumber + "', deviceTypeId='" + + deviceTypeId + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DeviceNotificationStateTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DeviceNotificationStateTO.java new file mode 100644 index 0000000000000..b7af4eadeb607 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DeviceNotificationStateTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link DeviceNotificationStateTO} encapsulates a command to enable/disable ascending alarms on a device + * + * @author Jan N. Klug - Initial contribution + */ +public class DeviceNotificationStateTO { + public String deviceSerialNumber; + public String deviceType; + public String softwareVersion; + public int volumeLevel; + + @Override + public @NonNull String toString() { + return "DeviceNotificationStateTO{deviceSerialNumber='" + deviceSerialNumber + "', deviceType='" + deviceType + + "', softwareVersion='" + softwareVersion + "', volumeLevel=" + volumeLevel + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DeviceTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DeviceTO.java new file mode 100644 index 0000000000000..034ed0021bf8b --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DeviceTO.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link DeviceTO} encapsulate information about a single device + * + * @author Jan N. Klug - Initial contribution + */ +public class DeviceTO { + public String accountName; + public String serialNumber; + public String deviceOwnerCustomerId; + public String deviceAccountId; + public String deviceFamily; + public String deviceType; + public String softwareVersion; + public boolean online; + public Set capabilities = Set.of(); + + @Override + public @NonNull String toString() { + return "Device{accountName='" + accountName + "', serialNumber='" + serialNumber + "', deviceOwnerCustomerId='" + + deviceOwnerCustomerId + "', deviceAccountId='" + deviceAccountId + "', deviceFamily='" + deviceFamily + + "', deviceType='" + deviceType + "', softwareVersion='" + softwareVersion + "', online=" + online + + ", capabilities=" + capabilities + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DoNotDisturbDeviceStatusTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DoNotDisturbDeviceStatusTO.java new file mode 100644 index 0000000000000..bb430a47119d1 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/DoNotDisturbDeviceStatusTO.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.util.SerializeNull; + +/** + * The {@link DoNotDisturbDeviceStatusTO} encapsulates a command to enable/disable ascending alarms on a device + * + * @author Jan N. Klug - Initial contribution + */ +public class DoNotDisturbDeviceStatusTO { + public boolean enabled; + public String deviceSerialNumber; + public String deviceType; + @SerializeNull + public Object deviceAccountId = null; + + @Override + public @NonNull String toString() { + return "DoNotDisturbDeviceStatusTO{enabled=" + enabled + ", deviceSerialNumber='" + deviceSerialNumber + + "', deviceType='" + deviceType + "', deviceAccountId=" + deviceAccountId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/EnabledFeedTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/EnabledFeedTO.java new file mode 100644 index 0000000000000..f9f483b50aad9 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/EnabledFeedTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link EnabledFeedTO} encapsulate a single feed + * + * @author Jan N. Klug - Initial contribution + */ +public class EnabledFeedTO { + public Object feedId; + public String name; + public String skillId; + public String imageUrl; + + @Override + public @NonNull String toString() { + return "EnabledFeedTO{feedId=" + feedId + ", name='" + name + "', skillId='" + skillId + "', imageUrl='" + + imageUrl + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/EnabledFeedsTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/EnabledFeedsTO.java new file mode 100644 index 0000000000000..6d0b4ab3a5bf9 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/EnabledFeedsTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link EnabledFeedsTO} encapsulate the data for handling enabled feeds + * + * @author Jan N. Klug - Initial contribution + */ +public class EnabledFeedsTO { + public List enabledFeeds = List.of(); + + @Override + public @NonNull String toString() { + return "EnabledFeedsTO{enabledFeeds=" + enabledFeeds + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/EqualizerTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/EqualizerTO.java new file mode 100644 index 0000000000000..efa9f51c26760 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/EqualizerTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link EqualizerTO} encapsulate an equalizer command/response + * + * @author Jan N. Klug - Initial contribution + */ +public class EqualizerTO { + public int bass = 0; + public int mid = 0; + public int treble = 0; + + @Override + public @NonNull String toString() { + return "JsonEqualizer{bass=" + bass + ", mid=" + mid + ", treble=" + treble + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/NotificationSoundTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/NotificationSoundTO.java new file mode 100644 index 0000000000000..c3f33a9b43d56 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/NotificationSoundTO.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link NotificationSoundTO} encapsulate a notification sound + * + * @author Michael Geramb - Initial contribution + */ +public class NotificationSoundTO { + public String displayName; + public String folder; + public String id = "system_alerts_melodic_01"; + public String providerId = "ECHO"; + public String sampleUrl; + + @Override + public @NonNull String toString() { + return "NotificationSoundTO{displayName='" + displayName + "', folder='" + folder + "', id='" + id + + "', providerId='" + providerId + "', sampleUrl='" + sampleUrl + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/NotificationStateTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/NotificationStateTO.java new file mode 100644 index 0000000000000..2a4cc39251351 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/NotificationStateTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link NotificationStateTO} encapsulates the request to set notification states + * + * @author Jan N. Klug - Initial contribution + */ +public class NotificationStateTO { + public String deviceSerialNumber; + public String deviceType; + public String softwareVersion; + public int volumeLevel; + + @Override + public @NonNull String toString() { + return "NotificationStateTO{deviceSerialNumber='" + deviceSerialNumber + "', deviceType='" + deviceType + + "', softwareVersion='" + softwareVersion + "', volumeLevel=" + volumeLevel + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/NotificationTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/NotificationTO.java new file mode 100644 index 0000000000000..13c91c33d3783 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/NotificationTO.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.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link NotificationTO} encapsulate a single notification + * + * @author Jan N. Klug - Initial contribution + */ +public class NotificationTO { + public String id; + public String type; + public String version; + + public String deviceSerialNumber; + public String deviceType; + + public long alarmTime; + public long createdDate; + public Object musicAlarmId; + public Object musicEntity; + public String notificationIndex; + public String originalDate; + public String originalTime; + public Object provider; + public boolean isRecurring; + public String recurringPattern; + public String reminderLabel; + public String reminderIndex; + public NotificationSoundTO sound = new NotificationSoundTO(); + public String status; + public String timeZoneId; + public String timerLabel; + public Object alarmIndex; + public boolean isSaveInFlight; + public Long triggerTime; + public Long remainingTime; + + @Override + public @NonNull String toString() { + return "NotificationTO{id='" + id + "', type='" + type + "', version='" + version + "', deviceSerialNumber='" + + deviceSerialNumber + "', deviceType='" + deviceType + "', alarmTime=" + alarmTime + ", createdDate=" + + createdDate + ", musicAlarmId=" + musicAlarmId + ", musicEntity=" + musicEntity + + ", notificationIndex='" + notificationIndex + "', originalDate='" + originalDate + "', originalTime='" + + originalTime + "', provider=" + provider + ", isRecurring=" + isRecurring + ", recurringPattern='" + + recurringPattern + "', reminderLabel='" + reminderLabel + "', reminderIndex='" + reminderIndex + + "', sound=" + sound + ", status='" + status + "', timeZoneId='" + timeZoneId + "', timerLabel='" + + timerLabel + "', alarmIndex=" + alarmIndex + ", isSaveInFlight=" + isSaveInFlight + ", triggerTime=" + + triggerTime + ", remainingTime=" + remainingTime + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlaySearchPhraseTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlaySearchPhraseTO.java new file mode 100644 index 0000000000000..a6002de765462 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlaySearchPhraseTO.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.reflect.TypeToken; + +/** + * The {@link PlaySearchPhraseTO} encapsulates the payload to validate a search phrase + * + * @author Jan N. Klug - Initial contribution + */ +public class PlaySearchPhraseTO { + @SuppressWarnings("unchecked") + public static final TypeToken> VALIDATION_RESULT_TO_TYPE_TOKEN = (TypeToken>) TypeToken + .getParameterized(BehaviorOperationValidationResultTO.class, PlaySearchPhraseTO.class); + + public String deviceType = "ALEXA_CURRENT_DEVICE_TYPE"; + public String deviceSerialNumber = "ALEXA_CURRENT_DSN"; + public String locale = "ALEXA_CURRENT_LOCALE"; + public String customerId; + public String searchPhrase; + public String sanitizedSearchPhrase; + public String musicProviderId = "ALEXA_CURRENT_DSN"; + + @Override + public @NonNull String toString() { + return "PlaySearchPhraseTO{deviceType='" + deviceType + "', deviceSerialNumber='" + deviceSerialNumber + + "', locale='" + locale + "', customerId='" + customerId + "', searchPhrase='" + searchPhrase + + "', sanitizedSearchPhrase='" + sanitizedSearchPhrase + "', musicProviderId='" + musicProviderId + + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateInfoTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateInfoTO.java new file mode 100644 index 0000000000000..7c05e97a3e0ce --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateInfoTO.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link PlayerStateInfoTO} encapsulates the information about a player + * + * @author Jan N. Klug - Initial contribution + */ +public class PlayerStateInfoTO { + public String queueId; + public String mediaId; + @SerializedName(value = "state", alternate = { "playerState" }) + public String state; + public PlayerStateInfoTextTO infoText = new PlayerStateInfoTextTO(); + public PlayerStateInfoTextTO miniInfoText = new PlayerStateInfoTextTO(); + public PlayerStateProviderTO provider = new PlayerStateProviderTO(); + public PlayerStateVolumeTO volume = new PlayerStateVolumeTO(); + public PlayerStateMainArtTO mainArt = new PlayerStateMainArtTO(); + public PlayerStateProgressTO progress = new PlayerStateProgressTO(); + public PlayerStateMediaReferenceTO mediaReference = new PlayerStateMediaReferenceTO(); + + @Override + public @NonNull String toString() { + return "PlayerStateInfoTO{queueId='" + queueId + "', mediaId='" + mediaId + "', state='" + state + + "', infoText=" + infoText + ", miniInfoText=" + miniInfoText + ", provider=" + provider + ", volume=" + + volume + ", mainArt=" + mainArt + ", progress=" + progress + ", mediaReference=" + mediaReference + + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateInfoTextTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateInfoTextTO.java new file mode 100644 index 0000000000000..ea26cda9c26c4 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateInfoTextTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PlayerStateInfoTextTO} encapsulates the info text section of a player info + * + * @author Jan N. Klug - Initial contribution + */ +public class PlayerStateInfoTextTO { + public boolean multiLineMode; + public String subText1; + public String subText2; + public String title; + + @Override + public @NonNull String toString() { + return "PlayerStateInfoTextTO{multiLineMode=" + multiLineMode + ", subText1='" + subText1 + "', subText2='" + + subText2 + "', title='" + title + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateMainArtTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateMainArtTO.java new file mode 100644 index 0000000000000..c0401f121e808 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateMainArtTO.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link PlayerStateMainArtTO} encapsulates the art section of a player info + * + * @author Jan N. Klug - Initial contribution + */ +public class PlayerStateMainArtTO { + public String altText; + public String artType; + public String contentType; + @SerializedName(value = "url", alternate = { "fullUrl" }) + public String url; + + @Override + public @NonNull String toString() { + return "PlayerStateMainArtTO{altText='" + altText + "', artType='" + artType + "', contentType='" + contentType + + "', url='" + url + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateMediaReferenceTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateMediaReferenceTO.java new file mode 100644 index 0000000000000..749d1329f5d33 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateMediaReferenceTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PlayerStateMediaReferenceTO} encapsulates the media reference / queue information of a Player info state + * + * @author Jan N. Klug - Initial contribution + */ +public class PlayerStateMediaReferenceTO { + public String namespace; + public String name; + public String value; + + @Override + public @NonNull String toString() { + return "MediaReferenceTO{namespace='" + namespace + "', name='" + name + "', value='" + value + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateProgressTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateProgressTO.java new file mode 100644 index 0000000000000..591578b1b6dd6 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateProgressTO.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PlayerStateProgressTO} encapsulates the progress section of a player info + * + * @author Jan N. Klug - Initial contribution + */ +public class PlayerStateProgressTO { + public boolean allowScrubbing; + public Object locationInfo; + public long mediaLength; + public long mediaProgress; + public boolean showTiming; + public boolean visible; + + @Override + public @NonNull String toString() { + return "PlayerStateProgressTO{allowScrubbing=" + allowScrubbing + ", locationInfo=" + locationInfo + + ", mediaLength=" + mediaLength + ", mediaProgress=" + mediaProgress + ", showTiming=" + showTiming + + ", visible=" + visible + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateProviderTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateProviderTO.java new file mode 100644 index 0000000000000..2f5ffa617a1cb --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateProviderTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PlayerStateProviderTO} encapsulates the provider section of a player info + * + * @author Jan N. Klug - Initial contribution + */ +public class PlayerStateProviderTO { + public String providerDisplayName; + public String providerName; + + @Override + public @NonNull String toString() { + return "PlayerStateProviderTO{providerDisplayName='" + providerDisplayName + "', providerName='" + providerName + + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateVolumeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateVolumeTO.java new file mode 100644 index 0000000000000..6ac683f49e557 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/PlayerStateVolumeTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PlayerStateVolumeTO} encapsulates the volume part of a player info + * + * @author Jan N. Klug - Initial contribution + */ +public class PlayerStateVolumeTO { + public boolean muted; + public int volume; + + @Override + public @NonNull String toString() { + return "PlayerStateVolumeTO{muted=" + muted + ", volume=" + volume + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/TOMapper.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/TOMapper.java new file mode 100644 index 0000000000000..3fe1353bda6a1 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/TOMapper.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto; + +import static org.eclipse.jetty.util.StringUtil.isNotBlank; + +import java.net.HttpCookie; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.types.Notification; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +/** + * The {@link TOMapper} contains mappers for TOs + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class TOMapper { + @SuppressWarnings("unchecked") + private static final TypeToken> MAP_TYPE_TOKEN = (TypeToken>) TypeToken + .getParameterized(Map.class, String.class, Object.class); + + private TOMapper() { + // prevent instantiation + } + + public static Map mapToMap(Gson gson, Object o) { + String json = gson.toJson(o); + return Objects.requireNonNullElse(gson.fromJson(json, MAP_TYPE_TOKEN), Map.of()); + } + + public static DeviceIdTO mapAnnouncementTargetDevice(DeviceTO device) { + DeviceIdTO targetDevice = new DeviceIdTO(); + targetDevice.deviceTypeId = device.deviceType; + targetDevice.deviceSerialNumber = device.serialNumber; + return targetDevice; + } + + public static CookieTO mapCookie(HttpCookie httpCookie) { + CookieTO cookie = new CookieTO(); + cookie.name = httpCookie.getName(); + cookie.value = httpCookie.getValue(); + cookie.secure = String.valueOf(httpCookie.getSecure()); + cookie.httpOnly = String.valueOf(httpCookie.isHttpOnly()); + return cookie; + } + + public static HttpCookie mapCookie(CookieTO cookie, String domain) { + HttpCookie httpCookie = new HttpCookie(cookie.name, cookie.value); + httpCookie.setPath(cookie.path); + httpCookie.setDomain(domain); + String secure = cookie.secure; + if (secure != null) { + httpCookie.setSecure(Boolean.getBoolean(secure)); + } + return httpCookie; + } + + public static @Nullable Notification map(NotificationTO notification, ZonedDateTime requestTime, + ZonedDateTime now) { + if (!"ON".equals(notification.status) || notification.deviceSerialNumber == null) { + return null; + } + ZonedDateTime alarmTime; + if ("Reminder".equals(notification.type) || "Alarm".equals(notification.type) + || "MusicAlarm".equals(notification.type)) { + LocalDate localDate = isNotBlank(notification.originalDate) ? LocalDate.parse(notification.originalDate) + : now.toLocalDate(); + LocalTime localTime = isNotBlank(notification.originalTime) ? LocalTime.parse(notification.originalTime) + : LocalTime.MIDNIGHT; + ZonedDateTime originalTime = ZonedDateTime.of(localDate, localTime, ZoneId.systemDefault()); + + if (notification.alarmTime == 0 || !isNotBlank(notification.recurringPattern)) { + alarmTime = originalTime; + } else { + // the alarm time needs to be DST adjusted + alarmTime = Instant.ofEpochMilli(notification.alarmTime).atZone(ZoneId.systemDefault()); + int alarmOffset = originalTime.getOffset().getTotalSeconds() - alarmTime.getOffset().getTotalSeconds(); + alarmTime = alarmTime.plusSeconds(alarmOffset); + } + } else if ("Timer".equals(notification.type) && notification.remainingTime > 0) { + alarmTime = requestTime.plus(notification.remainingTime, ChronoUnit.MILLIS); + } else { + return null; + } + return new Notification(notification.deviceSerialNumber, notification.type, alarmTime); + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyMediaSessionsUpdatedTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyMediaSessionsUpdatedTO.java new file mode 100644 index 0000000000000..1e8a5f61a9379 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyMediaSessionsUpdatedTO.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link NotifyMediaSessionsUpdatedTO} encapsulates NotifyMediaSessionsUpdated messages + * + * @author Jan N. Klug - Initial contribution + */ +public class NotifyMediaSessionsUpdatedTO { + private String customerId; + + private String name; + + private String messageId; + + private NotifyMediaSessionsUpdatedUpdateTO update; + + private boolean fallbackAllowed; + + @Override + public @NonNull String toString() { + return "NotifyMediaSessionsUpdatedTO{customerId='" + customerId + "', name='" + name + "', messageId='" + + messageId + "', update=" + update + ", fallbackAllowed=" + fallbackAllowed + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyMediaSessionsUpdatedUpdateTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyMediaSessionsUpdatedUpdateTO.java new file mode 100644 index 0000000000000..a1ebf90e4e264 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyMediaSessionsUpdatedUpdateTO.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link NotifyMediaSessionsUpdatedTO} encapsulates the inner update of NotifyMediaSessionsUpdated messages + * + * @author Jan N. Klug - Initial contribution + */ +public class NotifyMediaSessionsUpdatedUpdateTO { + public String type; + + @Override + public @NonNull String toString() { + return "NotifyMediaSessionsUpdatedUpdateTO{type='" + type + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyNowPlayingUpdatedOuterUpdateTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyNowPlayingUpdatedOuterUpdateTO.java new file mode 100644 index 0000000000000..d33bc74b637dc --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyNowPlayingUpdatedOuterUpdateTO.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link NotifyNowPlayingUpdatedOuterUpdateTO} encapsulates the outer update section of NotifyNowPlayingUpdated + * messages + * + * @author Jan N. Klug - Initial contribution + */ +public class NotifyNowPlayingUpdatedOuterUpdateTO { + + public String taskSessionId; + + public NotifyNowPlayingUpdatedUpdateTO update; + + public String type; + + @Override + public @NonNull String toString() { + return "NotifyNowPlayingUpdatedOuterUpdateTO{taskSessionId='" + taskSessionId + "', update=" + update + + ", type='" + type + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyNowPlayingUpdatedTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyNowPlayingUpdatedTO.java new file mode 100644 index 0000000000000..22208af9153fe --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyNowPlayingUpdatedTO.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link NotifyNowPlayingUpdatedTO} encapsulates NotifyNowPlayingUpdated messages + * + * @author Jan N. Klug - Initial contribution + */ +public class NotifyNowPlayingUpdatedTO { + + public String customerId; + + public String name; + + public String messageId; + + public NotifyNowPlayingUpdatedOuterUpdateTO update; + + public boolean fallbackAllowed; + + @Override + public @NonNull String toString() { + return "NotifyNowPlayingUpdatedTO{customerId='" + customerId + "', name='" + name + "'" + ", messageId='" + + messageId + "', update=" + update + ", fallbackAllowed=" + fallbackAllowed + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyNowPlayingUpdatedUpdateTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyNowPlayingUpdatedUpdateTO.java new file mode 100644 index 0000000000000..cd2515da5160c --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/NotifyNowPlayingUpdatedUpdateTO.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.PlayerStateInfoTO; + +/** + * The {@link NotifyNowPlayingUpdatedUpdateTO} encapsulates the inner update section of NotifyNowPlayingUpdated messages + * + * @author Jan N. Klug - Initial contribution + */ +public class NotifyNowPlayingUpdatedUpdateTO { + public boolean playbackError; + + public String errorMessage; + + public String cause; + + public String type; + + public PlayerStateInfoTO nowPlayingData; + + @Override + public @NonNull String toString() { + return "NotifyNowPlayingUpdatedUpdateTO{playbackError=" + playbackError + ", errorMessage='" + errorMessage + + "', cause='" + cause + "', type='" + type + "', nowPlayingData=" + nowPlayingData + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushAudioPlayerStateTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushAudioPlayerStateTO.java new file mode 100644 index 0000000000000..b7fa6715befdc --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushAudioPlayerStateTO.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushAudioPlayerStateTO} encapsulates PUSH_AUDIO_PLAYER_STATE messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushAudioPlayerStateTO extends PushDeviceTO { + public String mediaReferenceId; + public String quality; + public boolean error; + public AudioPlayerState audioPlayerState; + public String errorMessage; + + @Override + public @NonNull String toString() { + return "PushAudioplayerStateTO{mediaReferenceId='" + mediaReferenceId + "', error=" + error + + ", audioPlayerState=" + audioPlayerState + ", errorMessage='" + errorMessage + + "', destinationUserId='" + destinationUserId + "', dopplerId=" + dopplerId + '}'; + } + + public enum AudioPlayerState { + INTERRUPTED, + FINISHED, + PLAYING + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushBluetoothStateChangeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushBluetoothStateChangeTO.java new file mode 100644 index 0000000000000..42cd982205a2d --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushBluetoothStateChangeTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushBluetoothStateChangeTO} encapsulates PUSH_BLUETOOTH_STATE_CHANGE messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushBluetoothStateChangeTO extends PushDeviceTO { + public String bluetoothEvent; + public String bluetoothEventPayload; + public boolean bluetoothEventSuccess; + + @Override + public @NonNull String toString() { + return "PushBluetoothStateChangeTO{bluetoothEvent='" + bluetoothEvent + "', bluetoothEventPayload='" + + bluetoothEventPayload + "', bluetoothEventSuccess=" + bluetoothEventSuccess + ", destinationUserId='" + + destinationUserId + "', dopplerId=" + dopplerId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushCommandTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushCommandTO.java new file mode 100644 index 0000000000000..a06271ef0e3c6 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushCommandTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushCommandTO} encapsulates an activity stream command + * + * @author Jan N. Klug - Initial contribution + */ +public class PushCommandTO { + public String command; + public String payload; + public long timeStamp; + + @Override + public @NonNull String toString() { + return "CommandTO{command='" + command + "', payload='" + payload + "', timeStamp=" + timeStamp + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushContentFocusChangeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushContentFocusChangeTO.java new file mode 100644 index 0000000000000..13c91eccb872d --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushContentFocusChangeTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushContentFocusChangeTO} encapsulates PUSH_CONTENT_FOCUS_CHANGE messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushContentFocusChangeTO extends PushDeviceTO { + public String clientId; + public String deviceComponent; + + @Override + public @NonNull String toString() { + return "PushContentFocusChangeTO{clientId='" + clientId + "', deviceComponent='" + deviceComponent + + "', destinationUserId='" + destinationUserId + "', dopplerId=" + dopplerId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushDevicePushConnectionChangeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushDevicePushConnectionChangeTO.java new file mode 100644 index 0000000000000..c38406518331b --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushDevicePushConnectionChangeTO.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushDevicePushConnectionChangeTO} encapsulates PUSH_DOPPLER_CONNECTION_CHANGE messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushDevicePushConnectionChangeTO extends PushDeviceTO { + public DopplerConnectionState dopplerConnectionState; + + @Override + public @NonNull String toString() { + return "PushDopplerConnectionChangeTO{dopplerConnectionState=" + dopplerConnectionState + + ", destinationUserId='" + destinationUserId + "', dopplerId=" + dopplerId + "}"; + } + + public enum DopplerConnectionState { + ONLINE, + OFFLINE + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushDeviceTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushDeviceTO.java new file mode 100644 index 0000000000000..6307b66675388 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushDeviceTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushDeviceTO} encapsulates the header of a device/doppler message + * + * @author Jan N. Klug - Initial contribution + */ +public class PushDeviceTO { + public String destinationUserId; + public PushDopplerIdTO dopplerId; + + @Override + public @NonNull String toString() { + return "PushDeviceTO{destinationUserId='" + destinationUserId + "', dopplerId=" + dopplerId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushDopplerIdTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushDopplerIdTO.java new file mode 100644 index 0000000000000..7679e8cf5bb23 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushDopplerIdTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushDopplerIdTO} encapsulates the device information of activity messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushDopplerIdTO { + public String deviceSerialNumber; + public String deviceType; + + @Override + public @NonNull String toString() { + return "PushDopplerIdTO{deviceSerialNumber='" + deviceSerialNumber + "', deviceType='" + deviceType + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushEqualizerStateChangeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushEqualizerStateChangeTO.java new file mode 100644 index 0000000000000..c415e518ca639 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushEqualizerStateChangeTO.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushEqualizerStateChangeTO} encapsulates PUSH_EQUALIZER_STATE_CHANGE messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushEqualizerStateChangeTO extends PushDeviceTO { + public int bass; + public int midrange; + public int treble; + + @Override + public @NonNull String toString() { + return "PushEqualizerStateChangeTO{bass=" + bass + ", midrange=" + midrange + ", treble=" + treble + + ", destinationUserId='" + destinationUserId + "', dopplerId=" + dopplerId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushListItemChangeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushListItemChangeTO.java new file mode 100644 index 0000000000000..b7f80809d056e --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushListItemChangeTO.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +/** + * The {@link PushListItemChangeTO} encapsulates a PUSH_LIST_ITEM_CHANGE message + * + * @author Jan N. Klug - Initial contribution + */ +public class PushListItemChangeTO { + public String listId; + public String listItemId; + public int version; + public String eventName; + public String destinationUserId; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMediaChangeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMediaChangeTO.java new file mode 100644 index 0000000000000..53ddcb7fb5366 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMediaChangeTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushMediaChangeTO} encapsulates PUSH_MEDIA_CHANGE messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushMediaChangeTO extends PushDeviceTO { + public String mediaReferenceId; + + @Override + public @NonNull String toString() { + return "PushMediaChangeTO{mediaReferenceId='" + mediaReferenceId + "', destinationUserId='" + destinationUserId + + "', dopplerId=" + dopplerId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMediaProgressChangeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMediaProgressChangeTO.java new file mode 100644 index 0000000000000..c4e699edaf908 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMediaProgressChangeTO.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushMediaProgressChangeTO} encapsulates PUSH_MEDIA_PROGRESS_CHANGE messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushMediaProgressChangeTO extends PushDeviceTO { + public String mediaReferenceId; + public ProgressTO progress; + + public static class ProgressTO { + public long mediaProgress; + public long mediaLength; + } + + @Override + public @NonNull String toString() { + return "PushMediaProgressChangeTO{mediaReferenceId='" + mediaReferenceId + "', progress=" + progress + + ", destinationUserId='" + destinationUserId + "', dopplerId=" + dopplerId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMediaQueueChangeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMediaQueueChangeTO.java new file mode 100644 index 0000000000000..7f90b1772a776 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMediaQueueChangeTO.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link PushMediaQueueChangeTO} encapsulates PUSH_MEDIA_QUEUE_CHANGE messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushMediaQueueChangeTO extends PushDeviceTO { + public String changeType; + public @Nullable String playBackOrder; + public boolean trackOrderChanged; + public @Nullable String loopMode; + + @Override + public @NonNull String toString() { + return "PushMediaQueueChangeTO{changeType='" + changeType + "', playBackOrder='" + playBackOrder + "'" + + ", trackOrderChanged=" + trackOrderChanged + ", loopMode='" + loopMode + "', destinationUserId='" + + destinationUserId + "', dopplerId=" + dopplerId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMessageTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMessageTO.java new file mode 100644 index 0000000000000..e9eb72a4f52c9 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushMessageTO.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link PushMessageTO} is used to handle activity messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushMessageTO { + + public DirectiveTO directive = new DirectiveTO(); + + @Override + public @NonNull String toString() { + return "MessageTO{directive=" + directive + "}"; + } + + public static class DirectiveTO { + public HeaderTO header = new HeaderTO(); + public PayloadTO payload = new PayloadTO(); + + @Override + public @NonNull String toString() { + return "DirectiveTO{header=" + header + ", payload=" + payload + "}"; + } + } + + public static class HeaderTO { + public String namespace; + @SerializedName("name") + public String directiveName; + public String messageId; + + @Override + public @NonNull String toString() { + return "HeaderTO{namespace='" + namespace + "', directiveName='" + directiveName + "', messageId='" + + messageId + "'}"; + } + } + + public static class PayloadTO { + public List renderingUpdates = List.of(); + + @Override + public @NonNull String toString() { + return "PayloadTO{renderingUpdates=" + renderingUpdates + "}"; + } + } + + public static class RenderingUpdateTO { + public String route; + public String resourceId; + public String resourceMetadata; + + @Override + public @NonNull String toString() { + return "RenderingUpdateTO{route='" + route + "', resourceId='" + resourceId + "', resourceMetadata='" + + resourceMetadata + "'}"; + } + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushNotificationChangeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushNotificationChangeTO.java new file mode 100644 index 0000000000000..7edcd860ceb79 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushNotificationChangeTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushNotificationChangeTO} encapsulates PUSH_NOTIFICATION_CHANGE messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushNotificationChangeTO extends PushDeviceTO { + public String eventType; + public String notificationId; + public int notificationVersion; + + @Override + public @NonNull String toString() { + return "PushNotificationChangeTO{eventType='" + eventType + "', notificationId='" + notificationId + + "', notificationVersion=" + notificationVersion + ", destinationUserId='" + destinationUserId + + "', dopplerId=" + dopplerId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushVolumeChangeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushVolumeChangeTO.java new file mode 100644 index 0000000000000..71ce962bea8fc --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/push/PushVolumeChangeTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.push; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PushVolumeChangeTO} encapsulates PUSH_VOLUME_CHANGE messages + * + * @author Jan N. Klug - Initial contribution + */ +public class PushVolumeChangeTO extends PushDeviceTO { + public boolean isMuted; + public int volumeSetting; + + @Override + public @NonNull String toString() { + return "PushVolumeChangeTO{isMuted=" + isMuted + ", volumeSetting=" + volumeSetting + ", destinationUserId='" + + destinationUserId + "', dopplerId=" + dopplerId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementContentTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementContentTO.java new file mode 100644 index 0000000000000..67dfb21b38543 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementContentTO.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link AnnouncementContentTO} encapsulate the content of an announcement + * announcements + * + * @author Jan N. Klug - Initial contribution + */ +public class AnnouncementContentTO { + public String locale = ""; + public AnnouncementDisplayTO display = new AnnouncementDisplayTO(); + public AnnouncementSpeakTO speak = new AnnouncementSpeakTO(); + + @Override + public @NonNull String toString() { + return "AnnouncementContentTO{locale='" + locale + "'" + ", display=" + display + ", speak=" + speak + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementDisplayTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementDisplayTO.java new file mode 100644 index 0000000000000..022bb9bda81e3 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementDisplayTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link AnnouncementDisplayTO} encapsulates the display part of an announcement + * + * @author Jan N. Klug - Initial contribution + */ +public class AnnouncementDisplayTO { + public String title; + public String body; + + @Override + public @NonNull String toString() { + return "AnnouncementDisplayTO{title='" + title + "', body='" + body + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementSpeakTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementSpeakTO.java new file mode 100644 index 0000000000000..c597c4a9255e4 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementSpeakTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link AnnouncementSpeakTO} encapsulates the speak part of an announcement + * + * @author Jan N. Klug - Initial contribution + */ +public class AnnouncementSpeakTO { + public String type; + public String value; + + @Override + public @NonNull String toString() { + return "AnnouncementSpeakTO{type='" + type + "', value='" + value + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementTO.java new file mode 100644 index 0000000000000..2fe0356ca5247 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementTO.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link AnnouncementTO} encapsulates an announcement + * + * @author Jan N. Klug - Initial contribution + */ +public class AnnouncementTO { + public String expireAfter = "PT5S"; + public List content = List.of(); + public AnnouncementTargetTO target = new AnnouncementTargetTO(); + public String customerId; + + @Override + public @NonNull String toString() { + return "AnnouncementTO{expireAfter='" + expireAfter + "', content=" + content + ", target=" + target + + ", customerId='" + customerId + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementTargetTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementTargetTO.java new file mode 100644 index 0000000000000..e9620f70c5098 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AnnouncementTargetTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceIdTO; + +/** + * The {@link AnnouncementTargetTO} encapsulate the target section of an announcement + * + * @author Jan N. Klug - Initial contribution + */ +public class AnnouncementTargetTO { + public String customerId; + public List devices = List.of(); + + @Override + public @NonNull String toString() { + return "AnnouncementTargetTO{customerId='" + customerId + "', devices=" + devices + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonTokenResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterAuthTO.java similarity index 56% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonTokenResponse.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterAuthTO.java index 4a5ff4bb84d7e..e93b59e4d8e89 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonTokenResponse.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterAuthTO.java @@ -10,24 +10,24 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.dto.request; -import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import com.google.gson.annotations.SerializedName; /** - * The {@link JsonTokenResponse} encapsulate the GSON data of the token response + * The {@link AuthRegisterAuthTO} encapsulates the auth information of an app registration request * - * @author Michael Geramb - Initial contribution + * @author Jan N. Klug - Initial contribution */ -@NonNullByDefault -public class JsonTokenResponse { +public class AuthRegisterAuthTO { @SerializedName("access_token") public @Nullable String accessToken; - @SerializedName("token_type") - public @Nullable String tokenType; - @SerializedName("expires_in") - public @Nullable Integer expiresIn; + + @Override + public @NonNull String toString() { + return "AuthRegisterAuthTO{accessToken='" + accessToken + "'}"; + } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterCookiesTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterCookiesTO.java new file mode 100644 index 0000000000000..d506394247d7b --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterCookiesTO.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.CookieTO; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthRegisterCookiesTO} encapsulates the cookie information for a given domain + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterCookiesTO { + @SerializedName("website_cookies") + public List webSiteCookies = List.of(); + public String domain = ".amazon.com"; + + @Override + public @NonNull String toString() { + return "AuthRegisterCookiesTO{webSiteCookies=" + webSiteCookies + ", domain='" + domain + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterRegistrationTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterRegistrationTO.java new file mode 100644 index 0000000000000..7ed2a3cfc0220 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterRegistrationTO.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.API_VERSION; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.DI_OS_VERSION; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthRegisterRegistrationTO} encapsulates the registration data for an app registration request + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterRegistrationTO { + public String domain = "Device"; + @SerializedName("app_version") + public String appVersion = API_VERSION; + @SerializedName("device_type") + public String deviceType = "A2IVLV5VM2W81"; + @SerializedName("device_name") + public String deviceName = "%FIRST_NAME%'s%DUPE_STRATEGY_1ST%openHAB Alexa Binding"; + @SerializedName("os_version") + public String osVersion = DI_OS_VERSION; + @SerializedName("device_serial") + public @Nullable String deviceSerial; + @SerializedName("device_model") + public String deviceModel = "iPhone"; + @SerializedName("app_name") + public String appName = "openHAB Alexa Binding"; + @SerializedName("software_version") + public String softwareVersion = "1"; + + @Override + public @NonNull String toString() { + return "AuthRegisterRegistrationTO{domain='" + domain + "', appVersion='" + appVersion + "', deviceType='" + + deviceType + "', deviceName='" + deviceName + "', osVersion='" + osVersion + "', deviceSerial='" + + deviceSerial + "', deviceModel='" + deviceModel + "', appName='" + appName + "', softwareVersion='" + + softwareVersion + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterTO.java new file mode 100644 index 0000000000000..a292e30b785bd --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/AuthRegisterTO.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthRegisterTO} encapsulate the app registration request + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterTO { + @SerializedName("requested_extensions") + public List requestedExtensions = List.of("device_info", "customer_info"); + public AuthRegisterCookiesTO cookies = new AuthRegisterCookiesTO(); + @SerializedName("registration_data") + public AuthRegisterRegistrationTO registrationData = new AuthRegisterRegistrationTO(); + @SerializedName("auth_data") + public AuthRegisterAuthTO authData = new AuthRegisterAuthTO(); + @SerializedName("user_context_map") + public Map userContextMap = Map.of(); + @SerializedName("requested_token_type") + public List requestedTokenType = List.of("bearer", "mac_dms", "website_cookies"); + + @Override + public @NonNull String toString() { + return "AuthRegisterTO{requestedExtensions=" + requestedExtensions + ", cookies=" + cookies + + ", registrationData=" + registrationData + ", authData=" + authData + ", userContextMap=" + + userContextMap + ", requestedTokenType=" + requestedTokenType + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/BehaviorOperationValidateTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/BehaviorOperationValidateTO.java new file mode 100644 index 0000000000000..3db4139bb6848 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/BehaviorOperationValidateTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link BehaviorOperationValidateTO} encapsulates a behavior validation request + * + * @author Jan N. Klug - Initial contribution + */ +public class BehaviorOperationValidateTO { + public String type; + public String operationPayload; + + @Override + public @NonNull String toString() { + return "BehaviorOperationValidateTO{type='" + type + "', operationPayload='" + operationPayload + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/ExchangeTokenResponseTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/ExchangeTokenResponseTO.java new file mode 100644 index 0000000000000..cd65327377880 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/ExchangeTokenResponseTO.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link ExchangeTokenResponseTO} encapsulates the response of an exchange token request + * + * @author Jan N. Klug - Initial contribution + */ +public class ExchangeTokenResponseTO { + public ExchangeTokenTokensTO tokens = new ExchangeTokenTokensTO(); + + @Override + public @NonNull String toString() { + return "ExchangeTokenResponseTO{tokens=" + tokens + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/ExchangeTokenTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/ExchangeTokenTO.java new file mode 100644 index 0000000000000..c6e2515f4b369 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/ExchangeTokenTO.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link ExchangeTokenTO} encapsulates an exchange token request + * + * @author Jan N. Klug - Initial contribution + */ +public class ExchangeTokenTO { + public ExchangeTokenResponseTO response = new ExchangeTokenResponseTO(); + + @Override + public @NonNull String toString() { + return "ExchangeTokenTO{response=" + response + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/ExchangeTokenTokensTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/ExchangeTokenTokensTO.java new file mode 100644 index 0000000000000..33d925e17a47d --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/ExchangeTokenTokensTO.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.CookieTO; + +/** + * The {@link ExchangeTokenTokensTO} encapsulates the token section of an exchange token request + * + * @author Jan N. Klug - Initial contribution + */ +public class ExchangeTokenTokensTO { + public Map> cookies = new HashMap<>(); + + @Override + public @NonNull String toString() { + return "ExchangeTokenTokensTO{cookies=" + cookies + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/PlayerSeekMediaTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/PlayerSeekMediaTO.java new file mode 100644 index 0000000000000..6dc0c3e4da363 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/PlayerSeekMediaTO.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.util.SerializeNull; + +/** + * The {@link PlayerSeekMediaTO} encapsulates a command to seek in a media file + * + * @author Jan N. Klug - Initial contribution + */ +public class PlayerSeekMediaTO { + public String type = "SeekCommand"; + public long mediaPosition; + @SerializeNull + public Object contentFocusClientId = null; + + @Override + public @NonNull String toString() { + return "PlayerSeekMediaTO{type='" + type + "', mediaPosition=" + mediaPosition + ", contentFocusClientId=" + + contentFocusClientId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/SendConversationDTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/SendConversationDTO.java new file mode 100644 index 0000000000000..2194fcb1ab570 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/SendConversationDTO.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link SendConversationDTO} encapsulates a new message to all devices + * + * @author Jan N. Klug - Initial contribution + */ +public class SendConversationDTO { + public String conversationId; + public String clientMessageId; + public int messageId; + public String time; + public String sender; + public String type = "message/text"; + public Map payload = new HashMap<>(); + public int status = 1; + + @Override + public @NonNull String toString() { + return "SendConversationDTO{conversationId='" + conversationId + "', clientMessageId='" + clientMessageId + + "', messageId=" + messageId + ", nextAlarmTime='" + time + "', sender='" + sender + "', type='" + type + + "', payload=" + payload + ", status=" + status + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/StartRoutineTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/StartRoutineTO.java new file mode 100644 index 0000000000000..b0e87498630c7 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/StartRoutineTO.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link StartRoutineTO} encapsulate the request to start a routine + * + * @author Jan N. Klug - Initial contribution + */ +public class StartRoutineTO { + public String behaviorId = "PREVIEW"; + public String sequenceJson; + public String status = "ENABLED"; + + @Override + public @NonNull String toString() { + return "StartRoutineTO{behaviorId='" + behaviorId + "', sequenceJson='" + sequenceJson + "', status='" + status + + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/WHAVolumeLevelTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/WHAVolumeLevelTO.java new file mode 100644 index 0000000000000..8f8e3944514bf --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/request/WHAVolumeLevelTO.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.request; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link WHAVolumeLevelTO} encapsulates a command to set the WHA volume + * + * @author Jan N. Klug - Initial contribution + */ +public class WHAVolumeLevelTO { + public String type = "VolumeLevelCommand"; + public int volumeLevel; + public Object contentFocusClientId = "Default"; + + @Override + public @NonNull String toString() { + return "WHAVolumeLevelTO{type='" + type + "', volumeLevel=" + volumeLevel + ", contentFocusClientId=" + + contentFocusClientId + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AccountTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AccountTO.java new file mode 100644 index 0000000000000..46a43b7ad8db2 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AccountTO.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.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.reflect.TypeToken; + +/** + * The {@link AccountTO} encapsulates the account information + * + * @author Jan N. Klug - Initial contribution + */ +@SuppressWarnings("unused") +public class AccountTO { + @SuppressWarnings("unchecked") + public static final TypeToken> LIST_TYPE_TOKEN = (TypeToken>) TypeToken + .getParameterized(List.class, AccountTO.class); + + public String commsId; + public String directedId; + public String phoneCountryCode; + public String phoneNumber; + public String firstName; + public String lastName; + public String phoneticFirstName; + public String phoneticLastName; + public String commsProvisionStatus; + public Boolean isChild; + public Boolean signedInUser; + public Boolean commsProvisioned; + public Boolean speakerProvisioned; + + @Override + public @NonNull String toString() { + return "AccountTO{commsId='" + commsId + "', directedId='" + directedId + "', phoneCountryCode='" + + phoneCountryCode + "', phoneNumber='" + phoneNumber + "', firstName='" + firstName + "', lastName='" + + lastName + "', phoneticFirstName='" + phoneticFirstName + "', phoneticLastName='" + phoneticLastName + + "', commsProvisionStatus='" + commsProvisionStatus + "', isChild=" + isChild + ", signedInUser=" + + signedInUser + ", commsProvisioned=" + commsProvisioned + ", speakerProvisioned=" + speakerProvisioned + + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AscendingAlarmModelsTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AscendingAlarmModelsTO.java new file mode 100644 index 0000000000000..32ad4d6e2f80f --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AscendingAlarmModelsTO.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.AscendingAlarmModelTO; + +/** + * The {@link AscendingAlarmModelsTO} encapsulates the response of the /api/ascending-alarm + * + * @author Jan N. Klug - Initial contribution + */ +public class AscendingAlarmModelsTO { + public List ascendingAlarmModelList = List.of(); + + @Override + public @NonNull String toString() { + return "AscendingAlarmModelsTO{ascendingAlarmModelList=" + ascendingAlarmModelList + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterBearerTokenTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterBearerTokenTO.java new file mode 100644 index 0000000000000..4d9fa081ea205 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterBearerTokenTO.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthRegisterBearerTokenTO} encapsulates the bearer token information + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterBearerTokenTO { + @SerializedName("access_token") + public String accessToken; + + @SerializedName("refresh_token") + public String refreshToken; + + @SerializedName("expires_in") + public String expiresIn; + + @Override + public @NonNull String toString() { + return "AuthRegisterBearerTO{accessToken='" + accessToken + "', refreshToken='" + refreshToken + + "', expiresIn='" + expiresIn + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterCustomerInfoTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterCustomerInfoTO.java new file mode 100644 index 0000000000000..55bbc56c0cb52 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterCustomerInfoTO.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthRegisterCustomerInfoTO} encapsulates the customer information of a registration response + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterCustomerInfoTO { + @SerializedName("account_pool") + public String accountPool; + @SerializedName("user_id") + public String userId; + @SerializedName("home_region") + public String homeRegion; + public String name; + @SerializedName("given_name") + public String givenName; + + @Override + public @NonNull String toString() { + return "AuthRegisterCustomerInfoTO{accountPool='" + accountPool + "', userId='" + userId + "', homeRegion='" + + homeRegion + "', name='" + name + "', givenName='" + givenName + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterDeviceInfoTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterDeviceInfoTO.java new file mode 100644 index 0000000000000..037d579dccad9 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterDeviceInfoTO.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthRegisterDeviceInfoTO} encapsulates the device information of an app registration response + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterDeviceInfoTO { + @SerializedName("device_name") + public String deviceName = "Unknown"; + @SerializedName("device_serial_number") + public String deviceSerialNumber; + @SerializedName("device_type") + public String deviceType; + + @Override + public @NonNull String toString() { + return "AuthRegisterDeviceInfoTO{deviceName='" + deviceName + "', deviceSerialNumber='" + deviceSerialNumber + + "', deviceType='" + deviceType + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterExtensionsTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterExtensionsTO.java new file mode 100644 index 0000000000000..ab7bc161110cd --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterExtensionsTO.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthRegisterExtensionsTO} encapsulates the extension part of an app registration response + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterExtensionsTO { + @SerializedName("device_info") + public AuthRegisterDeviceInfoTO deviceInfo = new AuthRegisterDeviceInfoTO(); + @SerializedName("customer_info") + public AuthRegisterCustomerInfoTO customerInfo = new AuthRegisterCustomerInfoTO(); + @SerializedName("customer_id") + public @Nullable String customerId; + + @Override + public @NonNull String toString() { + return "AuthRegisterExtensions" + "TO{deviceInfo=" + deviceInfo + ", customerInfo=" + customerInfo + + ", customerId='" + customerId + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterMacDmsTokenTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterMacDmsTokenTO.java new file mode 100644 index 0000000000000..d35ffdeb0820b --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterMacDmsTokenTO.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthRegisterMacDmsTokenTO} encapsulates MAC dms tokens + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterMacDmsTokenTO { + @SerializedName("device_private_key") + public String devicePrivateKey; + + @SerializedName("adp_token") + public String adpToken; + + @Override + public @NonNull String toString() { + return "AuthRegisterMacDmsTokenTO{devicePrivateKey='" + devicePrivateKey + "', adpToken='" + adpToken + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterResponseTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterResponseTO.java new file mode 100644 index 0000000000000..beb5b1255be91 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterResponseTO.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link AuthRegisterResponseTO} encapsulates the internal response section of an app registration response + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterResponseTO { + public AuthRegisterSuccessTO success = new AuthRegisterSuccessTO(); + + @Override + public @NonNull String toString() { + return "AuthRegisterResponseTO{success=" + success + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterSuccessTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterSuccessTO.java new file mode 100644 index 0000000000000..a9986078f1cc2 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterSuccessTO.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthRegisterSuccessTO} encapsulates the success section of an app registration response + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterSuccessTO { + public AuthRegisterExtensionsTO extensions = new AuthRegisterExtensionsTO(); + public AuthRegisterTokensTO tokens = new AuthRegisterTokensTO(); + @SerializedName("customer_id") + public String customerId; + + @Override + public @NonNull String toString() { + return "AuthRegisterSuccessTO{extensions=" + extensions + ", tokens=" + tokens + ", customerId='" + customerId + + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterTO.java new file mode 100644 index 0000000000000..08a0bffcc11fe --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthRegisterTO} encapsulate an app registration response + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterTO { + @SerializedName("request_id") + public String requestId; + public AuthRegisterResponseTO response = new AuthRegisterResponseTO(); + + @Override + public @NonNull String toString() { + return "AuthRegister{requestId='" + requestId + "', response=" + response + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterTokensTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterTokensTO.java new file mode 100644 index 0000000000000..472c095a05b72 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthRegisterTokensTO.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.CookieTO; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthRegisterTokensTO} encapsulates all tokens for a connection registration response + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthRegisterTokensTO { + @SerializedName("website_cookies") + public List websiteCookies = List.of(); + @SerializedName("mac_dms") + public AuthRegisterMacDmsTokenTO macDms = new AuthRegisterMacDmsTokenTO(); + public AuthRegisterBearerTokenTO bearer = new AuthRegisterBearerTokenTO(); + + @Override + public @NonNull String toString() { + return "AuthRegisterTokensTO{websiteCookies=" + websiteCookies + ", macDms=" + macDms + ", bearer=" + bearer + + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthTokenTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthTokenTO.java new file mode 100644 index 0000000000000..ad4f55e4d92b9 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AuthTokenTO.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AuthTokenTO} encapsulates the response of a request to /auth/token + * + * @author Jan N. Klug - Initial contribution + */ +public class AuthTokenTO { + @SerializedName("access_token") + public String accessToken; + @SerializedName("token_type") + public String tokenType; + @SerializedName("expires_in") + public long expiresIn; + + @Override + public @NonNull String toString() { + return "AuthTokenTO{accessToken='" + accessToken + "', tokenType='" + tokenType + "', expiresIn=" + expiresIn + + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AutomationPayloadTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AutomationPayloadTO.java new file mode 100644 index 0000000000000..05f46dc3fd867 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AutomationPayloadTO.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link AutomationPayloadTO} encapsulates the payload section of n automation + * + * @author Jan N. Klug - Initial contribution + */ +public class AutomationPayloadTO { + public String customerId; + public String utterance; + public String locale; + public String marketplaceId; + + @Override + public @NonNull String toString() { + return "AutomationPayloadTO{customerId='" + customerId + "', utterance='" + utterance + "', locale='" + locale + + "', marketplaceId='" + marketplaceId + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AutomationTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AutomationTO.java new file mode 100644 index 0000000000000..af9218144dc44 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AutomationTO.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; +import java.util.TreeMap; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.reflect.TypeToken; + +/** + * The {@link AutomationTO} encapsulates a single routine/automation + * + * @author Jan N. Klug - Initial contribution + */ +public class AutomationTO { + @SuppressWarnings("unchecked") + public static final TypeToken> LIST_TYPE_TOKEN = (TypeToken>) TypeToken + .getParameterized(List.class, AutomationTO.class); + public String automationId; + public String name; + public List triggers = List.of(); + public TreeMap sequence; + public String status; + public long creationTimeEpochMillis; + public long lastUpdatedTimeEpochMillis; + + @Override + public @NonNull String toString() { + return "AutomationTO{automationId='" + automationId + "', name='" + name + "', triggers=" + triggers + + ", sequence=" + sequence + ", status='" + status + "', creationTimeEpochMillis=" + + creationTimeEpochMillis + ", lastUpdatedTimeEpochMillis=" + lastUpdatedTimeEpochMillis + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AutomationTriggerTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AutomationTriggerTO.java new file mode 100644 index 0000000000000..50c8e18d9641d --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/AutomationTriggerTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link AutomationTriggerTO} encapsulates the trigger section of an automation + * + * @author Jan N. Klug - Initial contribution + */ +public class AutomationTriggerTO { + public AutomationPayloadTO payload = new AutomationPayloadTO(); + public String id; + public String type; + + @Override + public @NonNull String toString() { + return "AutomationTriggerTO{payload=" + payload + ", id='" + id + "', type='" + type + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BluetoothStateTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BluetoothStateTO.java new file mode 100644 index 0000000000000..1dfaff8eb335c --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BluetoothStateTO.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.BluetoothPairedDeviceTO; + +/** + * The {@link BluetoothStateTO} encapsulate a single bluetoth state + * + * @author Jan N. Klug - Initial contribution + */ +public class BluetoothStateTO { + public String deviceSerialNumber; + public String deviceType; + public String friendlyName; + public boolean gadgetPaired; + public boolean online; + public List pairedDeviceList = List.of(); + + @Override + public @NonNull String toString() { + return "BluetoothStateTO{deviceSerialNumber='" + deviceSerialNumber + "', deviceType='" + deviceType + + "', friendlyName='" + friendlyName + "', gadgetPaired=" + gadgetPaired + ", online=" + online + + ", pairedDeviceList=" + pairedDeviceList + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BluetoothStatesTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BluetoothStatesTO.java new file mode 100644 index 0000000000000..0cfab281c3c34 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BluetoothStatesTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link BluetoothStatesTO} encapsulate the response of /api/bluetooth + * + * @author Jan N. Klug - Initial contribution + */ +public class BluetoothStatesTO { + public List bluetoothStates = List.of(); + + @Override + public @NonNull String toString() { + return "BluetoothStatesTO{bluetoothStates=" + bluetoothStates + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BootstrapAuthenticationTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BootstrapAuthenticationTO.java new file mode 100644 index 0000000000000..5762c38cd66a4 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BootstrapAuthenticationTO.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link BootstrapAuthenticationTO} encapsulates the authentication information of a bootstrap response + * + * @author Jan N. Klug - Initial contribution + */ +public class BootstrapAuthenticationTO { + public boolean authenticated; + public boolean canAccessPrimeMusicContent; + public String customerEmail; + public String customerId; + public String customerName; + + @Override + public @NonNull String toString() { + return "BootstrapAuthenticationTO{authenticated=" + authenticated + ", canAccessPrimeMusicContent=" + + canAccessPrimeMusicContent + ", customerEmail='" + customerEmail + "', customerId='" + customerId + + "', customerName='" + customerName + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BootstrapTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BootstrapTO.java new file mode 100644 index 0000000000000..f6c33583680e2 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/BootstrapTO.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link BootstrapTO} encapsulate the response of a boostrap request + * + * @author Jan N. Klug - Initial contribution + */ +public class BootstrapTO { + public BootstrapAuthenticationTO authentication; + + @Override + public @NonNull String toString() { + return "BootstrapTO{authentication=" + authentication + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/CustomerHistoryRecordTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/CustomerHistoryRecordTO.java new file mode 100644 index 0000000000000..d9f1bc53a7cf3 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/CustomerHistoryRecordTO.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link CustomerHistoryRecordTO} encapsulates + * + * @author Jan N. Klug - Initial contribution + */ +public class CustomerHistoryRecordTO { + public String recordKey; + public String recordType; + public long timestamp; + public String customerId; + public Object device; + public boolean isBinaryFeedbackProvided; + public boolean isFeedbackPositive; + public String utteranceType; + public String domain; + public String intent; + public String skillName; + public List voiceHistoryRecordItems = List.of(); + public List personsInfo = List.of(); + + @Override + public @NonNull String toString() { + return "CustomerHistoryRecordTO{recordKey='" + recordKey + "', recordType='" + recordType + "', timestamp=" + + timestamp + ", customerId='" + customerId + "', device=" + device + ", isBinaryFeedbackProvided=" + + isBinaryFeedbackProvided + ", isFeedbackPositive=" + isFeedbackPositive + ", utteranceType='" + + utteranceType + "', domain='" + domain + "', intent='" + intent + "', skillName='" + skillName + + "', voiceHistoryRecordItems=" + voiceHistoryRecordItems + ", personsInfo=" + personsInfo + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/CustomerHistoryRecordVoiceTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/CustomerHistoryRecordVoiceTO.java new file mode 100644 index 0000000000000..8bc76a4c7dc92 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/CustomerHistoryRecordVoiceTO.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link CustomerHistoryRecordVoiceTO} encapsulates a voice history record of a customer history + * + * @author Jan N. Klug - Initial contribution + */ +public class CustomerHistoryRecordVoiceTO { + public String recordItemKey; + public String recordItemType; + public String utteranceId; + public long timestamp; + public String transcriptText; + public String agentVisualName; + public List personsInfo = List.of(); + + @Override + public @NonNull String toString() { + return "CustomerHistoryRecordVoiceTO{recordItemKey='" + recordItemKey + "', recordItemType='" + recordItemType + + "', utteranceId='" + utteranceId + "', timestamp=" + timestamp + ", transcriptText='" + transcriptText + + "', agentVisualName='" + agentVisualName + "', personsInfo=" + personsInfo + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/CustomerHistoryRecordsTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/CustomerHistoryRecordsTO.java new file mode 100644 index 0000000000000..ecb34d9095a69 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/CustomerHistoryRecordsTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link CustomerHistoryRecordsTO} encapsulate the response for customer history record requests + * + * @author Jan N. Klug - Initial contribution + */ +public class CustomerHistoryRecordsTO { + public List customerHistoryRecords = List.of(); + + @Override + public @NonNull String toString() { + return "CustomerHistoryRecordsTO{customerHistoryRecords=" + customerHistoryRecords + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/DeviceListTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/DeviceListTO.java new file mode 100644 index 0000000000000..a769e33f08bbf --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/DeviceListTO.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceTO; + +/** + * The {@link DeviceListTO} encapsulate the response of /api/devices-v2 + * + * @author Jan N. Klug - Initial contribution + */ +public class DeviceListTO { + public List devices = List.of(); + + @Override + public @NonNull String toString() { + return "DeviceListTO{devices=" + devices + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/DeviceNotificationStatesTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/DeviceNotificationStatesTO.java new file mode 100644 index 0000000000000..57bb0326f6eb6 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/DeviceNotificationStatesTO.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceNotificationStateTO; + +/** + * The {@link DeviceNotificationStatesTO} encapsulate the response of the /api/device-notification-state + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class DeviceNotificationStatesTO { + public List deviceNotificationStates = List.of(); + + @Override + public @NonNull String toString() { + return "DeviceNotificationStatesTO{deviceNotificationStates=" + deviceNotificationStates + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/DoNotDisturbDeviceStatusesTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/DoNotDisturbDeviceStatusesTO.java new file mode 100644 index 0000000000000..b9ba51d04827d --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/DoNotDisturbDeviceStatusesTO.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.DoNotDisturbDeviceStatusTO; + +/** + * The {@link DoNotDisturbDeviceStatusesTO} encapsulate the response of /api/dnd/device-status-list + * + * @author Jan N. Klug - Initial contribution + */ +public class DoNotDisturbDeviceStatusesTO { + public List doNotDisturbDeviceStatusList = List.of(); + + @Override + public @NonNull String toString() { + return "DoNotDisturbDeviceStatusesTO{doNotDisturbDeviceStatusList=" + doNotDisturbDeviceStatusList + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/EligibilityTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/EligibilityTO.java new file mode 100644 index 0000000000000..316e2620bc41d --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/EligibilityTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link EligibilityTO} encapsulates the eligibility section of a media session + * + * @author Jan N. Klug - Initial contribution + */ +public class EligibilityTO { + public boolean isEligible; + public String reasonCode; + + @Override + public @NonNull String toString() { + return "EligibilityTO{isEligible=" + isEligible + ", reasonCode='" + reasonCode + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/EndpointTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/EndpointTO.java new file mode 100644 index 0000000000000..a423f5f89b0cc --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/EndpointTO.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link DoNotDisturbDeviceStatusesTO} encapsulate the response of /api/endpoints + * + * @author Jan N. Klug - Initial contribution + */ +public class EndpointTO { + public String alexaApiUrl; + public String awsRegion; + public String retailDomain; + public String retailUrl; + public String skillsStoreUrl; + public String websiteApiUrl; + public String websiteUrl; + + @Override + public @NonNull String toString() { + return "EndpointTO{alexaApiUrl='" + alexaApiUrl + "', awsRegion='" + awsRegion + "', retailDomain='" + + retailDomain + "', retailUrl='" + retailUrl + "', skillsStoreUrl='" + skillsStoreUrl + + "', websiteApiUrl='" + websiteApiUrl + "', websiteUrl='" + websiteUrl + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/ListItemTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/ListItemTO.java new file mode 100644 index 0000000000000..63aae67553fa0 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/ListItemTO.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link ListItemTO} encapsulates a single list item + * + * @author Jan N. Klug - Initial contribution + */ +public class ListItemTO { + public String listId; + public boolean shoppingListItem; + public String customerId; + public long createdDateTime; + public boolean completed; + public String id; + public String value; + public int version; + public long updatedDateTime; + + @Override + public @NonNull String toString() { + return "ListItemTO{listId='" + listId + "', shoppingListItem=" + shoppingListItem + ", customerId='" + + customerId + "', createdDateTime=" + createdDateTime + ", completed=" + completed + ", id='" + id + + "', value='" + value + "', version=" + version + ", updatedDateTime=" + updatedDateTime + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushMediaChange.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/ListMediaSessionTO.java similarity index 50% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushMediaChange.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/ListMediaSessionTO.java index 814909ba63572..dc39c6621bb86 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushMediaChange.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/ListMediaSessionTO.java @@ -10,18 +10,22 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.dto.response; -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; /** - * The {@link JsonCommandPayloadPushMediaChange} encapsulate the GSON data of automation query + * The {@link ListMediaSessionTO} encapsulates the response to /api/np/list-media.sessions * - * @author Michael Geramb - Initial contribution + * @author Jan N. Klug - Initial contribution */ -@NonNullByDefault -public class JsonCommandPayloadPushMediaChange extends JsonCommandPayloadPushDevice { - public @Nullable String destinationUserId; - public @Nullable String mediaReferenceId; +public class ListMediaSessionTO { + public List mediaSessionList = List.of(); + + @Override + public @NonNull String toString() { + return "ListMediaSessionTO{mediaSessionList=" + mediaSessionList + "}"; + } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaSessionEndpointTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaSessionEndpointTO.java new file mode 100644 index 0000000000000..937eef193b565 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaSessionEndpointTO.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceIdTO; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link MediaSessionEndpointTO} encapsulates a single endpoint information for a media session + * + * @author Jan N. Klug - Initial contribution + */ +public class MediaSessionEndpointTO { + @SerializedName("__type") + public String type; + public String encryptedFriendlyName; + public DeviceIdTO id; + + @Override + public @NonNull String toString() { + return "MediaSessionEndpointTO{type='" + type + "', encryptedFriendlyName='" + encryptedFriendlyName + "', id=" + + id + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaSessionTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaSessionTO.java new file mode 100644 index 0000000000000..23e52b20518cc --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaSessionTO.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.PlayerStateInfoTO; + +/** + * The {@link MediaSessionTO} encapsulates a single media session + * + * @author Jan N. Klug - Initial contribution + */ +public class MediaSessionTO { + public EligibilityTO castEligibility; + public List endpointList = List.of(); + public PlayerStateInfoTO nowPlayingData; + + @Override + public @NonNull String toString() { + return "MediaSessionTO{castEligibility=" + castEligibility + ", endpointList=" + endpointList + + ", nowPlayingData=" + nowPlayingData + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaStateQueueEntryTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaStateQueueEntryTO.java new file mode 100644 index 0000000000000..8ee8d2be61b73 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaStateQueueEntryTO.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link MediaStateQueueEntryTO} encapsulates a queue entry in media content state + * + * @author Jan N. Klug - Initial contribution + */ +public class MediaStateQueueEntryTO { + public String album; + public String albumAsin; + public String artist; + public String asin; + public String cardImageURL; + public String contentId; + public String contentType; + public int durationSeconds; + public boolean feedbackDisabled; + public String historicalId; + public String imageURL; + public int index; + public boolean isAd; + public boolean isDisliked; + public boolean isFreeWithPrime; + public boolean isLiked; + public String programId; + public String programName; + public String providerId; + public String queueId; + public String radioStationCallSign; + public String radioStationId; + public String radioStationLocation; + public String radioStationSlogan; + public String referenceId; + public String service; + public String startTime; + public String title; + public String trackId; + public String trackStatus; + + @Override + public @NonNull String toString() { + return "MediaStateQueueEntryTO{album='" + album + "', albumAsin='" + albumAsin + "', artist='" + artist + + "', asin='" + asin + "', cardImageURL='" + cardImageURL + "', contentId='" + contentId + + "', contentType='" + contentType + "', durationSeconds=" + durationSeconds + ", feedbackDisabled=" + + feedbackDisabled + ", historicalId='" + historicalId + "', imageURL='" + imageURL + "', index=" + + index + ", isAd=" + isAd + ", isDisliked=" + isDisliked + ", isFreeWithPrime=" + isFreeWithPrime + + ", isLiked=" + isLiked + ", programId='" + programId + "', programName='" + programName + + "', providerId='" + providerId + "', queueId='" + queueId + "', radioStationCallSign='" + + radioStationCallSign + "', radioStationId='" + radioStationId + "', radioStationLocation='" + + radioStationLocation + "', radioStationSlogan='" + radioStationSlogan + "', referenceId='" + + referenceId + "', service='" + service + "', startTime='" + startTime + "', title='" + title + + "', trackId='" + trackId + "', trackStatus='" + trackStatus + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaStateTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaStateTO.java new file mode 100644 index 0000000000000..3fe22d7eb0f94 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MediaStateTO.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link MediaStateTO} encapsulates the state of a media content + * + * @author Jan N. Klug - Initial contribution + */ +public class MediaStateTO { + + public String clientId; + public String contentId; + public String contentType; + public String currentState; + public String imageURL; + public boolean isDisliked; + public boolean isLiked; + public boolean looping; + public String mediaOwnerCustomerId; + public boolean muted; + public String programId; + public int progressSeconds; + public String providerId; + public List queue = List.of(); + public String queueId; + public int queueSize; + public int radioVariety; + public String referenceId; + public String service; + public boolean shuffling; + public int volume; + + @Override + public @NonNull String toString() { + return "MediaStateTO{clientId='" + clientId + "', contentId='" + contentId + "', contentType='" + contentType + + "', currentState='" + currentState + "', imageURL='" + imageURL + "', isDisliked=" + isDisliked + + ", isLiked=" + isLiked + ", looping=" + looping + ", mediaOwnerCustomerId='" + mediaOwnerCustomerId + + "', muted=" + muted + ", programId='" + programId + "', progressSeconds=" + progressSeconds + + ", providerId='" + providerId + "', queue=" + queue + ", queueId='" + queueId + "', queueSize=" + + queueSize + "', radioVariety=" + radioVariety + ", referenceId='" + referenceId + "', service='" + + service + "', shuffling=" + shuffling + ", volume=" + volume + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MusicProviderDataTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MusicProviderDataTO.java new file mode 100644 index 0000000000000..c3d56416cdbc5 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MusicProviderDataTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link MusicProviderDataTO} encapsulates music provider metadata + * + * @author Jan N. Klug - Initial contribution + */ +public class MusicProviderDataTO { + public boolean isDefaultMusicProvider; + public boolean isDefaultStationProvider; + + @Override + public @NonNull String toString() { + return "MusicProviderDataTO{isDefaultMusicProvider=" + isDefaultMusicProvider + ", isDefaultStationProvider=" + + isDefaultStationProvider + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MusicProviderTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MusicProviderTO.java new file mode 100644 index 0000000000000..16daca4b5d181 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/MusicProviderTO.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.reflect.TypeToken; + +/** + * The {@link MusicProviderTO} encapsulate a single music provider + * + * @author Jan N. Klug - Initial contribution + */ +public class MusicProviderTO { + @SuppressWarnings("unchecked") + public static final TypeToken> LIST_TYPE_TOKEN = (TypeToken>) TypeToken + .getParameterized(List.class, MusicProviderTO.class); + + public String id; + public String displayName; + public String description; + public List supportedProperties = List.of(); + public List supportedTriggers = List.of(); + public List supportedOperations = List.of(); + public String availability; + public String icon; + public MusicProviderDataTO providerData = new MusicProviderDataTO(); + + @Override + public @NonNull String toString() { + return "MusicProviderTO{id='" + id + "', displayName='" + displayName + "', description='" + description + + "', supportedProperties=" + supportedProperties + ", supportedTriggers=" + supportedTriggers + + ", supportedOperations=" + supportedOperations + ", availability='" + availability + "', icon='" + + icon + "', providerData=" + providerData + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NamedListsInfoTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NamedListsInfoTO.java new file mode 100644 index 0000000000000..2d813b0a0c72f --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NamedListsInfoTO.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link NamedListsInfoTO} encapsulates the response of /api/namedLists/listId + * + * @author Jan N. Klug - Initial contribution + */ +public class NamedListsInfoTO { + public List listIds; + public long updatedDate; + public String type; + public int version; + public boolean defaultList; + public boolean archived; + public String itemId; + public long createdDate; + public Object listReorderVersion; + public Object originalAudioId; + public String customerId; + public String name; + public Object nbestItems; + + @Override + public @NonNull String toString() { + return "NamedListsInfoTO{listIds=" + listIds + ", updatedDate=" + updatedDate + ", type='" + type + + "', version=" + version + ", defaultList=" + defaultList + ", archived=" + archived + ", itemId='" + + itemId + "', createdDate=" + createdDate + ", listReorderVersion=" + listReorderVersion + + ", originalAudioId=" + originalAudioId + ", customerId='" + customerId + "', name=" + name + + ", nbestItems=" + nbestItems + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NamedListsItemsTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NamedListsItemsTO.java new file mode 100644 index 0000000000000..6b8261aea7352 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NamedListsItemsTO.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link NamedListsItemsTO} encapsulate the response of /api/namedLists/listId/items + * + * @author Jan N. Klug - Initial contribution + */ +public class NamedListsItemsTO { + public Object listReorderVersion; + public List list; + + @Override + public @NonNull String toString() { + return "NamedListsItemsTO{listReorderVersion=" + listReorderVersion + ", list=" + list + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NotificationListResponseTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NotificationListResponseTO.java new file mode 100644 index 0000000000000..b500072108801 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NotificationListResponseTO.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.NotificationTO; + +/** + * The {@link NotificationListResponseTO} encapsulate response of a /api/notifications + * + * @author Jan N. Klug - Initial contribution + */ +public class NotificationListResponseTO { + public List notifications = List.of(); + + @Override + public @NonNull String toString() { + return "NotificationListResponseTO{notifications=" + notifications + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NotificationSoundResponseTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NotificationSoundResponseTO.java new file mode 100644 index 0000000000000..2bd7da8322ffc --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/NotificationSoundResponseTO.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.NotificationSoundTO; + +/** + * The {@link NotificationSoundResponseTO} encapsulate response of /api/notification/sounds + * + * @author Jan N. Klug - Initial contribution + */ +public class NotificationSoundResponseTO { + public List notificationSounds = List.of(); + + @Override + public @NonNull String toString() { + return "NotificationSoundResponseTO{notificationSounds=" + notificationSounds + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/PlayListTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/PlayListTO.java new file mode 100644 index 0000000000000..7eccf638e4c4f --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/PlayListTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PlayListTO} encapsulates a single playlist + * + * @author Jan N. Klug - Initial contribution + */ +public class PlayListTO { + public String playlistId; + public String title; + public int trackCount; + + @Override + public @NonNull String toString() { + return "PlayListTO{playlistId='" + playlistId + "', title='" + title + "', trackCount=" + trackCount + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/PlayerStateTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/PlayerStateTO.java new file mode 100644 index 0000000000000..bae1a3cf1446f --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/PlayerStateTO.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.amazonechocontrol.internal.dto.PlayerStateInfoTO; + +/** + * The {@link PlayerStateTO} encapsulate the response of a request to /api/np/player + * + * @author Jan N. Klug - Initial contribution + */ +public class PlayerStateTO { + public PlayerStateInfoTO playerInfo = new PlayerStateInfoTO(); + + @Override + public @NonNull String toString() { + return "PlayerStateTO{playerInfo=" + playerInfo + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/PlaylistsTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/PlaylistsTO.java new file mode 100644 index 0000000000000..269c8c2eba5d4 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/PlaylistsTO.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link PlayerStateTO} encapsulates the response of a playlist request + * + * @author Jan N. Klug - Initial contribution + */ +public class PlaylistsTO { + public Map> playlists = Map.of(); + + @Override + public @NonNull String toString() { + return "PlaylistsTO{playlists=" + playlists + "}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/UsersMeTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/UsersMeTO.java new file mode 100644 index 0000000000000..dcb92d8709d7a --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/UsersMeTO.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link UsersMeTO} encapsulate the response of /api/users/me + * + * @author Michael Geramb - Initial contribution + */ +public class UsersMeTO { + public String countryOfResidence; + public String effectiveMarketPlaceId; + public String email; + public Boolean eulaAcceptance; + public List features = List.of(); + public String fullName; + public Boolean hasActiveDopplers; + public String id; + public String marketPlaceDomainName; + public String marketPlaceId; + public String marketPlaceLocale; + + @Override + public @NonNull String toString() { + return "UsersMeTO{countryOfResidence='" + countryOfResidence + "', effectiveMarketPlaceId='" + + effectiveMarketPlaceId + "', email='" + email + "', eulaAcceptance=" + eulaAcceptance + ", features=" + + features + ", fullName='" + fullName + "'" + ", hasActiveDopplers=" + hasActiveDopplers + ", id='" + + id + "', marketPlaceDomainName='" + marketPlaceDomainName + "', marketPlaceId='" + marketPlaceId + + "', marketPlaceLocale='" + marketPlaceLocale + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/WakeWordTO.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/WakeWordTO.java new file mode 100644 index 0000000000000..ad7885c8ddfe0 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/WakeWordTO.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.dto.response; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * The {@link WakeWordTO} encapsulates a single wake word definition + * + * @author Jan N. Klug - Initial contribution + */ +public class WakeWordTO { + public boolean active; + public String deviceSerialNumber; + public String deviceType; + public Object midFieldState; + public String wakeWord; + + @Override + public @NonNull String toString() { + return "WakeWordTO{active=" + active + ", deviceSerialNumber='" + deviceSerialNumber + "', deviceType='" + + deviceType + "', midFieldState=" + midFieldState + ", wakeWord='" + wakeWord + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSounds.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/WakeWordsTO.java similarity index 54% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSounds.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/WakeWordsTO.java index d313bd291580b..f41586b5f44c0 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSounds.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/response/WakeWordsTO.java @@ -10,19 +10,22 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.dto.response; import java.util.List; -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jdt.annotation.NonNull; /** - * The {@link JsonNotificationSounds} encapsulate the GSON data for a notification sounds + * The {@link WakeWordsTO} encapsulate the response of a request to /api/wake-word * * @author Michael Geramb - Initial contribution */ -@NonNullByDefault -public class JsonNotificationSounds { - public @Nullable List notificationSounds; +public class WakeWordsTO { + public List wakeWords = List.of(); + + @Override + public @NonNull String toString() { + return "WakeWords{wakeWords=" + wakeWords + "}"; + } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeCapability.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeCapability.java new file mode 100644 index 0000000000000..96c27c96e4191 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeCapability.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.amazonechocontrol.internal.dto.smarthome; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link JsonSmartHomeCapability} encapsulates a smarthome capability API response + * + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class JsonSmartHomeCapability { + public @Nullable String capabilityType; + public @Nullable String type; + public @Nullable String version; + public @Nullable String interfaceName; + public @Nullable Properties properties; + public @Nullable Configuration configuration; + public @Nullable Resources resources; + public @Nullable String instance; + + @Override + public String toString() { + return "SmartHomeCapability{capabilityType='" + capabilityType + "', type='" + type + "', version='" + version + + "', interfaceName='" + interfaceName + "', properties=" + properties + "', configuration=" + + configuration + "', resources=" + resources + "}"; + } + + public static class Properties { + public @Nullable List supported; + public boolean proactivelyReported = false; + public boolean retrievable = false; + public boolean readOnly = true; + + @Override + public String toString() { + return "Properties{supported=" + supported + ", proactivelyReported=" + proactivelyReported + + ", retrievable=" + retrievable + ", readOnly=" + readOnly + "}"; + } + + public static class Property { + public @Nullable String name; + + @Override + public String toString() { + return "Property{name='" + name + "'}"; + } + } + } + + public static class Configuration { + public @Nullable Range supportedRange; + public @Nullable String unitOfMeasure; + public @Nullable List presets; + + @Override + public String toString() { + return "Configuration{supportedRange=" + supportedRange + ", unitOfMeasure='" + unitOfMeasure + "'" + + ", presets=" + presets + "}"; + } + + public static class Range { + public @Nullable Double minimumValue; + public @Nullable Double maximumValue; + public @Nullable Double precision; + + @Override + public String toString() { + return "Range{minimumValue=" + minimumValue + ", maximumValue=" + maximumValue + ", precision=" + + precision + "}"; + } + } + } + + public static class Resources { + public @Nullable List friendlyNames; + + @Override + public String toString() { + return "Resources{friendlyNames=" + friendlyNames + "}"; + } + + public static class Names { + public @Nullable Value value; + @SerializedName("@type") + public @Nullable String type; + + @Override + public String toString() { + return "Names{value=" + value + ", type='" + type + "'}"; + } + + public static class Value { + public @Nullable String assetId; + public @Nullable String text; + public @Nullable String locale; + + @Override + public String toString() { + return "Value{assetId='" + assetId + "'" + ", text='" + text + "'" + ", locale='" + locale + "'}"; + } + } + } + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeDevice.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeDevice.java new file mode 100644 index 0000000000000..109f1fb9c3ef5 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeDevice.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.amazonechocontrol.internal.dto.smarthome; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDeviceNetworkState.SmartHomeDeviceNetworkState; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeTags.JsonSmartHomeTag; + +/** + * The {@link JsonSmartHomeDevice} encapsulates smarthome device API responses + * + * @author Lukas Knoeller - Initial contribution + */ +@NonNullByDefault +public class JsonSmartHomeDevice implements SmartHomeBaseDevice { + public @Nullable Integer updateIntervalInSeconds; + public @Nullable String applianceId; + public @Nullable String manufacturerName; + public @Nullable String friendlyDescription; + public @Nullable String modelName; + public @Nullable String friendlyName; + public @Nullable String reachability; + public @Nullable String entityId; + public @Nullable SmartHomeDeviceNetworkState applianceNetworkState; + public @Nullable List capabilities; + public @Nullable JsonSmartHomeTag tags; + public @Nullable List applianceTypes; + public @Nullable List aliases; + public @Nullable List groupDevices; + public @Nullable String connectedVia; + public @Nullable List alexaDeviceIdentifierList; + public @Nullable DriverIdentity driverIdentity; + public @Nullable List mergedApplianceIds; + public @Nullable List smarthomeDevices; + + public List getCapabilities() { + return Objects.requireNonNullElse(capabilities, List.of()); + } + + @Override + public @Nullable String findId() { + return applianceId; + } + + @Override + public boolean isGroup() { + return false; + } + + @Override + public String toString() { + return "JsonSmartHomeDevice{" + "updateIntervalInSeconds=" + updateIntervalInSeconds + ", applianceId='" + + applianceId + "'" + ", manufacturerName='" + manufacturerName + "'" + ", friendlyDescription='" + + friendlyDescription + "'" + ", modelName='" + modelName + "'" + ", friendlyName='" + friendlyName + + "'" + ", reachability='" + reachability + "'" + ", entityId='" + entityId + "'" + + ", applianceNetworkState=" + applianceNetworkState + ", capabilities=" + capabilities + ", tags=" + + tags + ", applianceTypes=" + applianceTypes + ", aliases=" + aliases + ", groupDevices=" + + groupDevices + ", connectedVia='" + connectedVia + "'" + ", alexaDeviceIdentifierList=" + + alexaDeviceIdentifierList + ", driverIdentity=" + driverIdentity + ", mergedApplianceIds=" + + mergedApplianceIds + ", smarthomeDevices=" + smarthomeDevices + "}"; + } + + public static class DriverIdentity { + public @Nullable String namespace; + public @Nullable String identifier; + + @Override + public String toString() { + return "DriverIdentity{" + "namespace='" + namespace + '\'' + ", identifier='" + identifier + '\'' + '}'; + } + } + + public static class DeviceIdentifier { + public @Nullable String dmsDeviceSerialNumber; + public @Nullable String dmsDeviceTypeId; + + @Override + public String toString() { + return "DeviceIdentifier{" + "dmsDeviceSerialNumber='" + dmsDeviceSerialNumber + "'" + ", dmsDeviceTypeId='" + + dmsDeviceTypeId + "'" + "}"; + } + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceAlias.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeDeviceAlias.java similarity index 72% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceAlias.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeDeviceAlias.java index 49842fd0e69ae..596a069459c22 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceAlias.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeDeviceAlias.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.dto.smarthome; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -23,11 +23,8 @@ public class JsonSmartHomeDeviceAlias { public @Nullable String friendlyName; public @Nullable Boolean enabled; - public JsonSmartHomeDeviceAlias(String friendlyName, Boolean enabled) { - this.friendlyName = friendlyName; - this.enabled = enabled; - } - - public JsonSmartHomeDeviceAlias() { + @Override + public String toString() { + return "JsonSmartHomeDeviceAlias{friendlyName='" + friendlyName + "', enabled=" + enabled + "}"; } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceNetworkState.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeDeviceNetworkState.java similarity index 68% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceNetworkState.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeDeviceNetworkState.java index 2d4873c57cbd2..4c75b3ff2f75b 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDeviceNetworkState.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeDeviceNetworkState.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.dto.smarthome; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -24,7 +24,17 @@ public class JsonSmartHomeDeviceNetworkState { public static class SmartHomeDeviceNetworkState { public @Nullable String reachability; + + @Override + public String toString() { + return "SmartHomeDeviceNetworkState{reachability='" + reachability + "'}"; + } } public @Nullable SmartHomeDeviceNetworkState networkState; + + @Override + public String toString() { + return "JsonSmartHomeDeviceNetworkState{networkState=" + networkState + "}"; + } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentifiers.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeGroupIdentifiers.java similarity index 69% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentifiers.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeGroupIdentifiers.java index cd75624414ae1..1830f8bd3255d 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentifiers.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeGroupIdentifiers.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.dto.smarthome; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -23,7 +23,17 @@ public class JsonSmartHomeGroupIdentifiers { public static class SmartHomeGroupIdentifier { public @Nullable String value; + + @Override + public String toString() { + return "SmartHomeGroupIdentifier{value='" + value + "'}"; + } } public @Nullable SmartHomeGroupIdentifier identifier; + + @Override + public String toString() { + return "JsonSmartHomeGroupIdentifiers{identifier=" + identifier + "}"; + } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentity.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeGroupIdentity.java similarity index 69% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentity.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeGroupIdentity.java index 6b305901f3dd3..a680df6ca97e5 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroupIdentity.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeGroupIdentity.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.dto.smarthome; import java.util.List; @@ -26,7 +26,17 @@ public class JsonSmartHomeGroupIdentity { public static class SmartHomeGroupIdentity { public @Nullable List groupIdentity; + + @Override + public String toString() { + return "SmartHomeGroupIdentity{groupIdentity=" + groupIdentity + "}"; + } } public @Nullable List groupIdentity; + + @Override + public String toString() { + return "JsonSmartHomeGroupIdentity{groupIdentity=" + groupIdentity + "}"; + } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeGroups.java similarity index 64% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeGroups.java index 9cc11e4a46d26..b9e8a0572737b 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeGroups.java @@ -10,13 +10,13 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.dto.smarthome; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroupIdentifiers.SmartHomeGroupIdentifier; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeGroupIdentifiers.SmartHomeGroupIdentifier; /** * @author Lukas Knoeller - Initial contribution @@ -39,19 +39,6 @@ public static class SmartHomeGroup implements SmartHomeBaseDevice { return value; } - @Override - public @Nullable String findEntityId() { - SmartHomeGroupIdentifier applianceGroupIdentifier = this.applianceGroupIdentifier; - if (applianceGroupIdentifier == null) { - return null; - } - String value = applianceGroupIdentifier.value; - if (value == null) { - return null; - } - return value; - } - @Override public boolean isGroup() { return true; @@ -64,10 +51,15 @@ public boolean isGroup() { @Override public String toString() { - return "SmartHomeGroup{" + "applianceGroupName='" + applianceGroupName + '\'' + ", isSpace=" + isSpace - + ", space=" + space + ", applianceGroupIdentifier=" + applianceGroupIdentifier + '}'; + return "SmartHomeGroup{applianceGroupName='" + applianceGroupName + "', isSpace=" + isSpace + ", space=" + + space + ", applianceGroupIdentifier=" + applianceGroupIdentifier + "}"; } } public @Nullable List groups; + + @Override + public String toString() { + return "JsonSmartHomeGroups{groups=" + groups + "}"; + } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeTags.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeTags.java similarity index 80% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeTags.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeTags.java index b215df588e2c7..a1f17f4f96e02 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeTags.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/JsonSmartHomeTags.java @@ -10,11 +10,11 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.dto.smarthome; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroupIdentity.SmartHomeGroupIdentity; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeGroupIdentity.SmartHomeGroupIdentity; /** * @author Lukas Knoeller - Initial contribution diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/SmartHomeBaseDevice.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/SmartHomeBaseDevice.java similarity index 87% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/SmartHomeBaseDevice.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/SmartHomeBaseDevice.java index 0a73f7a9f37b6..ada1a3650ce88 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/SmartHomeBaseDevice.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/dto/smarthome/SmartHomeBaseDevice.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.dto.smarthome; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -25,8 +25,5 @@ public interface SmartHomeBaseDevice { @Nullable String findId(); - @Nullable - String findEntityId(); - boolean isGroup(); } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java index 6246b9d3650e5..124d778a8af5a 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java @@ -12,20 +12,28 @@ */ package org.openhab.binding.amazonechocontrol.internal.handler; -import java.io.IOException; -import java.net.URISyntaxException; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.CHANNEL_REFRESH_ACTIVITY; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.CHANNEL_SEND_MESSAGE; +import static org.openhab.binding.amazonechocontrol.internal.dto.TOMapper.map; +import static org.openhab.binding.amazonechocontrol.internal.push.PushConnection.State.CLOSED; +import static org.openhab.binding.amazonechocontrol.internal.push.PushConnection.State.CONNECTED; +import static org.openhab.binding.amazonechocontrol.internal.util.Util.findIn; + import java.net.URLEncoder; -import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; import java.time.ZonedDateTime; +import java.time.chrono.ChronoZonedDateTime; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledFuture; @@ -34,36 +42,41 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http2.client.HTTP2Client; import org.openhab.binding.amazonechocontrol.internal.AccountHandlerConfig; -import org.openhab.binding.amazonechocontrol.internal.AccountServlet; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlCommandDescriptionProvider; import org.openhab.binding.amazonechocontrol.internal.ConnectionException; -import org.openhab.binding.amazonechocontrol.internal.HttpException; -import org.openhab.binding.amazonechocontrol.internal.IWebSocketCommandHandler; -import org.openhab.binding.amazonechocontrol.internal.WebSocketConnection; -import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandler; -import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandlerSendMessage; -import org.openhab.binding.amazonechocontrol.internal.channelhandler.IAmazonThingHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushActivity.Key; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushDevice.DopplerId; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPushCommand; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonWakeWords.WakeWord; -import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; +import org.openhab.binding.amazonechocontrol.internal.discovery.SmartHomeDevicesDiscovery; +import org.openhab.binding.amazonechocontrol.internal.dto.AscendingAlarmModelTO; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceNotificationStateTO; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceTO; +import org.openhab.binding.amazonechocontrol.internal.dto.DoNotDisturbDeviceStatusTO; +import org.openhab.binding.amazonechocontrol.internal.dto.EnabledFeedTO; +import org.openhab.binding.amazonechocontrol.internal.dto.NotificationSoundTO; +import org.openhab.binding.amazonechocontrol.internal.dto.push.NotifyNowPlayingUpdatedTO; +import org.openhab.binding.amazonechocontrol.internal.dto.push.PushCommandTO; +import org.openhab.binding.amazonechocontrol.internal.dto.push.PushDeviceTO; +import org.openhab.binding.amazonechocontrol.internal.dto.push.PushDopplerIdTO; +import org.openhab.binding.amazonechocontrol.internal.dto.push.PushListItemChangeTO; +import org.openhab.binding.amazonechocontrol.internal.dto.request.SendConversationDTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.AccountTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.BluetoothStateTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.CustomerHistoryRecordTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.ListItemTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.MusicProviderTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.WakeWordTO; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeGroups; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.SmartHomeBaseDevice; +import org.openhab.binding.amazonechocontrol.internal.push.PushConnection; +import org.openhab.binding.amazonechocontrol.internal.smarthome.JsonNetworkDetails; import org.openhab.binding.amazonechocontrol.internal.smarthome.SmartHomeDeviceStateGroupUpdateCalculator; +import org.openhab.binding.amazonechocontrol.internal.types.Notification; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; import org.openhab.core.storage.Storage; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; @@ -73,15 +86,15 @@ import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; -import org.openhab.core.types.RefreshType; -import org.openhab.core.types.State; -import org.osgi.service.http.HttpService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; /** @@ -90,137 +103,138 @@ * @author Michael Geramb - Initial Contribution */ @NonNullByDefault -public class AccountHandler extends BaseBridgeHandler implements IWebSocketCommandHandler, IAmazonThingHandler { +public class AccountHandler extends BaseBridgeHandler implements PushConnection.Listener { + private static final int CHECK_DATA_INTERVAL = 3600; // in seconds (always refresh every hour) + private static final int CHECK_LOGIN_INTERVAL = 60; // in seconds (always check every minute) + private final Logger logger = LoggerFactory.getLogger(AccountHandler.class); - private final Storage stateStorage; - private @Nullable Connection connection; - private @Nullable WebSocketConnection webSocketConnection; + private final Storage sessionStorage; + private final AmazonEchoControlCommandDescriptionProvider commandDescriptionProvider; + private Connection connection; - private final Set echoHandlers = new CopyOnWriteArraySet<>(); + private final Map echoHandlers = new ConcurrentHashMap<>(); private final Set smartHomeDeviceHandlers = new CopyOnWriteArraySet<>(); private final Set flashBriefingProfileHandlers = new CopyOnWriteArraySet<>(); + private final Set deviceSerialNumbers = new CopyOnWriteArraySet<>(); private final Object synchronizeConnection = new Object(); - private Map jsonSerialNumberDeviceMapping = new HashMap<>(); + private Map serialNumberDeviceMapping = new HashMap<>(); private Map jsonIdSmartHomeDeviceMapping = new HashMap<>(); private @Nullable ScheduledFuture checkDataJob; - private @Nullable ScheduledFuture checkLoginJob; private @Nullable ScheduledFuture updateSmartHomeStateJob; - private @Nullable ScheduledFuture refreshAfterCommandJob; + private @Nullable ScheduledFuture refreshActivityJob; private @Nullable ScheduledFuture refreshSmartHomeAfterCommandJob; private final Object synchronizeSmartHomeJobScheduler = new Object(); - private @Nullable ScheduledFuture forceCheckDataJob; - private String currentFlashBriefingJson = ""; - private final HttpService httpService; - private @Nullable AccountServlet accountServlet; + + private List currentFlashBriefings = List.of(); private final Gson gson; - private int checkDataCounter; + private int lastMessageId = 1000; + private long nextDataRefresh = 0; + private long nextLoginCheck = 0; + private long nextRefreshNotifications = 0; + private final LinkedBlockingQueue requestedDeviceUpdates = new LinkedBlockingQueue<>(); private @Nullable SmartHomeDeviceStateGroupUpdateCalculator smartHomeDeviceStateGroupUpdateCalculator; - private List channelHandlers = new ArrayList<>(); private AccountHandlerConfig handlerConfig = new AccountHandlerConfig(); + private final PushConnection pushConnection; + private boolean disposing = false; + private @Nullable AccountTO accountInformation; - public AccountHandler(Bridge bridge, HttpService httpService, Storage stateStorage, Gson gson) { + public AccountHandler(Bridge bridge, Storage stateStorage, Gson gson, HttpClient httpClient, + HTTP2Client http2Client, AmazonEchoControlCommandDescriptionProvider commandDescriptionProvider) { super(bridge); this.gson = gson; - this.httpService = httpService; - this.stateStorage = stateStorage; - channelHandlers.add(new ChannelHandlerSendMessage(this, this.gson)); + this.sessionStorage = stateStorage; + this.pushConnection = new PushConnection(http2Client, gson, this, scheduler); + this.commandDescriptionProvider = commandDescriptionProvider; + this.connection = new Connection(null, gson, httpClient); } @Override public void initialize() { + disposing = false; handlerConfig = getConfig().as(AccountHandlerConfig.class); - synchronized (synchronizeConnection) { - Connection connection = this.connection; - if (connection == null) { - this.connection = new Connection(null, gson); - } - } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login"); - if (accountServlet == null) { - try { - accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, gson); - } catch (IllegalStateException e) { - logger.warn("Failed to create account servlet", e); - } - } + nextDataRefresh = 0; + nextLoginCheck = 0; - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login"); + checkDataJob = scheduler.scheduleWithFixedDelay(this::checkLoginAndData, 0, 1, TimeUnit.SECONDS); - checkLoginJob = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS); - checkDataJob = scheduler.scheduleWithFixedDelay(this::checkData, 4, 60, TimeUnit.SECONDS); + int pollingIntervalAlexa = Math.min(handlerConfig.pollingIntervalSmartHomeAlexa, 10); + int pollingIntervalSkills = Math.min(handlerConfig.pollingIntervalSmartSkills, 60); - int pollingIntervalAlexa = handlerConfig.pollingIntervalSmartHomeAlexa; - if (pollingIntervalAlexa < 10) { - pollingIntervalAlexa = 10; - } - int pollingIntervalSkills = handlerConfig.pollingIntervalSmartSkills; - if (pollingIntervalSkills < 60) { - pollingIntervalSkills = 60; - } smartHomeDeviceStateGroupUpdateCalculator = new SmartHomeDeviceStateGroupUpdateCalculator(pollingIntervalAlexa, pollingIntervalSkills); updateSmartHomeStateJob = scheduler.scheduleWithFixedDelay(() -> updateSmartHomeState(null), 20, 10, TimeUnit.SECONDS); } - @Override - public void updateChannelState(String channelId, State state) { - updateState(channelId, state); - } - @Override public void handleCommand(ChannelUID channelUID, Command command) { try { logger.trace("Command '{}' received for channel '{}'", command, channelUID); - Connection connection = this.connection; - if (connection == null) { + if (!connection.isLoggedIn()) { + logger.info("Can't handle commands when account is logged out."); return; } - String channelId = channelUID.getId(); - for (ChannelHandler channelHandler : channelHandlers) { - if (channelHandler.tryHandleCommand(new Device(), connection, channelId, command)) { + if (channelId.equals(CHANNEL_REFRESH_ACTIVITY) && command instanceof OnOffType) { + for (CustomerHistoryRecordTO record : getCustomerActivity(null)) { + String[] keyParts = record.recordKey.split("#"); + String serialNumber = keyParts[keyParts.length - 1]; + EchoHandler echoHandler = echoHandlers.get(serialNumber); + if (echoHandler != null) { + echoHandler.handlePushActivity(record); + } + } + } else if (channelId.equals(CHANNEL_SEND_MESSAGE) && command instanceof StringType) { + String commandValue = command.toFullString(); + String baseUrl = "https://alexa-comms-mobile-service." + connection.getRetailDomain(); + + AccountTO currentAccount = this.accountInformation; + if (currentAccount == null) { + String accountResult = connection.getRequestBuilder().get(baseUrl + "/accounts") + .syncSend(String.class); + List accounts = gson.fromJson(accountResult, AccountTO.LIST_TYPE_TOKEN); + currentAccount = accounts.stream().filter(a -> a.signedInUser).findFirst().orElse(null); + this.accountInformation = currentAccount; + } + + if (currentAccount == null || currentAccount.commsId == null) { return; } + + SendConversationDTO conversation = new SendConversationDTO(); + conversation.conversationId = "amzn1.comms.messaging.id.conversationV2~31e6fe8f-8b0c-4e84-a1e4-80030a09009b"; + conversation.clientMessageId = java.util.UUID.randomUUID().toString(); + conversation.messageId = lastMessageId++; + conversation.sender = currentAccount.commsId; + conversation.time = LocalDateTime.now().toString(); + conversation.payload.put("text", commandValue); + + String sendUrl = baseUrl + "/users/" + currentAccount.commsId + "/conversations/" + + currentAccount.commsId + "/messages"; + connection.getRequestBuilder().post(sendUrl).withContent(List.of(conversation)).syncSend(); } - if (command instanceof RefreshType) { - refreshData(); - } - } catch (IOException | URISyntaxException | InterruptedException e) { + } catch (ConnectionException e) { logger.info("handleCommand fails", e); } } - @Override - public void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title, - @Nullable Integer volume) throws IOException, URISyntaxException { - EchoHandler echoHandler = findEchoHandlerBySerialNumber(device.serialNumber); - if (echoHandler != null) { - echoHandler.startAnnouncement(device, speak, bodyText, title, volume); - } - } - - public List getFlashBriefingProfileHandlers() { - return new ArrayList<>(flashBriefingProfileHandlers); + public Set getFlashBriefingProfileHandlers() { + return Set.copyOf(flashBriefingProfileHandlers); } - public List getLastKnownDevices() { - return new ArrayList<>(jsonSerialNumberDeviceMapping.values()); + public List getLastKnownDevices() { + return List.copyOf(serialNumberDeviceMapping.values()); } public List getLastKnownSmartHomeDevices() { - return new ArrayList<>(jsonIdSmartHomeDeviceMapping.values()); - } - - public void addEchoHandler(EchoHandler echoHandler) { - if (echoHandlers.add(echoHandler)) { - forceCheckData(); - } + return List.copyOf(jsonIdSmartHomeDeviceMapping.values()); } public void addSmartHomeDeviceHandler(SmartHomeDeviceHandler smartHomeDeviceHandler) { @@ -230,47 +244,15 @@ public void addSmartHomeDeviceHandler(SmartHomeDeviceHandler smartHomeDeviceHand } public void forceCheckData() { - if (forceCheckDataJob == null) { - forceCheckDataJob = scheduler.schedule(this::checkData, 1000, TimeUnit.MILLISECONDS); - } - } - - public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) { - EchoHandler echoHandler = findEchoHandlerBySerialNumber(deviceSerialNumber); - if (echoHandler != null) { - return echoHandler.getThing(); - } - return null; + nextDataRefresh = 0; } - public @Nullable EchoHandler findEchoHandlerBySerialNumber(@Nullable String deviceSerialNumber) { - for (EchoHandler echoHandler : echoHandlers) { - if (deviceSerialNumber != null && deviceSerialNumber.equals(echoHandler.findSerialNumber())) { - return echoHandler; - } - } - return null; - } - - public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) { - flashBriefingProfileHandlers.add(flashBriefingProfileHandler); - Connection connection = this.connection; - if (connection != null && connection.getIsLoggedIn()) { - if (currentFlashBriefingJson.isEmpty()) { - updateFlashBriefingProfiles(connection); - } - flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson); + public @Nullable Thing getThingBySerialNumber(@Nullable String deviceSerialNumber) { + if (deviceSerialNumber == null) { + return null; } - } - - private void scheduleUpdate() { - checkDataCounter = 999; - } - - @Override - public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) { - super.childHandlerInitialized(childHandler, childThing); - scheduleUpdate(); + EchoHandler echoHandler = echoHandlers.get(deviceSerialNumber); + return echoHandler == null ? null : echoHandler.getThing(); } @Override @@ -279,32 +261,41 @@ public void handleRemoval() { super.handleRemoval(); } + @Override + public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) { + if (childHandler instanceof EchoHandler echoHandler) { + echoHandlers.put(echoHandler.getSerialNumber(), echoHandler); + forceCheckData(); + return; + } else if (childHandler instanceof FlashBriefingProfileHandler flashBriefingProfileHandler) { + flashBriefingProfileHandlers.add(flashBriefingProfileHandler); + if (currentFlashBriefings.isEmpty()) { + currentFlashBriefings = updateFlashBriefingProfiles(); + flashBriefingProfileHandler.updateAndCheck(currentFlashBriefings); + } + // set flash-briefing description on echo handlers + commandDescriptionProvider.setEchoHandlerStartCommands(echoHandlers.values(), flashBriefingProfileHandlers); + } + nextDataRefresh = Math.min(nextDataRefresh, System.currentTimeMillis() + 60L * 1000); // refresh latest within + // one minute + } + @Override public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) { - // check for echo handler if (childHandler instanceof EchoHandler) { - echoHandlers.remove(childHandler); - } - // check for flash briefing profile handler - if (childHandler instanceof FlashBriefingProfileHandler) { + echoHandlers.values().remove(childHandler); + } else if (childHandler instanceof FlashBriefingProfileHandler) { flashBriefingProfileHandlers.remove(childHandler); - } - // check for flash briefing profile handler - if (childHandler instanceof SmartHomeDeviceHandler) { + commandDescriptionProvider.setEchoHandlerStartCommands(echoHandlers.values(), flashBriefingProfileHandlers); + } else if (childHandler instanceof SmartHomeDeviceHandler) { smartHomeDeviceHandlers.remove(childHandler); } - super.childHandlerDisposed(childHandler, childThing); } @Override public void dispose() { - AccountServlet accountServlet = this.accountServlet; - if (accountServlet != null) { - accountServlet.dispose(); - } - this.accountServlet = null; + disposing = true; cleanup(); - super.dispose(); } private void cleanup() { @@ -319,32 +310,13 @@ private void cleanup() { refreshJob.cancel(true); this.checkDataJob = null; } - ScheduledFuture refreshLogin = this.checkLoginJob; - if (refreshLogin != null) { - refreshLogin.cancel(true); - this.checkLoginJob = null; - } - ScheduledFuture foceCheckDataJob = this.forceCheckDataJob; - if (foceCheckDataJob != null) { - foceCheckDataJob.cancel(true); - this.forceCheckDataJob = null; - } - ScheduledFuture refreshAfterCommandJob = this.refreshAfterCommandJob; - if (refreshAfterCommandJob != null) { - refreshAfterCommandJob.cancel(true); - this.refreshAfterCommandJob = null; - } ScheduledFuture refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob; if (refreshSmartHomeAfterCommandJob != null) { refreshSmartHomeAfterCommandJob.cancel(true); this.refreshSmartHomeAfterCommandJob = null; } - Connection connection = this.connection; - if (connection != null) { - connection.logout(); - this.connection = null; - } - closeWebSocketConnection(); + pushConnection.close(); + connection.logout(false); } private void checkLogin() { @@ -353,441 +325,374 @@ private void checkLogin() { logger.debug("check login {}", uid.getAsString()); synchronized (synchronizeConnection) { - Connection currentConnection = this.connection; - if (currentConnection == null) { - return; - } - try { - if (currentConnection.getIsLoggedIn()) { - if (currentConnection.checkRenewSession()) { - setConnection(currentConnection); + if (connection.isLoggedIn()) { + if (connection.renewTokens()) { + storeSession(); } } else { // read session data from property - String sessionStore = this.stateStorage.get("sessionStorage"); + String sessionStore = sessionStorage.get("sessionStorage"); - // try use the session data - if (currentConnection.tryRestoreLogin(sessionStore, null)) { - setConnection(currentConnection); + // try to use the session data + if (connection.restoreLogin(sessionStore, null)) { + storeSession(); + nextDataRefresh = 0; } } - if (!currentConnection.getIsLoggedIn()) { + if (!connection.isLoggedIn()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, - "Please login in through web site: http(s)://:/amazonechocontrol/" - + URLEncoder.encode(uid.getId(), "UTF8")); + "Please login in through servlet: http(s)://:/amazonechocontrol/" + + URLEncoder.encode(uid.getId(), StandardCharsets.UTF_8)); + if (pushConnection.getState() != CLOSED) { + // close push connection if we are not logged in + pushConnection.close(); + } + } else { + updateStatus(ThingStatus.ONLINE); + if (pushConnection.getState() == CLOSED) { + pushConnection.open(connection.getRetailDomain(), connection.getAccessToken()); + } else if (pushConnection.getState() == CONNECTED) { + // if the push connection is already logged in, check if it is alive + pushConnection.sendPing(); + } } } catch (ConnectionException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); - } catch (HttpException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - } catch (UnknownHostException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Unknown host name '" + e.getMessage() + "'. Maybe your internet connection is offline"); - } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); - } catch (URISyntaxException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } } - } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail. + } catch (RuntimeException e) { // this handler can be removed later, if we know that nothing else can fail. logger.error("check login fails with unexpected error", e); } } - // used to set a valid connection from the web proxy login - public void setConnection(@Nullable Connection connection) { - this.connection = connection; - if (connection != null) { - String serializedStorage = connection.serializeLoginData(); - this.stateStorage.put("sessionStorage", serializedStorage); - } else { - this.stateStorage.put("sessionStorage", null); - updateStatus(ThingStatus.OFFLINE); - } - closeWebSocketConnection(); - if (connection != null) { - updateDeviceList(); - updateSmartHomeDeviceList(false); - updateFlashBriefingHandlers(); - updateStatus(ThingStatus.ONLINE); - scheduleUpdate(); - checkData(); - } + public void resetConnection(boolean newDevice) { + pushConnection.close(); + connection.logout(newDevice); + sessionStorage.put("sessionStorage", null); + + updateStatus(ThingStatus.OFFLINE); } - void closeWebSocketConnection() { - WebSocketConnection webSocketConnection = this.webSocketConnection; - this.webSocketConnection = null; - if (webSocketConnection != null) { - webSocketConnection.close(); - } + // used to set a valid connection from the web proxy login + public void setConnection(Connection newConnection) { + pushConnection.close(); + connection = newConnection; + storeSession(); + + // force data check + nextLoginCheck = 0; + nextDataRefresh = 0; } - private boolean checkWebSocketConnection() { - WebSocketConnection webSocketConnection = this.webSocketConnection; - if (webSocketConnection == null || webSocketConnection.isClosed()) { - Connection connection = this.connection; - if (connection != null && connection.getIsLoggedIn()) { - try { - this.webSocketConnection = new WebSocketConnection(connection.getAmazonSite(), - connection.getSessionCookies(), this); - } catch (IOException e) { - logger.warn("Web socket connection starting failed", e); - } - } - return false; - } - return true; + private void storeSession() { + String serializedStorage = connection.getLoginData().serializeLoginData(); + sessionStorage.put("sessionStorage", serializedStorage); } - private void checkData() { + private void checkLoginAndData() { + long now = System.currentTimeMillis(); synchronized (synchronizeConnection) { try { - Connection connection = this.connection; - if (connection != null && connection.getIsLoggedIn()) { - checkDataCounter++; - if (checkDataCounter > 60 || forceCheckDataJob != null) { - checkDataCounter = 0; - forceCheckDataJob = null; - } - if (!checkWebSocketConnection() || checkDataCounter == 0) { + if (now > nextLoginCheck) { + nextLoginCheck = now + CHECK_LOGIN_INTERVAL * 1000; + checkLogin(); + } + if (connection.isLoggedIn()) { + if (now > nextDataRefresh) { + nextDataRefresh = now + CHECK_DATA_INTERVAL * 1000; refreshData(); } + if (now > nextRefreshNotifications) { + refreshNotifications(); + } } - logger.debug("checkData {} finished", getThing().getUID().getAsString()); - } catch (HttpException | JsonSyntaxException | ConnectionException e) { - logger.debug("checkData fails", e); - } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail. - logger.error("checkData fails with unexpected error", e); + } catch (RuntimeException e) { // this handler can be removed later, if we know that nothing else can fail. + logger.warn("checkData fails with unexpected error", e); } } } - private void refreshNotifications(@Nullable JsonCommandPayloadPushNotificationChange pushPayload) { - Connection currentConnection = this.connection; - if (currentConnection == null) { - return; - } - if (!currentConnection.getIsLoggedIn()) { + private void refreshNotifications() { + if (!connection.isLoggedIn()) { return; } - ZonedDateTime timeStamp = ZonedDateTime.now(); - try { - List notifications = currentConnection.notifications(); - ZonedDateTime timeStampNow = ZonedDateTime.now(); - echoHandlers.forEach(echoHandler -> echoHandler.updateNotifications(timeStamp, timeStampNow, pushPayload, - notifications)); - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.debug("refreshNotifications failed", e); - return; + ZonedDateTime requestTime = ZonedDateTime.now(); + List notifications = connection.getNotifications().stream() + .map(n -> map(n, requestTime, ZonedDateTime.now())).filter(Objects::nonNull) + .map(Objects::requireNonNull).toList(); + echoHandlers.values().forEach(echoHandler -> echoHandler.updateNotifications(notifications)); + ZonedDateTime first = notifications.stream().map(Notification::nextAlarmTime) + .min(ChronoZonedDateTime::compareTo).orElse(null); + if (first != null) { + nextRefreshNotifications = first.toEpochSecond() * 1000; + } else { + nextRefreshNotifications = Long.MAX_VALUE; } } private void refreshData() { - synchronized (synchronizeConnection) { - try { - logger.debug("refreshing data {}", getThing().getUID().getAsString()); - - // check if logged in - Connection currentConnection = null; - currentConnection = connection; - if (currentConnection != null) { - if (!currentConnection.getIsLoggedIn()) { - return; - } - } - if (currentConnection == null) { + try { + logger.debug("refreshing data {}", getThing().getUID().getAsString()); + + // check if logged in + if (!connection.isLoggedIn()) { + return; + } + + // get all devices registered in the account + updateDeviceList(); + updateSmartHomeDeviceList(false); + updateFlashBriefingHandlers(); + + List deviceNotificationStates = connection.getDeviceNotificationStates(); + List ascendingAlarmModels = connection.getAscendingAlarms(); + List doNotDisturbDeviceStatuses = connection.getDoNotDisturbs(); + List bluetoothStates = connection.getBluetoothConnectionStates(); + List musicProviders = connection.getMusicProviders(); + + // forward device information to echo handler + echoHandlers.forEach((serialNumber, echoHandler) -> { + DeviceTO device = serialNumberDeviceMapping.get(serialNumber); + if (device == null) { return; } - // get all devices registered in the account - updateDeviceList(); - updateSmartHomeDeviceList(false); - updateFlashBriefingHandlers(); - - List deviceNotificationStates = List.of(); - List ascendingAlarmModels = List.of(); - JsonBluetoothStates states = null; - List musicProviders = null; - if (currentConnection.getIsLoggedIn()) { - // update notification states - deviceNotificationStates = currentConnection.getDeviceNotificationStates(); - - // update ascending alarm - ascendingAlarmModels = currentConnection.getAscendingAlarm(); - - // update bluetooth states - states = currentConnection.getBluetoothConnectionStates(); - - // update music providers - if (currentConnection.getIsLoggedIn()) { - try { - musicProviders = currentConnection.getMusicProviders(); - } catch (HttpException | JsonSyntaxException | ConnectionException e) { - logger.debug("Update music provider failed", e); - } - } - } - // forward device information to echo handler - for (EchoHandler child : echoHandlers) { - Device device = findDeviceJson(child.findSerialNumber()); - - List notificationSounds = List.of(); - JsonPlaylists playlists = null; - if (device != null && currentConnection.getIsLoggedIn()) { - // update notification sounds - try { - notificationSounds = currentConnection.getNotificationSounds(device); - } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) { - logger.debug("Update notification sounds failed", e); - } - // update playlists - try { - playlists = currentConnection.getPlaylists(device); - } catch (IOException | HttpException | JsonSyntaxException | ConnectionException e) { - logger.debug("Update playlist failed", e); - } - } + // update alarm sounds + List notificationSounds = connection.getNotificationSounds(device); + commandDescriptionProvider.setEchoHandlerAlarmSounds(echoHandler, notificationSounds); - BluetoothState state = null; - if (states != null) { - state = states.findStateByDevice(device); - } - DeviceNotificationState deviceNotificationState = null; - AscendingAlarmModel ascendingAlarmModel = null; - if (device != null) { - final String serialNumber = device.serialNumber; - if (serialNumber != null) { - ascendingAlarmModel = ascendingAlarmModels.stream() - .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst() - .orElse(null); - deviceNotificationState = deviceNotificationStates.stream() - .filter(current -> serialNumber.equals(current.deviceSerialNumber)).findFirst() - .orElse(null); - } - } - child.updateState(this, device, state, deviceNotificationState, ascendingAlarmModel, playlists, - notificationSounds, musicProviders); - } + BluetoothStateTO bluetoothState = findIn(bluetoothStates, k -> k.deviceSerialNumber, + device.serialNumber).orElse(null); + AscendingAlarmModelTO ascendingAlarmModel = findIn(ascendingAlarmModels, a -> a.deviceSerialNumber, + device.serialNumber).orElse(null); + DeviceNotificationStateTO deviceNotificationState = findIn(deviceNotificationStates, + a -> a.deviceSerialNumber, device.serialNumber).orElse(null); + DoNotDisturbDeviceStatusTO doNotDisturbDeviceStatus = findIn(doNotDisturbDeviceStatuses, + a -> a.deviceSerialNumber, device.serialNumber).orElse(null); - // refresh notifications - refreshNotifications(null); + echoHandler.updateState(device, bluetoothState, deviceNotificationState, ascendingAlarmModel, + doNotDisturbDeviceStatus, musicProviders); + }); - // update account state - updateStatus(ThingStatus.ONLINE); + // refresh notifications + refreshNotifications(); - logger.debug("refresh data {} finished", getThing().getUID().getAsString()); - } catch (HttpException | JsonSyntaxException | ConnectionException e) { - logger.debug("refresh data fails", e); - } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail. - logger.error("refresh data fails with unexpected error", e); - } + // update account state + updateStatus(ThingStatus.ONLINE); + + logger.debug("refresh data {} finished", getThing().getUID().getAsString()); + } catch (JsonSyntaxException e) { + logger.debug("refresh data fails", e); + } catch (RuntimeException e) { // this handler can be removed later, if we know that nothing else can fail. + logger.error("refresh data fails with unexpected error", e); } } - public @Nullable Device findDeviceJson(@Nullable String serialNumber) { + public @Nullable DeviceTO findDevice(@Nullable String serialNumber) { if (serialNumber == null || serialNumber.isEmpty()) { return null; } - return this.jsonSerialNumberDeviceMapping.get(serialNumber); + return serialNumberDeviceMapping.get(serialNumber); } - public @Nullable Device findDeviceJsonBySerialOrName(@Nullable String serialOrName) { + public @Nullable DeviceTO findDeviceBySerialOrName(@Nullable String serialOrName) { if (serialOrName == null || serialOrName.isEmpty()) { return null; } - return this.jsonSerialNumberDeviceMapping.values().stream().filter( + return this.serialNumberDeviceMapping.values().stream().filter( d -> serialOrName.equalsIgnoreCase(d.serialNumber) || serialOrName.equalsIgnoreCase(d.accountName)) .findFirst().orElse(null); } - public List updateDeviceList() { - Connection currentConnection = connection; - if (currentConnection == null) { - return new ArrayList<>(); + public List updateDeviceList() { + if (!connection.isLoggedIn()) { + return List.of(); } - List devices = null; try { - if (currentConnection.getIsLoggedIn()) { - devices = currentConnection.getDeviceList(); - } - } catch (IOException | URISyntaxException | InterruptedException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); - } - if (devices != null) { - // create new device map - jsonSerialNumberDeviceMapping = devices.stream().filter(device -> device.serialNumber != null) - .collect(Collectors.toMap(d -> Objects.requireNonNull(d.serialNumber), d -> d)); - } + List devices = connection.getDeviceList(); + List wakeWords = connection.getWakeWords(); - List wakeWords = currentConnection.getWakeWords(); - // update handlers - for (EchoHandler echoHandler : echoHandlers) { - String serialNumber = echoHandler.findSerialNumber(); - String deviceWakeWord = wakeWords.stream() - .filter(wakeWord -> serialNumber.equals(wakeWord.deviceSerialNumber)).findFirst() - .map(wakeWord -> wakeWord.wakeWord).orElse(null); - echoHandler.setDeviceAndUpdateThingState(this, findDeviceJson(serialNumber), deviceWakeWord); - } + // create new device map + serialNumberDeviceMapping = devices.stream().collect(Collectors.toMap(d -> d.serialNumber, d -> d)); + // notify flash briefing profile handlers of changed device list + commandDescriptionProvider.setFlashBriefingTargets(flashBriefingProfileHandlers, + serialNumberDeviceMapping.values()); + commandDescriptionProvider.setEchoHandlerStartCommands(echoHandlers.values(), flashBriefingProfileHandlers); + + echoHandlers.forEach((serialNumber, echoHandler) -> { + DeviceTO device = serialNumberDeviceMapping.get(serialNumber); + if (device != null) { + String deviceWakeWord = findIn(wakeWords, w -> w.deviceSerialNumber, serialNumber) + .map(wakeWord -> wakeWord.wakeWord).orElse(null); + echoHandler.setDeviceAndUpdateThingStatus(device, deviceWakeWord); + } + }); - if (devices != null) { return devices; + } catch (ConnectionException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } + return List.of(); } - public void setEnabledFlashBriefingsJson(String flashBriefingJson) { - Connection currentConnection = connection; - JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class); - if (currentConnection != null && feeds != null) { + public void setEnabledFlashBriefing(List flashBriefingConfiguration) { + if (connection.isLoggedIn()) { try { - currentConnection.setEnabledFlashBriefings(Arrays.asList(feeds)); - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.warn("Set flashbriefing profile failed", e); + connection.setEnabledFlashBriefings(flashBriefingConfiguration); + } catch (ConnectionException e) { + logger.warn("Set flash-briefing profile failed", e); } } updateFlashBriefingHandlers(); } - public String getNewCurrentFlashbriefingConfiguration() { - return updateFlashBriefingHandlers(); - } - - public String updateFlashBriefingHandlers() { - Connection currentConnection = connection; - if (currentConnection != null) { - return updateFlashBriefingHandlers(currentConnection); - } - return ""; - } - - private String updateFlashBriefingHandlers(Connection currentConnection) { - if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) { - updateFlashBriefingProfiles(currentConnection); - } - boolean flashBriefingProfileFound = false; - for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) { - flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson); - } - if (flashBriefingProfileFound) { - return ""; + public List updateFlashBriefingHandlers() { + List currentConfiguration = getEnabledFlashBriefings(); + if (!currentConfiguration.isEmpty()) { + boolean match = false; + for (FlashBriefingProfileHandler handler : flashBriefingProfileHandlers) { + match |= handler.updateAndCheck(currentConfiguration); + } + if (!match) { + // there is no handler associated with the current configuration + return currentConfiguration; + } } - return this.currentFlashBriefingJson; + return List.of(); } - public @Nullable Connection findConnection() { + public Connection getConnection() { return this.connection; } - public String getEnabledFlashBriefingsJson() { - Connection currentConnection = this.connection; - if (currentConnection == null) { - return ""; + public List getEnabledFlashBriefings() { + if (!currentFlashBriefings.isEmpty()) { + return currentFlashBriefings; } - updateFlashBriefingProfiles(currentConnection); - return this.currentFlashBriefingJson; + currentFlashBriefings = updateFlashBriefingProfiles(); + return currentFlashBriefings; } - private void updateFlashBriefingProfiles(Connection currentConnection) { - try { - // Make a copy and remove changeable parts - JsonFeed[] forSerializer = currentConnection.getEnabledFlashBriefings().stream() - .map(source -> new JsonFeed(source.feedId, source.skillId)).toArray(JsonFeed[]::new); - this.currentFlashBriefingJson = gson.toJson(forSerializer); - } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException - | InterruptedException e) { - logger.warn("get flash briefing profiles fails", e); - } + private List updateFlashBriefingProfiles() { + return connection.isLoggedIn() ? connection.getEnabledFlashBriefings().stream().map(this::copyFeed).toList() + : List.of(); } - @Override - public void webSocketCommandReceived(JsonPushCommand pushCommand) { - try { - handleWebsocketCommand(pushCommand); - } catch (Exception e) { - // should never happen, but if the exception is going out of this function, the binding stop working. - logger.warn("handling of websockets fails", e); - } + private EnabledFeedTO copyFeed(EnabledFeedTO feed) { + EnabledFeedTO newFeed = new EnabledFeedTO(); + newFeed.feedId = feed.feedId; + newFeed.skillId = feed.skillId; + return newFeed; } - void handleWebsocketCommand(JsonPushCommand pushCommand) { + @Override + public void onPushCommandReceived(PushCommandTO pushCommand) { + logger.debug("Processing {}", pushCommand); + String payload = pushCommand.payload; String command = pushCommand.command; - if (command != null) { - ScheduledFuture refreshDataDelayed = this.refreshAfterCommandJob; - switch (command) { - case "PUSH_ACTIVITY": - handlePushActivity(pushCommand.payload); - break; - case "PUSH_DOPPLER_CONNECTION_CHANGE": - case "PUSH_BLUETOOTH_STATE_CHANGE": - if (refreshDataDelayed != null) { - refreshDataDelayed.cancel(false); - } - this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700, - TimeUnit.MILLISECONDS); - break; - case "PUSH_NOTIFICATION_CHANGE": - JsonCommandPayloadPushNotificationChange pushPayload = gson.fromJson(pushCommand.payload, - JsonCommandPayloadPushNotificationChange.class); - refreshNotifications(pushPayload); - break; - default: - String payload = pushCommand.payload; - if (payload != null && payload.startsWith("{") && payload.endsWith("}")) { - JsonCommandPayloadPushDevice devicePayload = Objects - .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushDevice.class)); - DopplerId dopplerId = devicePayload.dopplerId; - if (dopplerId != null) { - handlePushDeviceCommand(dopplerId, command, payload); + switch (command) { + case "PUSH_ACTIVITY": + // currently unused, seems to be removed, log a warning if it re-appears + logger.warn("Activity detected: {}", pushCommand); + break; + case "PUSH_DOPPLER_CONNECTION_CHANGE": + case "PUSH_BLUETOOTH_STATE_CHANGE": + long now = System.currentTimeMillis(); + if (nextDataRefresh > now + 1000) { + nextDataRefresh = now + 1000; + } + break; + + case "PUSH_NOTIFICATION_CHANGE": + refreshNotifications(); + break; + case "PUSH_AUDIO_PLAYER_STATE": + case "PUSH_MEDIA_QUEUE_CHANGE": + case "PUSH_MEDIA_CHANGE": + case "PUSH_MEDIA_PROGRESS_CHANGE": + case "PUSH_VOLUME_CHANGE": + case "PUSH_CONTENT_FOCUS_CHANGE": + case "PUSH_EQUALIZER_STATE_CHANGE": + if (payload.startsWith("{") && payload.endsWith("}")) { + PushDeviceTO devicePayload = Objects.requireNonNull(gson.fromJson(payload, PushDeviceTO.class)); + PushDopplerIdTO dopplerId = devicePayload.dopplerId; + if (dopplerId != null) { + EchoHandler echoHandler = echoHandlers.get(dopplerId.deviceSerialNumber); + if (echoHandler == null) { + return; + } + echoHandler.handlePushCommand(command, payload); + if ("PUSH_EQUALIZER_STATE_CHANGE".equals(command) || "PUSH_VOLUME_CHANGE".equals(command)) { + deviceSerialNumbers.add(dopplerId.deviceSerialNumber); + ScheduledFuture refreshActivityJob = this.refreshActivityJob; + if (refreshActivityJob != null) { + refreshActivityJob.cancel(false); + } + this.refreshActivityJob = scheduler.schedule( + () -> handlePushActivity(deviceSerialNumbers, pushCommand.timeStamp), + handlerConfig.activityRequestDelay, TimeUnit.SECONDS); } } - break; + } + break; + case "NotifyNowPlayingUpdated": + NotifyNowPlayingUpdatedTO update = Objects + .requireNonNull(gson.fromJson(payload, NotifyNowPlayingUpdatedTO.class)); + echoHandlers.values().forEach(e -> e.handleNowPlayingUpdated(update.update.update.nowPlayingData)); + break; + case "NotifyMediaSessionsUpdated": + // we can't determine which session was updated, but it only makes sense for currently playing devices + // echoHandlers.forEach(e -> e.refreshAudioPlayerState(true)); + echoHandlers.values().forEach(EchoHandler::updateMediaSessions); + break; + case "PUSH_LIST_ITEM_CHANGE": + PushListItemChangeTO itemChange = Objects + .requireNonNull(gson.fromJson(payload, PushListItemChangeTO.class)); + List lists = connection.getNamedListItems(itemChange.listId); + // TODO: create channels + break; + default: + logger.warn("Detected unknown command from activity stream: {}", pushCommand); + } + } + + private List getCustomerActivity(@Nullable Long timestamp) { + if (!connection.isLoggedIn()) { + return List.of(); + } + long realTimestamp = Objects.requireNonNullElse(timestamp, System.currentTimeMillis()); + long startTimestamp = realTimestamp - 120000; + long endTimestamp = realTimestamp + 30000; + + return connection.getActivities(startTimestamp, endTimestamp); + } + + private void handlePushActivity(Set deviceSerialNumbers, @Nullable Long timestamp) { + List activityRecords = getCustomerActivity(timestamp); + + Iterator iterator = deviceSerialNumbers.iterator(); + while (iterator.hasNext()) { + try { + String deviceSerialNumber = iterator.next(); + EchoHandler echoHandler = echoHandlers.get(deviceSerialNumber); + if (echoHandler == null) { + logger.warn("Could not find thing handler for serialnumber {}", deviceSerialNumber); + return; + } + activityRecords.stream().filter(r -> r.recordKey.endsWith(deviceSerialNumber)) + .forEach(echoHandler::handlePushActivity); + } finally { + iterator.remove(); } } } - private void handlePushDeviceCommand(DopplerId dopplerId, String command, String payload) { - EchoHandler echoHandler = findEchoHandlerBySerialNumber(dopplerId.deviceSerialNumber); - if (echoHandler != null) { - echoHandler.handlePushCommand(command, payload); - } - } - - private void handlePushActivity(@Nullable String payload) { - if (payload == null) { - return; - } - JsonCommandPayloadPushActivity pushActivity = Objects - .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushActivity.class)); - - Key key = pushActivity.key; - if (key == null) { - return; - } - - Connection connection = this.connection; - if (connection == null || !connection.getIsLoggedIn()) { - return; - } - - String search = key.registeredUserId + "#" + key.entryId; - connection.getActivities(10, pushActivity.timestamp).stream().filter(activity -> search.equals(activity.id)) - .findFirst() - .ifPresent(currentActivity -> currentActivity.getSourceDeviceIds().stream() - .map(sourceDeviceId -> findEchoHandlerBySerialNumber(sourceDeviceId.serialNumber)) - .filter(Objects::nonNull).forEach(echoHandler -> Objects.requireNonNull(echoHandler) - .handlePushActivity(currentActivity))); - } - - void refreshAfterCommand() { - refreshData(); - } - - private @Nullable SmartHomeBaseDevice findSmartDeviceHomeJson(SmartHomeDeviceHandler handler) { + private @Nullable SmartHomeBaseDevice findSmartHomeDeviceJson(SmartHomeDeviceHandler handler) { String id = handler.getId(); if (!id.isEmpty()) { return jsonIdSmartHomeDeviceMapping.get(id); @@ -800,51 +705,76 @@ public int getSmartHomeDevicesDiscoveryMode() { } public List updateSmartHomeDeviceList(boolean forceUpdate) { - Connection currentConnection = connection; - if (currentConnection == null) { - return Collections.emptyList(); - } - - if (!forceUpdate && smartHomeDeviceHandlers.isEmpty() && getSmartHomeDevicesDiscoveryMode() == 0) { - return Collections.emptyList(); + if (!forceUpdate && smartHomeDeviceHandlers.isEmpty() && handlerConfig.discoverSmartHome == 0) { + return List.of(); } - List smartHomeDevices = null; try { - if (currentConnection.getIsLoggedIn()) { - smartHomeDevices = currentConnection.getSmarthomeDeviceList(); - } - } catch (IOException | URISyntaxException | InterruptedException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); - } - if (smartHomeDevices != null) { - // create new id map - Map newJsonIdSmartHomeDeviceMapping = new HashMap<>(); - for (Object smartHomeDevice : smartHomeDevices) { - if (smartHomeDevice instanceof SmartHomeBaseDevice smartHomeBaseDevice) { - String id = smartHomeBaseDevice.findId(); + if (connection.isLoggedIn()) { + JsonNetworkDetails networkDetails = connection.getRequestBuilder() + .get(connection.getAlexaServer() + "/api/phoenix").syncSend(JsonNetworkDetails.class); + Object jsonObject = gson.fromJson(networkDetails.networkDetail, Object.class); + List smartHomeDevices = new ArrayList<>(); + searchSmartHomeDevicesRecursive(jsonObject, smartHomeDevices); + + // create new id map + Map newJsonIdSmartHomeDeviceMapping = new HashMap<>(); + for (SmartHomeBaseDevice smartHomeDevice : smartHomeDevices) { + String id = smartHomeDevice.findId(); if (id != null) { - newJsonIdSmartHomeDeviceMapping.put(id, smartHomeBaseDevice); + newJsonIdSmartHomeDeviceMapping.put(id, smartHomeDevice); } } + jsonIdSmartHomeDeviceMapping = newJsonIdSmartHomeDeviceMapping; + + // update handlers + smartHomeDeviceHandlers + .forEach(child -> child.setDeviceAndUpdateThingState(this, findSmartHomeDeviceJson(child))); + return smartHomeDevices; } - jsonIdSmartHomeDeviceMapping = newJsonIdSmartHomeDeviceMapping; + } catch (ConnectionException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } - // update handlers - smartHomeDeviceHandlers - .forEach(child -> child.setDeviceAndUpdateThingState(this, findSmartDeviceHomeJson(child))); - if (smartHomeDevices != null) { - return smartHomeDevices; - } + return List.of(); + } - return Collections.emptyList(); + private void searchSmartHomeDevicesRecursive(@Nullable Object jsonNode, List devices) { + if (jsonNode instanceof Map) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + Map map = (Map) jsonNode; + if (map.containsKey("entityId") && map.containsKey("friendlyName") && map.containsKey("actions")) { + // device node found, create type element and add it to the results + JsonElement element = gson.toJsonTree(jsonNode); + JsonSmartHomeDevice shd = parseJson(element.toString(), JsonSmartHomeDevice.class); + if (shd != null) { + devices.add(shd); + } + } else if (map.containsKey("applianceGroupName")) { + JsonElement element = gson.toJsonTree(jsonNode); + JsonSmartHomeGroups.SmartHomeGroup shg = parseJson(element.toString(), + JsonSmartHomeGroups.SmartHomeGroup.class); + if (shg != null) { + devices.add(shg); + } + } else { + map.values().forEach(value -> searchSmartHomeDevicesRecursive(value, devices)); + } + } } - public void forceDelayedSmartHomeStateUpdate(@Nullable String deviceId) { - if (deviceId == null) { - return; + // parser + private @Nullable T parseJson(String json, Class type) throws JsonSyntaxException, IllegalStateException { + try { + // gson.fromJson is non-null if json is non-null and not empty + return gson.fromJson(json, type); + } catch (JsonParseException | IllegalStateException e) { + logger.warn("Parsing json failed: {}", json, e); + throw e; } + } + + public void forceDelayedSmartHomeStateUpdate(String deviceId) { synchronized (synchronizeSmartHomeJobScheduler) { requestedDeviceUpdates.add(deviceId); ScheduledFuture refreshSmartHomeAfterCommandJob = this.refreshSmartHomeAfterCommandJob; @@ -860,8 +790,7 @@ private void updateSmartHomeStateJob() { Set deviceUpdates = new HashSet<>(); synchronized (synchronizeSmartHomeJobScheduler) { - Connection connection = this.connection; - if (connection == null || !connection.getIsLoggedIn()) { + if (!connection.isLoggedIn()) { this.refreshSmartHomeAfterCommandJob = scheduler.schedule(this::updateSmartHomeStateJob, 1000, TimeUnit.MILLISECONDS); return; @@ -875,9 +804,8 @@ private void updateSmartHomeStateJob() { private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) { try { - logger.debug("updateSmartHomeState started with deviceFilterId={}", deviceFilterId); - Connection connection = this.connection; - if (connection == null || !connection.getIsLoggedIn()) { + logger.trace("updateSmartHomeState started with deviceFilterId={}", deviceFilterId); + if (!connection.isLoggedIn()) { return; } List allDevices = getLastKnownSmartHomeDevices(); @@ -893,15 +821,14 @@ private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) if (smartHomeDeviceHandlers.isEmpty()) { return; } - List devicesToUpdate = new ArrayList<>(); + List devicesToUpdate = new ArrayList<>(); for (SmartHomeDeviceHandler device : smartHomeDeviceHandlers) { String id = device.getId(); SmartHomeBaseDevice baseDevice = jsonIdSmartHomeDeviceMapping.get(id); - SmartHomeDeviceHandler.getSupportedSmartHomeDevices(baseDevice, allDevices) - .forEach(devicesToUpdate::add); + devicesToUpdate.addAll(SmartHomeDeviceHandler.getSupportedSmartHomeDevices(baseDevice, allDevices)); } smartHomeDeviceStateGroupUpdateCalculator.removeDevicesWithNoUpdate(devicesToUpdate); - devicesToUpdate.stream().filter(Objects::nonNull).forEach(targetDevices::add); + targetDevices.addAll(devicesToUpdate); if (targetDevices.isEmpty()) { return; } @@ -923,10 +850,23 @@ private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) } logger.debug("updateSmartHomeState finished"); - } catch (HttpException | JsonSyntaxException | ConnectionException e) { + } catch (JsonSyntaxException | ConnectionException e) { logger.debug("updateSmartHomeState fails", e); } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail. logger.warn("updateSmartHomeState fails with unexpected error", e); } } + + @Override + public Collection> getServices() { + return Set.of(AmazonEchoDiscovery.class, SmartHomeDevicesDiscovery.class); + } + + @Override + public void onPushConnectionStateChange(PushConnection.State state) { + if (!disposing && state == CLOSED) { + // force check of login + nextLoginCheck = 0; + } + } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java index 096c56223bdee..38f66254134f3 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java @@ -12,56 +12,57 @@ */ package org.openhab.binding.amazonechocontrol.internal.handler; +import static org.eclipse.jetty.util.StringUtil.isNotBlank; import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; +import static org.openhab.binding.amazonechocontrol.internal.dto.push.PushAudioPlayerStateTO.AudioPlayerState.*; +import static org.openhab.binding.amazonechocontrol.internal.util.Util.findIn; -import java.io.IOException; -import java.net.URISyntaxException; -import java.time.Instant; -import java.time.ZoneId; +import java.time.Duration; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlStateDescriptionProvider; import org.openhab.binding.amazonechocontrol.internal.ConnectionException; -import org.openhab.binding.amazonechocontrol.internal.HttpException; -import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandler; -import org.openhab.binding.amazonechocontrol.internal.channelhandler.ChannelHandlerAnnouncement; -import org.openhab.binding.amazonechocontrol.internal.channelhandler.IEchoThingHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity.Description; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.PairedDevice; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushNotificationChange; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonCommandPayloadPushVolumeChange; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonEqualizer; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState.QueueEntry; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.InfoText; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.MainArt; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Progress; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Provider; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Volume; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.AscendingAlarmModelTO; +import org.openhab.binding.amazonechocontrol.internal.dto.BluetoothPairedDeviceTO; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceNotificationStateTO; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceTO; +import org.openhab.binding.amazonechocontrol.internal.dto.DoNotDisturbDeviceStatusTO; +import org.openhab.binding.amazonechocontrol.internal.dto.EqualizerTO; +import org.openhab.binding.amazonechocontrol.internal.dto.NotificationSoundTO; +import org.openhab.binding.amazonechocontrol.internal.dto.NotificationTO; +import org.openhab.binding.amazonechocontrol.internal.dto.PlayerStateInfoTO; +import org.openhab.binding.amazonechocontrol.internal.dto.PlayerStateInfoTextTO; +import org.openhab.binding.amazonechocontrol.internal.dto.PlayerStateMainArtTO; +import org.openhab.binding.amazonechocontrol.internal.dto.PlayerStateProgressTO; +import org.openhab.binding.amazonechocontrol.internal.dto.PlayerStateProviderTO; +import org.openhab.binding.amazonechocontrol.internal.dto.PlayerStateVolumeTO; +import org.openhab.binding.amazonechocontrol.internal.dto.push.PushAudioPlayerStateTO; +import org.openhab.binding.amazonechocontrol.internal.dto.push.PushEqualizerStateChangeTO; +import org.openhab.binding.amazonechocontrol.internal.dto.push.PushVolumeChangeTO; +import org.openhab.binding.amazonechocontrol.internal.dto.request.PlayerSeekMediaTO; +import org.openhab.binding.amazonechocontrol.internal.dto.request.WHAVolumeLevelTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.BluetoothStateTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.CustomerHistoryRecordTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.CustomerHistoryRecordVoiceTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.MediaSessionTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.MusicProviderTO; +import org.openhab.binding.amazonechocontrol.internal.dto.response.PlayerStateTO; +import org.openhab.binding.amazonechocontrol.internal.types.Announcement; +import org.openhab.binding.amazonechocontrol.internal.types.Notification; +import org.openhab.core.cache.ExpiringCacheMap; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.IncreaseDecreaseType; @@ -77,16 +78,21 @@ 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.ThingUID; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.openhab.core.types.StateOption; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; /** * The {@link EchoHandler} is responsible for the handling of the echo device @@ -94,79 +100,70 @@ * @author Michael Geramb - Initial contribution */ @NonNullByDefault -public class EchoHandler extends BaseThingHandler implements IEchoThingHandler { +public class EchoHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(EchoHandler.class); - private Gson gson; - private @Nullable Device device; + private final Gson gson; + private final AmazonEchoControlStateDescriptionProvider dynamicStateDescriptionProvider; + + private @Nullable DeviceTO device; private Set capabilities = new HashSet<>(); - private @Nullable AccountHandler account; + private @Nullable AccountHandler account = null; private @Nullable ScheduledFuture updateStateJob; private @Nullable ScheduledFuture updateProgressJob; - private Object progressLock = new Object(); + private final Object progressLock = new Object(); private @Nullable String wakeWord; - private @Nullable String lastKnownRadioStationId; private @Nullable String lastKnownBluetoothMAC; - private @Nullable String lastKnownAmazonMusicId; + private long lastCustomerHistoryRecordTimestamp = System.currentTimeMillis(); private String musicProviderId = "TUNEIN"; private boolean isPlaying = false; private boolean isPaused = false; private int lastKnownVolume = 25; private int textToSpeechVolume = 0; - private @Nullable JsonEqualizer lastKnownEqualizer = null; - private @Nullable BluetoothState bluetoothState; + private @Nullable EqualizerTO lastKnownEqualizer = null; private boolean disableUpdate = false; - private boolean updateRemind = true; - private boolean updateTextToSpeech = true; - private boolean updateTextCommand = true; - private boolean updateAlarm = true; - private boolean updateRoutine = true; - private boolean updatePlayMusicVoiceCommand = true; - private boolean updateStartCommand = true; - private @Nullable Integer notificationVolumeLevel; - private @Nullable Boolean ascendingAlarm; - private @Nullable JsonPlaylists playLists; - private List alarmSounds = List.of(); - private List musicProviders = List.of(); - private List channelHandlers = new ArrayList<>(); - - private @Nullable JsonNotificationResponse currentNotification; - private @Nullable ScheduledFuture currentNotifcationUpdateTimer; - long mediaLengthMs; - long mediaProgressMs; - long mediaStartMs; - String lastSpokenText = ""; - - public EchoHandler(Thing thing, Gson gson) { + + private @Nullable NotificationTO currentNotification; + private @Nullable ScheduledFuture currentNotificationUpdateTimer; + private long mediaLengthMs; + private long mediaProgressMs; + private long mediaStartMs; + + private String currentlyPlayingQueueId = ""; + + // used to block further updates when an update is already taking place + private final AtomicBoolean waitingForUpdate = new AtomicBoolean(false); + + private final ExpiringCacheMap stateCache = new ExpiringCacheMap<>(Duration.ofSeconds(30)); + + public EchoHandler(Thing thing, Gson gson, + AmazonEchoControlStateDescriptionProvider dynamicStateDescriptionProvider) { super(thing); this.gson = gson; - channelHandlers.add(new ChannelHandlerAnnouncement(this, this.gson)); + this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider; } @Override public void initialize() { - logger.debug("Amazon Echo Control Binding initialized"); Bridge bridge = this.getBridge(); if (bridge != null) { - AccountHandler account = (AccountHandler) bridge.getHandler(); - if (account != null) { - setDeviceAndUpdateThingState(account, this.device, null); - account.addEchoHandler(this); - } + account = (AccountHandler) bridge.getHandler(); + } + if (account == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Bridge handler not found."); + } else { + updateStatus(ThingStatus.UNKNOWN); } + + lastCustomerHistoryRecordTimestamp = System.currentTimeMillis(); } - public boolean setDeviceAndUpdateThingState(AccountHandler accountHandler, @Nullable Device device, - @Nullable String wakeWord) { - this.account = accountHandler; + public boolean setDeviceAndUpdateThingStatus(DeviceTO device, @Nullable String wakeWord) { if (wakeWord != null) { this.wakeWord = wakeWord; } - if (device == null) { - updateStatus(ThingStatus.UNKNOWN); - return false; - } + this.device = device; - this.capabilities = device.getCapabilities(); + this.capabilities = device.capabilities; if (!device.online) { updateStatus(ThingStatus.OFFLINE); return false; @@ -185,7 +182,6 @@ public void dispose() { updateStateJob.cancel(false); } stopProgressTimer(); - super.dispose(); } private void stopProgressTimer() { @@ -196,53 +192,40 @@ private void stopProgressTimer() { } } - public @Nullable BluetoothState findBluetoothState() { - return this.bluetoothState; - } - - public @Nullable JsonPlaylists findPlaylists() { - return this.playLists; - } - - public List findAlarmSounds() { - return this.alarmSounds; - } - - public List findMusicProviders() { - return this.musicProviders; - } - - private @Nullable Connection findConnection() { - AccountHandler accountHandler = this.account; - if (accountHandler != null) { - return accountHandler.findConnection(); - } - return null; - } - - public @Nullable AccountHandler findAccount() { - return this.account; + private Optional getAccountHandler() { + return Optional.ofNullable(account); } - public @Nullable Device findDevice() { - return this.device; + private Optional findConnection() { + return getAccountHandler().map(AccountHandler::getConnection); } - public String findSerialNumber() { + public String getSerialNumber() { String id = (String) getConfig().get(DEVICE_PROPERTY_SERIAL_NUMBER); - if (id == null) { - return ""; - } - return id; + return id != null ? id : ""; } @Override public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + State state = stateCache.get(channelUID.getId()); + if (state != null) { + // if we have cached value use that and return + // call the original update method to prevent prolonging the same value in cache + super.updateState(channelUID, state); + return; + } else { + getAccountHandler().ifPresent(accountHandler -> { + if (waitingForUpdate.compareAndSet(false, true)) { + accountHandler.forceCheckData(); + } + }); + } + } try { logger.trace("Command '{}' received for channel '{}'", command, channelUID); int waitForUpdate = 1000; boolean needBluetoothRefresh = false; - String lastKnownBluetoothMAC = this.lastKnownBluetoothMAC; ScheduledFuture updateStateJob = this.updateStateJob; this.updateStateJob = null; @@ -250,86 +233,127 @@ public void handleCommand(ChannelUID channelUID, Command command) { this.disableUpdate = false; updateStateJob.cancel(false); } - AccountHandler account = this.account; - if (account == null) { + + AccountHandler accountHandler = getAccountHandler().orElse(null); + if (accountHandler == null) { return; } - Connection connection = account.findConnection(); + Connection connection = findConnection().orElse(null); if (connection == null) { return; } - Device device = this.device; + DeviceTO device = this.device; if (device == null) { return; } String channelId = channelUID.getId(); - for (ChannelHandler channelHandler : channelHandlers) { - if (channelHandler.tryHandleCommand(device, connection, channelId, command)) { - return; + if (channelId.equals(CHANNEL_ANNOUNCEMENT) && command instanceof StringType) { + String commandValue = command.toFullString(); + String body = commandValue; + String title = null; + String speak = commandValue; + Integer volume = null; + if (commandValue.startsWith("{") && commandValue.endsWith("}")) { + try { + Announcement request = gson.fromJson(commandValue, Announcement.class); + if (request != null) { + speak = isNotBlank(request.speak) ? request.speak : "."; // generate beep if no text + Objects.requireNonNull(speak); // fix the null-checker + volume = request.volume; + title = request.title; + body = request.body != null ? request.body : speak; + Boolean sound = request.sound; + if (sound != null) { + if (!sound && !speak.startsWith("")) { + speak = "" + speak + ""; + } + if (sound && speak.startsWith("")) { + body = "Error: The combination of sound and speak in SSML syntax is not allowed"; + title = "Error"; + speak = "Error: The combination of sound and speak in SSML syntax is not allowed"; + } + } + if (" ".equals(speak)) { + volume = -1; // Do not change volume + } + } + } catch (JsonSyntaxException e) { + body = "Invalid Json." + e.getLocalizedMessage(); + title = "Error"; + speak = "" + body + ""; + body = e.getLocalizedMessage(); + } + } + Integer vol; + if (volume == null && textToSpeechVolume != 0) { + vol = textToSpeechVolume; + } else if (volume != null && volume < 0) { + vol = null;// the meaning of negative values is 'do not use'. The api requires null in this case. + } else { + vol = volume; } + String finalSpeak = speak; + String finalBody = Objects.requireNonNullElse(body, ""); + String finalTitle = title; + connection.announcement(device, finalSpeak, finalBody, finalTitle, vol, lastKnownVolume); } // Player commands if (channelId.equals(CHANNEL_PLAYER)) { if (command == PlayPauseType.PAUSE || command == OnOffType.OFF) { - connection.command(device, "{\"type\":\"PauseCommand\"}"); + connection.command(device, Map.of("type", "PauseCommand")); } else if (command == PlayPauseType.PLAY || command == OnOffType.ON) { if (isPaused) { - connection.command(device, "{\"type\":\"PlayCommand\"}"); + connection.command(device, Map.of("type", "PlayCommand")); } else { connection.playMusicVoiceCommand(device, this.musicProviderId, "!"); waitForUpdate = 3000; } } else if (command == NextPreviousType.NEXT) { - connection.command(device, "{\"type\":\"NextCommand\"}"); + connection.command(device, Map.of("type", "NextCommand")); } else if (command == NextPreviousType.PREVIOUS) { - connection.command(device, "{\"type\":\"PreviousCommand\"}"); + connection.command(device, Map.of("type", "PreviousCommand")); } else if (command == RewindFastforwardType.FASTFORWARD) { - connection.command(device, "{\"type\":\"ForwardCommand\"}"); + connection.command(device, Map.of("type", "ForwardCommand")); } else if (command == RewindFastforwardType.REWIND) { - connection.command(device, "{\"type\":\"RewindCommand\"}"); + connection.command(device, Map.of("type", "RewindCommand")); } } // Notification commands if (channelId.equals(CHANNEL_NOTIFICATION_VOLUME)) { - if (command instanceof PercentType percentCommand) { - int volume = percentCommand.intValue(); - connection.notificationVolume(device, volume); - this.notificationVolumeLevel = volume; + if (command instanceof PercentType percent) { + connection.setNotificationVolume(device, percent.intValue()); waitForUpdate = -1; - account.forceCheckData(); + accountHandler.forceCheckData(); } } if (channelId.equals(CHANNEL_ASCENDING_ALARM)) { - if (command == OnOffType.OFF) { - connection.ascendingAlarm(device, false); - this.ascendingAlarm = false; - waitForUpdate = -1; - account.forceCheckData(); - } - if (command == OnOffType.ON) { - connection.ascendingAlarm(device, true); - this.ascendingAlarm = true; - waitForUpdate = -1; - account.forceCheckData(); - } + boolean ascendingAlarm = command == OnOffType.ON; + connection.setAscendingAlarm(device, ascendingAlarm); + waitForUpdate = -1; + accountHandler.forceCheckData(); + } + // Do Not Disturb command + if (channelId.equals(CHANNEL_DO_NOT_DISTURB) && command instanceof OnOffType) { + boolean newDnd = command == OnOffType.ON; + connection.setDoNotDisturb(device, newDnd); + waitForUpdate = -1; + accountHandler.forceCheckData(); } // Media progress commands Long mediaPosition = null; if (channelId.equals(CHANNEL_MEDIA_PROGRESS)) { - if (command instanceof PercentType percentCommand) { - int percent = percentCommand.intValue(); - mediaPosition = Math.round((mediaLengthMs / 1000d) * (percent / 100d)); + if (command instanceof PercentType percent) { + mediaPosition = Math.round((mediaLengthMs / 1000d) * (percent.intValue() / 100d)); } } if (channelId.equals(CHANNEL_MEDIA_PROGRESS_TIME)) { - if (command instanceof DecimalType decimalCommand) { - mediaPosition = decimalCommand.longValue(); + if (command instanceof DecimalType decimal) { + mediaPosition = decimal.longValue(); } - if (command instanceof QuantityType quantityCommand) { - @Nullable - QuantityType seconds = quantityCommand.toUnit(Units.SECOND); + if (command instanceof QuantityType quantity) { + QuantityType seconds = quantity.toUnit(Units.SECOND); if (seconds != null) { mediaPosition = seconds.longValue(); } @@ -338,8 +362,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (mediaPosition != null) { waitForUpdate = -1; synchronized (progressLock) { - String seekCommand = "{\"type\":\"SeekCommand\",\"mediaPosition\":" + mediaPosition - + ",\"contentFocusClientId\":null}"; + PlayerSeekMediaTO seekCommand = new PlayerSeekMediaTO(); + seekCommand.mediaPosition = mediaPosition; connection.command(device, seekCommand); connection.command(device, seekCommand); // Must be sent twice, the first one is ignored sometimes this.mediaProgressMs = mediaPosition * 1000; @@ -350,8 +374,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { // Volume commands if (channelId.equals(CHANNEL_VOLUME)) { Integer volume = null; - if (command instanceof PercentType percentCommand) { - volume = percentCommand.intValue(); + if (command instanceof PercentType value) { + volume = value.intValue(); } else if (command == OnOffType.OFF) { volume = 0; } else if (command == OnOffType.ON) { @@ -369,10 +393,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { } if (volume != null) { if ("WHA".equals(device.deviceFamily)) { - connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume - + ",\"contentFocusClientId\":\"Default\"}"); + WHAVolumeLevelTO volumeCommand = new WHAVolumeLevelTO(); + volumeCommand.volumeLevel = volume; + connection.command(device, volumeCommand); } else { - connection.volume(device, volume); + connection.setVolume(device, volume); } lastKnownVolume = volume; updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume)); @@ -389,42 +414,36 @@ public void handleCommand(ChannelUID channelUID, Command command) { // shuffle command if (channelId.equals(CHANNEL_SHUFFLE)) { - if (command instanceof OnOffType value) { - connection.command(device, "{\"type\":\"ShuffleCommand\",\"shuffle\":\"" - + (value == OnOffType.ON ? "true" : "false") + "\"}"); + if (command instanceof OnOffType onOff) { + connection.command(device, Map.of("type", "ShuffleCommand", "shuffle", onOff == OnOffType.ON)); } } // play music command - if (channelId.equals(CHANNEL_MUSIC_PROVIDER_ID)) { - if (command instanceof StringType) { - waitForUpdate = 0; - String musicProviderId = command.toFullString(); - if (!musicProviderId.equals(this.musicProviderId)) { - this.musicProviderId = musicProviderId; - if (this.isPlaying) { - connection.playMusicVoiceCommand(device, this.musicProviderId, "!"); - waitForUpdate = 3000; - } + if (channelId.equals(CHANNEL_MUSIC_PROVIDER_ID) && command instanceof StringType) { + waitForUpdate = 0; + String musicProviderId = command.toFullString(); + if (!musicProviderId.equals(this.musicProviderId)) { + this.musicProviderId = musicProviderId; + if (this.isPlaying) { + connection.playMusicVoiceCommand(device, this.musicProviderId, "!"); + waitForUpdate = 3000; } } } - if (channelId.equals(CHANNEL_PLAY_MUSIC_VOICE_COMMAND)) { - if (command instanceof StringType) { - String voiceCommand = command.toFullString(); - if (!this.musicProviderId.isEmpty()) { - connection.playMusicVoiceCommand(device, this.musicProviderId, voiceCommand); - waitForUpdate = 3000; - updatePlayMusicVoiceCommand = true; - } + if (channelId.equals(CHANNEL_PLAY_MUSIC_VOICE_COMMAND) && command instanceof StringType) { + String voiceCommand = command.toFullString(); + if (!this.musicProviderId.isEmpty()) { + connection.playMusicVoiceCommand(device, this.musicProviderId, voiceCommand); + waitForUpdate = 3000; } } // bluetooth commands if (channelId.equals(CHANNEL_BLUETOOTH_MAC)) { needBluetoothRefresh = true; - if (command instanceof StringType stringCommand) { - String address = stringCommand.toFullString(); + if (command instanceof StringType) { + String address = command.toFullString(); if (!address.isEmpty()) { waitForUpdate = 4000; } @@ -433,19 +452,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { } if (channelId.equals(CHANNEL_BLUETOOTH)) { needBluetoothRefresh = true; + String lastKnownBluetoothMAC = this.lastKnownBluetoothMAC; if (command == OnOffType.ON) { waitForUpdate = 4000; - String bluetoothId = lastKnownBluetoothMAC; - BluetoothState state = bluetoothState; - if (state != null && (bluetoothId == null || bluetoothId.isEmpty())) { - for (PairedDevice paired : state.getPairedDeviceList()) { - String pairedAddress = paired.address; - if (pairedAddress != null && !pairedAddress.isEmpty()) { - lastKnownBluetoothMAC = pairedAddress; - break; - } - } - } if (lastKnownBluetoothMAC != null && !lastKnownBluetoothMAC.isEmpty()) { connection.bluetooth(device, lastKnownBluetoothMAC); } @@ -456,112 +465,49 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (channelId.equals(CHANNEL_BLUETOOTH_DEVICE_NAME)) { needBluetoothRefresh = true; } - // amazon music commands - if (channelId.equals(CHANNEL_AMAZON_MUSIC_TRACK_ID)) { - if (command instanceof StringType) { - String trackId = command.toFullString(); - if (!trackId.isEmpty()) { - waitForUpdate = 3000; - } - connection.playAmazonMusicTrack(device, trackId); - } - } - if (channelId.equals(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID)) { - if (command instanceof StringType) { - String playListId = command.toFullString(); - if (!playListId.isEmpty()) { - waitForUpdate = 3000; - } - connection.playAmazonMusicPlayList(device, playListId); - } - } - if (channelId.equals(CHANNEL_AMAZON_MUSIC)) { - if (command == OnOffType.ON) { - String lastKnownAmazonMusicId = this.lastKnownAmazonMusicId; - if (lastKnownAmazonMusicId != null && !lastKnownAmazonMusicId.isEmpty()) { - waitForUpdate = 3000; - } - connection.playAmazonMusicTrack(device, lastKnownAmazonMusicId); - } else if (command == OnOffType.OFF) { - connection.playAmazonMusicTrack(device, ""); - } - } - - // radio commands - if (channelId.equals(CHANNEL_RADIO_STATION_ID)) { - if (command instanceof StringType) { - String stationId = command.toFullString(); - if (!stationId.isEmpty()) { - waitForUpdate = 3000; - } - connection.playRadio(device, stationId); - } - } - if (channelId.equals(CHANNEL_RADIO)) { - if (command == OnOffType.ON) { - String lastKnownRadioStationId = this.lastKnownRadioStationId; - if (lastKnownRadioStationId != null && !lastKnownRadioStationId.isEmpty()) { - waitForUpdate = 3000; - } - connection.playRadio(device, lastKnownRadioStationId); - } else if (command == OnOffType.OFF) { - connection.playRadio(device, ""); - } - } // notification - if (channelId.equals(CHANNEL_REMIND)) { - if (command instanceof StringType) { - stopCurrentNotification(); - String reminder = command.toFullString(); - if (!reminder.isEmpty()) { - waitForUpdate = 3000; - updateRemind = true; - currentNotification = connection.notification(device, "Reminder", reminder, null); - currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> { - updateNotificationTimerState(); - }, 1, 1, TimeUnit.SECONDS); - } - } - } - if (channelId.equals(CHANNEL_PLAY_ALARM_SOUND)) { - if (command instanceof StringType) { - stopCurrentNotification(); - String alarmSound = command.toFullString(); - if (!alarmSound.isEmpty()) { - waitForUpdate = 3000; - updateAlarm = true; - String[] parts = alarmSound.split(":", 2); - JsonNotificationSound sound = new JsonNotificationSound(); - if (parts.length == 2) { - sound.providerId = parts[0]; - sound.id = parts[1]; - } else { - sound.providerId = "ECHO"; - sound.id = alarmSound; - } - currentNotification = connection.notification(device, "Alarm", null, sound); - currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> { - updateNotificationTimerState(); - }, 1, 1, TimeUnit.SECONDS); + if (channelId.equals(CHANNEL_REMIND) && command instanceof StringType) { + stopCurrentNotification(); + String reminder = command.toFullString(); + if (!reminder.isBlank()) { + waitForUpdate = 3000; + currentNotification = connection.createNotification(device, "Reminder", reminder, null); + currentNotificationUpdateTimer = scheduler + .scheduleWithFixedDelay(this::updateNotificationTimerState, 1, 1, TimeUnit.SECONDS); + } + } + if (channelId.equals(CHANNEL_PLAY_ALARM_SOUND) && command instanceof StringType) { + stopCurrentNotification(); + String alarmSound = command.toFullString(); + if (!alarmSound.isEmpty()) { + waitForUpdate = 3000; + String[] parts = alarmSound.split(":", 2); + NotificationSoundTO sound = new NotificationSoundTO(); + if (parts.length == 2) { + sound.providerId = parts[0]; + sound.id = parts[1]; + } else { + sound.providerId = "ECHO"; + sound.id = alarmSound; } + currentNotification = connection.createNotification(device, "Alarm", null, sound); + currentNotificationUpdateTimer = scheduler + .scheduleWithFixedDelay(this::updateNotificationTimerState, 1, 1, TimeUnit.SECONDS); } } // routine commands - if (channelId.equals(CHANNEL_TEXT_TO_SPEECH)) { - if (command instanceof StringType) { - String text = command.toFullString(); - if (!text.isEmpty()) { - waitForUpdate = 1000; - updateTextToSpeech = true; - startTextToSpeech(connection, device, text); - } + if (channelId.equals(CHANNEL_TEXT_TO_SPEECH) && command instanceof StringType) { + String text = command.toFullString(); + if (!text.isEmpty()) { + waitForUpdate = 1000; + startTextToSpeech(connection, device, text); } } if (channelId.equals(CHANNEL_TEXT_TO_SPEECH_VOLUME)) { - if (command instanceof PercentType percentCommand) { - textToSpeechVolume = percentCommand.intValue(); + if (command instanceof PercentType percent) { + textToSpeechVolume = percent.intValue(); } else if (command == OnOffType.OFF) { textToSpeechVolume = 0; } else if (command == OnOffType.ON) { @@ -577,62 +523,48 @@ public void handleCommand(ChannelUID channelUID, Command command) { } this.updateState(channelId, new PercentType(textToSpeechVolume)); } - if (channelId.equals(CHANNEL_TEXT_COMMAND)) { - if (command instanceof StringType) { - String text = command.toFullString(); - if (!text.isEmpty()) { - waitForUpdate = 1000; - updateTextCommand = true; - startTextCommand(connection, device, text); - } - } - } - if (channelId.equals(CHANNEL_LAST_VOICE_COMMAND)) { - if (command instanceof StringType) { - String text = command.toFullString(); - if (!text.isEmpty()) { - waitForUpdate = -1; - startTextToSpeech(connection, device, text); - } + if (channelId.equals(CHANNEL_TEXT_COMMAND) && command instanceof StringType) { + String text = command.toFullString(); + if (!text.isEmpty()) { + waitForUpdate = 1000; + startTextCommand(connection, device, text); } } - if (channelId.equals(CHANNEL_START_COMMAND)) { - if (command instanceof StringType) { - String commandText = command.toFullString(); - if (!commandText.isEmpty()) { - updateStartCommand = true; - if (commandText.startsWith(FLASH_BRIEFING_COMMAND_PREFIX)) { - // Handle custom flashbriefings commands - String flashBriefingId = commandText.substring(FLASH_BRIEFING_COMMAND_PREFIX.length()); - for (FlashBriefingProfileHandler flashBriefingHandler : account - .getFlashBriefingProfileHandlers()) { - ThingUID flashBriefingUid = flashBriefingHandler.getThing().getUID(); - if (flashBriefingId.equals(flashBriefingHandler.getThing().getUID().getId())) { - flashBriefingHandler.handleCommand( - new ChannelUID(flashBriefingUid, CHANNEL_PLAY_ON_DEVICE), - new StringType(device.serialNumber)); - break; - } - } - } else { - // Handle standard commands - if (!commandText.startsWith("Alexa.")) { - commandText = "Alexa." + commandText + ".Play"; - } - waitForUpdate = 1000; - connection.executeSequenceCommand(device, commandText, Map.of()); + if (channelId.equals(CHANNEL_LAST_VOICE_COMMAND) && command instanceof StringType) { + String text = command.toFullString(); + if (!text.isEmpty()) { + waitForUpdate = -1; + startTextToSpeech(connection, device, text); + } + } + if (channelId.equals(CHANNEL_START_COMMAND) && command instanceof StringType) { + String commandText = command.toFullString(); + if (commandText.startsWith(FLASH_BRIEFING_COMMAND_PREFIX)) { + // Handle custom flashbriefings commands + String flashBriefingId = commandText.substring(FLASH_BRIEFING_COMMAND_PREFIX.length()); + for (FlashBriefingProfileHandler flashBriefingHandler : accountHandler + .getFlashBriefingProfileHandlers()) { + ThingUID flashBriefingUid = flashBriefingHandler.getThing().getUID(); + if (flashBriefingId.equals(flashBriefingHandler.getThing().getUID().getId())) { + flashBriefingHandler.handleCommand(new ChannelUID(flashBriefingUid, CHANNEL_PLAY_ON_DEVICE), + new StringType(device.serialNumber)); + break; } } + } else if (!commandText.isBlank()) { + // Handle standard commands + if (!commandText.startsWith("Alexa.")) { + commandText = "Alexa." + commandText + ".Play"; + } + waitForUpdate = 1000; + connection.executeSequenceCommand(device, commandText, Map.of()); } } - if (channelId.equals(CHANNEL_START_ROUTINE)) { - if (command instanceof StringType) { - String utterance = command.toFullString(); - if (!utterance.isEmpty()) { - waitForUpdate = 1000; - updateRoutine = true; - connection.startRoutine(device, utterance); - } + if (channelId.equals(CHANNEL_START_ROUTINE) && command instanceof StringType) { + String utterance = command.toFullString(); + if (!utterance.isEmpty()) { + waitForUpdate = 1000; + connection.startRoutine(device, utterance); } } if (waitForUpdate < 0) { @@ -643,493 +575,321 @@ public void handleCommand(ChannelUID channelUID, Command command) { final boolean bluetoothRefresh = needBluetoothRefresh; Runnable doRefresh = () -> { this.disableUpdate = false; - BluetoothState state = null; + BluetoothStateTO state = null; if (bluetoothRefresh) { - JsonBluetoothStates states; - states = connection.getBluetoothConnectionStates(); - if (states != null) { - state = states.findStateByDevice(device); - } + List states = connection.getBluetoothConnectionStates(); + state = findIn(states, a -> a.deviceSerialNumber, device.serialNumber).orElse(null); } - - updateState(account, device, state, null, null, null, null, null); + updateState(device, state, null, null, null, null); }; - if (command instanceof RefreshType) { - waitForUpdate = 0; - account.forceCheckData(); - } if (waitForUpdate == 0) { doRefresh.run(); } else { this.updateStateJob = scheduler.schedule(doRefresh, waitForUpdate, TimeUnit.MILLISECONDS); } - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.info("handleCommand fails", e); + } catch (ConnectionException e) { + logger.info("Failed to handle command '{}' to '{}'", command, channelUID); + } catch (RuntimeException e) { + logger.warn("RuntimeException in handle command", e); } } - private boolean handleEqualizerCommands(String channelId, Command command, Connection connection, Device device) - throws URISyntaxException { - if (command instanceof RefreshType) { - this.lastKnownEqualizer = null; - } - if (command instanceof DecimalType decimalCommand) { - if (this.lastKnownEqualizer == null) { + private boolean handleEqualizerCommands(String channelId, Command command, Connection connection, DeviceTO device) { + if (command instanceof DecimalType decimal) { + if (lastKnownEqualizer == null) { updateEqualizerState(); } - JsonEqualizer lastKnownEqualizer = this.lastKnownEqualizer; - if (lastKnownEqualizer != null) { - JsonEqualizer newEqualizerSetting = lastKnownEqualizer.createClone(); - if (channelId.equals(CHANNEL_EQUALIZER_BASS)) { - newEqualizerSetting.bass = decimalCommand.intValue(); - } - if (channelId.equals(CHANNEL_EQUALIZER_MIDRANGE)) { - newEqualizerSetting.mid = decimalCommand.intValue(); - } - if (channelId.equals(CHANNEL_EQUALIZER_TREBLE)) { - newEqualizerSetting.treble = decimalCommand.intValue(); - } - try { - connection.setEqualizer(device, newEqualizerSetting); - return true; - } catch (HttpException | IOException | ConnectionException | InterruptedException e) { - logger.debug("Update equalizer failed", e); - this.lastKnownEqualizer = null; - } + EqualizerTO oldEqualizer = lastKnownEqualizer; + if (oldEqualizer != null) { + EqualizerTO newEqualizer = new EqualizerTO(); + newEqualizer.bass = channelId.equals(CHANNEL_EQUALIZER_BASS) ? decimal.intValue() : oldEqualizer.bass; + newEqualizer.mid = channelId.equals(CHANNEL_EQUALIZER_MIDRANGE) ? decimal.intValue() : oldEqualizer.mid; + newEqualizer.treble = channelId.equals(CHANNEL_EQUALIZER_TREBLE) ? decimal.intValue() + : oldEqualizer.treble; + return connection.setEqualizer(device, newEqualizer); } } return false; } - private void startTextToSpeech(Connection connection, Device device, String text) - throws IOException, URISyntaxException { - Integer volume = null; - if (textToSpeechVolume != 0) { - volume = textToSpeechVolume; - } + private void startTextToSpeech(Connection connection, DeviceTO device, String text) { + Integer volume = textToSpeechVolume != 0 ? textToSpeechVolume : null; connection.textToSpeech(device, text, volume, lastKnownVolume); } - private void startTextCommand(Connection connection, Device device, String text) - throws IOException, URISyntaxException { - Integer volume = null; - if (textToSpeechVolume != 0) { - volume = textToSpeechVolume; - } + private void startTextCommand(Connection connection, DeviceTO device, String text) { + Integer volume = textToSpeechVolume != 0 ? textToSpeechVolume : null; connection.textCommand(device, text, volume, lastKnownVolume); } - @Override - public void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title, - @Nullable Integer volume) throws IOException, URISyntaxException { - Connection connection = this.findConnection(); - if (connection == null) { - return; - } - if (volume == null && textToSpeechVolume != 0) { - volume = textToSpeechVolume; - } - if (volume != null && volume < 0) { - volume = null; // the meaning of negative values is 'do not use'. The api requires null in this case. - } - connection.announcement(device, speak, bodyText, title, volume, lastKnownVolume); - } - private void stopCurrentNotification() { - ScheduledFuture currentNotifcationUpdateTimer = this.currentNotifcationUpdateTimer; - if (currentNotifcationUpdateTimer != null) { - this.currentNotifcationUpdateTimer = null; - currentNotifcationUpdateTimer.cancel(true); + ScheduledFuture currentNotificationUpdateTimer = this.currentNotificationUpdateTimer; + if (currentNotificationUpdateTimer != null) { + this.currentNotificationUpdateTimer = null; + // do not interrupt the current set, otherwise the DELETE request will be aborted + currentNotificationUpdateTimer.cancel(false); } - JsonNotificationResponse currentNotification = this.currentNotification; + NotificationTO currentNotification = this.currentNotification; if (currentNotification != null) { this.currentNotification = null; - Connection currentConnection = this.findConnection(); - if (currentConnection != null) { - try { - currentConnection.stopNotification(currentNotification); - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.warn("Stop notification failed", e); - } - } + findConnection().ifPresent(connection -> connection.deleteNotification(currentNotification.id)); } } private void updateNotificationTimerState() { boolean stopCurrentNotification = true; - JsonNotificationResponse currentNotification = this.currentNotification; + NotificationTO currentNotification = this.currentNotification; + Connection currentConnection = this.findConnection().orElse(null); try { - if (currentNotification != null) { - Connection currentConnection = this.findConnection(); - if (currentConnection != null) { - JsonNotificationResponse newState = currentConnection.getNotificationState(currentNotification); - if (newState != null && "ON".equals(newState.status)) { - stopCurrentNotification = false; - } + if (currentNotification != null && currentConnection != null) { + String status = currentConnection.getNotification(currentNotification.id).status; + if ("ON".equals(status)) { + stopCurrentNotification = false; } } - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.warn("update notification state fails", e); + } catch (ConnectionException e) { + logger.warn("Failed to update notification state"); } if (stopCurrentNotification) { - if (currentNotification != null) { - String type = currentNotification.type; - if (type != null) { - if ("Reminder".equals(type)) { - updateState(CHANNEL_REMIND, StringType.EMPTY); - updateRemind = false; - } - if ("Alarm".equals(type)) { - updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY); - updateAlarm = false; - } - } - } stopCurrentNotification(); } } - public void updateState(AccountHandler accountHandler, @Nullable Device device, - @Nullable BluetoothState bluetoothState, @Nullable DeviceNotificationState deviceNotificationState, - @Nullable AscendingAlarmModel ascendingAlarmModel, @Nullable JsonPlaylists playlists, - @Nullable List alarmSounds, @Nullable List musicProviders) { - try { - this.logger.debug("Handle updateState {}", this.getThing().getUID()); - - if (deviceNotificationState != null) { - notificationVolumeLevel = deviceNotificationState.volumeLevel; + private void createMusicProviderStateDescription(List musicProviders) { + List options = new ArrayList<>(); + for (MusicProviderTO musicProvider : musicProviders) { + if (!musicProvider.supportedProperties.contains("Alexa.Music.PlaySearchPhrase")) { + continue; } - if (ascendingAlarmModel != null) { - ascendingAlarm = ascendingAlarmModel.ascendingAlarmEnabled; - } - if (playlists != null) { - this.playLists = playlists; - } - if (alarmSounds != null) { - this.alarmSounds = alarmSounds; - } - if (musicProviders != null) { - this.musicProviders = musicProviders; - } - if (!setDeviceAndUpdateThingState(accountHandler, device, null)) { - this.logger.debug("Handle updateState {} aborted: Not online", this.getThing().getUID()); - return; - } - if (device == null) { - this.logger.debug("Handle updateState {} aborted: No device", this.getThing().getUID()); - return; + String providerId = musicProvider.id; + String displayName = musicProvider.displayName; + if (isNotBlank(providerId) && "AVAILABLE".equals(musicProvider.availability)) { + options.add(new StateOption(providerId, isNotBlank(displayName) ? displayName : providerId)); } + } + ChannelUID channelUID = new ChannelUID(thing.getUID(), CHANNEL_MUSIC_PROVIDER_ID); + StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withOptions(options).build() + .toStateDescription(); - if (this.disableUpdate) { - this.logger.debug("Handle updateState {} aborted: Disabled", this.getThing().getUID()); - return; - } - Connection connection = this.findConnection(); - if (connection == null) { - return; - } + if (stateDescription != null) { + dynamicStateDescriptionProvider.setDescription(channelUID, stateDescription); + } + } - if (this.lastKnownEqualizer == null) { - updateEqualizerState(); + private void createBluetoothMACStateDescription(BluetoothStateTO bluetoothState) { + List options = new ArrayList<>(); + options.add(new StateOption("", "")); + for (BluetoothPairedDeviceTO device : bluetoothState.pairedDeviceList) { + final String value = device.address; + if (value != null && device.friendlyName != null) { + options.add(new StateOption(value, device.friendlyName)); } + } + ChannelUID channelUID = new ChannelUID(thing.getUID(), CHANNEL_BLUETOOTH_MAC); + StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withOptions(options).build() + .toStateDescription(); - PlayerInfo playerInfo = null; - Provider provider = null; - InfoText infoText = null; - MainArt mainArt = null; - String musicProviderId = null; - Progress progress = null; - try { - JsonPlayerState playerState = connection.getPlayer(device); - if (playerState != null) { - playerInfo = playerState.playerInfo; - if (playerInfo != null) { - infoText = playerInfo.infoText; - if (infoText == null) { - infoText = playerInfo.miniInfoText; - } - mainArt = playerInfo.mainArt; - provider = playerInfo.provider; - if (provider != null) { - musicProviderId = provider.providerName; - // Map the music provider id to the one used for starting music with voice command - if (musicProviderId != null) { - musicProviderId = musicProviderId.toUpperCase(); - - if ("AMAZON MUSIC".equals(musicProviderId)) { - musicProviderId = "AMAZON_MUSIC"; - } - if ("CLOUD_PLAYER".equals(musicProviderId)) { - musicProviderId = "AMAZON_MUSIC"; - } - if (musicProviderId.startsWith("TUNEIN")) { - musicProviderId = "TUNEIN"; - } - if (musicProviderId.startsWith("IHEARTRADIO")) { - musicProviderId = "I_HEART_RADIO"; - } - if ("APPLE".equals(musicProviderId) && musicProviderId.contains("MUSIC")) { - musicProviderId = "APPLE_MUSIC"; - } - } - } - progress = playerInfo.progress; - } + if (stateDescription != null) { + dynamicStateDescriptionProvider.setDescription(channelUID, stateDescription); + } + } + + private void updateMediaPlayerState(PlayerStateInfoTO playerInfo, boolean sequenceNodeRunning, int timeFactor) { + PlayerStateProviderTO provider = playerInfo.provider; + PlayerStateInfoTextTO infoText = playerInfo.infoText != null ? playerInfo.infoText : playerInfo.miniInfoText; + PlayerStateMainArtTO mainArt = playerInfo.mainArt; + String musicProviderId = null; + PlayerStateProgressTO progress = playerInfo.progress; + if (provider != null) { + musicProviderId = provider.providerName; + // Map the music provider id to the one used for starting music with voice command + if (musicProviderId != null) { + musicProviderId = musicProviderId.toUpperCase(); + + if ("AMAZON MUSIC".equals(musicProviderId) || "CLOUD_PLAYER".equals(musicProviderId)) { + musicProviderId = "AMAZON_MUSIC"; } - } catch (HttpException e) { - if (e.getCode() != 400) { - logger.info("getPlayer fails", e); + if (musicProviderId.startsWith("TUNEIN")) { + musicProviderId = "TUNEIN"; } - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.info("getPlayer fails", e); - } - // check playing - isPlaying = (playerInfo != null && "PLAYING".equals(playerInfo.state)); - - isPaused = (playerInfo != null && "PAUSED".equals(playerInfo.state)); - synchronized (progressLock) { - Boolean showTime = null; - Long mediaLength = null; - Long mediaProgress = null; - if (progress != null) { - showTime = progress.showTiming; - mediaLength = progress.mediaLength; - mediaProgress = progress.mediaProgress; + if (musicProviderId.startsWith("IHEARTRADIO")) { + musicProviderId = "I_HEART_RADIO"; } - if (showTime != null && showTime && mediaProgress != null && mediaLength != null) { - mediaProgressMs = mediaProgress * 1000; - mediaLengthMs = mediaLength * 1000; - mediaStartMs = System.currentTimeMillis() - mediaProgressMs; - if (isPlaying) { - if (updateProgressJob == null) { - updateProgressJob = scheduler.scheduleWithFixedDelay(this::updateMediaProgress, 1000, 1000, - TimeUnit.MILLISECONDS); - } - } else { - stopProgressTimer(); - } - } else { - stopProgressTimer(); - mediaProgressMs = 0; - mediaStartMs = 0; - mediaLengthMs = 0; + if (musicProviderId.startsWith("APPLE") && musicProviderId.contains("MUSIC")) { + musicProviderId = "APPLE_MUSIC"; } - updateMediaProgress(true); } + } - JsonMediaState mediaState = null; - try { - if ("AMAZON_MUSIC".equalsIgnoreCase(musicProviderId) || "TUNEIN".equalsIgnoreCase(musicProviderId)) { - mediaState = connection.getMediaState(device); - } - } catch (HttpException e) { - if (e.getCode() == 400) { - updateState(CHANNEL_RADIO_STATION_ID, StringType.EMPTY); - } else { - logger.info("getMediaState fails", e); - } - } catch (IOException | URISyntaxException | InterruptedException e) { - logger.info("getMediaState fails", e); - } + // check playing + isPlaying = "PLAYING".equals(playerInfo.state); + isPaused = "PAUSED".equals(playerInfo.state); - // handle music provider id - if (provider != null && isPlaying) { - if (musicProviderId != null) { - this.musicProviderId = musicProviderId; - } - } + if (isPlaying) { + currentlyPlayingQueueId = playerInfo.queueId; + } - // handle amazon music - String amazonMusicTrackId = ""; - String amazonMusicPlayListId = ""; - boolean amazonMusic = false; - if (mediaState != null) { - String contentId = mediaState.contentId; - if (isPlaying && "CLOUD_PLAYER".equals(mediaState.providerId) && contentId != null - && !contentId.isEmpty()) { - amazonMusicTrackId = contentId; - lastKnownAmazonMusicId = amazonMusicTrackId; - amazonMusic = true; + synchronized (progressLock) { + if (isPlaying) { + if (progress != null) { + mediaProgressMs = progress.mediaProgress * timeFactor; + mediaLengthMs = progress.mediaLength * timeFactor; + mediaStartMs = System.currentTimeMillis() - mediaProgressMs; + } + if (updateProgressJob == null) { + updateProgressJob = scheduler.scheduleWithFixedDelay(() -> updateMediaProgress(false), 1000, 1000, + TimeUnit.MILLISECONDS); } + } else { + stopProgressTimer(); + mediaProgressMs = 0; + mediaStartMs = 0; + mediaLengthMs = 0; } + updateMediaProgress(true); + } - // handle bluetooth - String bluetoothMAC = ""; - String bluetoothDeviceName = ""; - boolean bluetoothIsConnected = false; - if (bluetoothState != null) { - this.bluetoothState = bluetoothState; - for (PairedDevice paired : bluetoothState.getPairedDeviceList()) { - String pairedAddress = paired.address; - if (paired.connected && pairedAddress != null) { - bluetoothIsConnected = true; - bluetoothMAC = pairedAddress; - bluetoothDeviceName = paired.friendlyName; - if (bluetoothDeviceName == null || bluetoothDeviceName.isEmpty()) { - bluetoothDeviceName = pairedAddress; - } - break; - } - } + // handle music provider id + if (musicProviderId != null && isPlaying) { + this.musicProviderId = musicProviderId; + } + // handle title, subtitle, imageUrl + String title = ""; + String subTitle1 = ""; + String subTitle2 = ""; + String imageUrl = ""; + if (infoText != null) { + if (infoText.title != null) { + title = infoText.title; } - if (!bluetoothMAC.isEmpty()) { - lastKnownBluetoothMAC = bluetoothMAC; + if (infoText.subText1 != null) { + subTitle1 = infoText.subText1; } - // handle radio - boolean isRadio = false; - String radioStationId = ""; - if (mediaState != null) { - radioStationId = Objects.requireNonNullElse(mediaState.radioStationId, ""); - if (!radioStationId.isEmpty()) { - lastKnownRadioStationId = radioStationId; - if ("TUNEIN".equalsIgnoreCase(musicProviderId)) { - isRadio = true; - if (!"PLAYING".equals(mediaState.currentState)) { - radioStationId = ""; - } - } - } + if (infoText.subText2 != null) { + subTitle2 = infoText.subText2; } + } + if (mainArt != null) { + if (mainArt.url != null) { + imageUrl = mainArt.url; + } + } - // handle title, subtitle, imageUrl - String title = ""; - String subTitle1 = ""; - String subTitle2 = ""; - String imageUrl = ""; - if (infoText != null) { - if (infoText.title != null) { - title = infoText.title; - } - if (infoText.subText1 != null) { - subTitle1 = infoText.subText1; - } + // handle provider + String providerDisplayName = ""; + if (provider != null) { + providerDisplayName = Objects.requireNonNullElse(provider.providerDisplayName, providerDisplayName); + String providerName = provider.providerName; + if (isNotBlank(providerName) && providerDisplayName.isEmpty()) { + providerDisplayName = providerName; + } + } - if (infoText.subText2 != null) { - subTitle2 = infoText.subText2; - } + // handle volume + if (!sequenceNodeRunning) { + Integer volume = null; + PlayerStateVolumeTO volumeInfo = playerInfo.volume; + if (volumeInfo != null) { + volume = volumeInfo.volume; } - if (mainArt != null) { - if (mainArt.url != null) { - imageUrl = mainArt.url; - } + if (volume != null && volume > 0) { + lastKnownVolume = volume; + updateState(CHANNEL_VOLUME, new PercentType(volume)); } - if (mediaState != null) { - List queueEntries = Objects.requireNonNullElse(mediaState.queue, List.of()); - if (!queueEntries.isEmpty()) { - QueueEntry entry = queueEntries.get(0); - if (isRadio) { - if ((imageUrl == null || imageUrl.isEmpty()) && entry.imageURL != null) { - imageUrl = entry.imageURL; - } - if ((subTitle1 == null || subTitle1.isEmpty()) && entry.radioStationSlogan != null) { - subTitle1 = entry.radioStationSlogan; - } - if ((subTitle2 == null || subTitle2.isEmpty()) && entry.radioStationLocation != null) { - subTitle2 = entry.radioStationLocation; - } - } + } - } - } + // Update states + updateState(CHANNEL_MUSIC_PROVIDER_ID, new StringType(musicProviderId)); + updateState(CHANNEL_PROVIDER_DISPLAY_NAME, new StringType(providerDisplayName)); + updateState(CHANNEL_PLAYER, isPlaying ? PlayPauseType.PLAY : PlayPauseType.PAUSE); + updateState(CHANNEL_IMAGE_URL, new StringType(imageUrl)); + updateState(CHANNEL_TITLE, new StringType(title)); + updateState(CHANNEL_SUBTITLE1, new StringType(subTitle1)); + updateState(CHANNEL_SUBTITLE2, new StringType(subTitle2)); + } - // handle provider - String providerDisplayName = ""; - if (provider != null) { - if (provider.providerDisplayName != null) { - providerDisplayName = Objects.requireNonNullElse(provider.providerDisplayName, providerDisplayName); - } - String providerName = provider.providerName; - if (providerName != null && !providerName.isEmpty() && providerDisplayName.isEmpty()) { - providerDisplayName = provider.providerName; - } - } + public void updateState(DeviceTO device, @Nullable BluetoothStateTO bluetoothState, + @Nullable DeviceNotificationStateTO deviceNotificationState, + @Nullable AscendingAlarmModelTO ascendingAlarmModel, + @Nullable DoNotDisturbDeviceStatusTO doNotDisturbDeviceStatus, + @Nullable List musicProviders) { + try { + waitingForUpdate.set(false); + logger.debug("Handle updateState {}", this.getThing().getUID()); - // handle volume - Integer volume = null; - if (!connection.isSequenceNodeQueueRunning()) { - if (mediaState != null) { - volume = mediaState.volume; - } - if (playerInfo != null && volume == null) { - Volume volumnInfo = playerInfo.volume; - if (volumnInfo != null) { - volume = volumnInfo.volume; - } - } - if (volume != null && volume > 0) { - lastKnownVolume = volume; - } - if (volume == null) { - volume = lastKnownVolume; - } + if (deviceNotificationState != null) { + int notificationVolumeLevel = deviceNotificationState.volumeLevel; + updateState(CHANNEL_NOTIFICATION_VOLUME, new PercentType(notificationVolumeLevel)); } - // Update states - if (updateRemind && currentNotifcationUpdateTimer == null) { - updateRemind = false; - updateState(CHANNEL_REMIND, StringType.EMPTY); + + if (ascendingAlarmModel != null) { + boolean ascendingAlarm = ascendingAlarmModel.ascendingAlarmEnabled; + updateState(CHANNEL_ASCENDING_ALARM, OnOffType.from(ascendingAlarm)); } - if (updateAlarm && currentNotifcationUpdateTimer == null) { - updateAlarm = false; - updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY); + + if (doNotDisturbDeviceStatus != null) { + boolean doNotDisturb = doNotDisturbDeviceStatus.enabled; + updateState(CHANNEL_DO_NOT_DISTURB, OnOffType.from(doNotDisturb)); } - if (updateRoutine) { - updateRoutine = false; - updateState(CHANNEL_START_ROUTINE, StringType.EMPTY); + + if (musicProviders != null) { + createMusicProviderStateDescription(musicProviders); } - if (updateTextToSpeech) { - updateTextToSpeech = false; - updateState(CHANNEL_TEXT_TO_SPEECH, StringType.EMPTY); + + if (!setDeviceAndUpdateThingStatus(device, null)) { + logger.debug("Handle updateState {} aborted: Not online", this.getThing().getUID()); + return; } - if (updateTextCommand) { - updateTextCommand = false; - updateState(CHANNEL_TEXT_COMMAND, StringType.EMPTY); + + if (disableUpdate) { + logger.debug("Handle updateState {} aborted: Disabled", this.getThing().getUID()); + return; } - if (updatePlayMusicVoiceCommand) { - updatePlayMusicVoiceCommand = false; - updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, StringType.EMPTY); + + Connection connection = findConnection().orElse(null); + if (connection == null) { + return; } - if (updateStartCommand) { - updateStartCommand = false; - updateState(CHANNEL_START_COMMAND, StringType.EMPTY); + + if (lastKnownEqualizer == null) { + updateEqualizerState(); } - updateState(CHANNEL_MUSIC_PROVIDER_ID, new StringType(musicProviderId)); - updateState(CHANNEL_AMAZON_MUSIC_TRACK_ID, new StringType(amazonMusicTrackId)); - updateState(CHANNEL_AMAZON_MUSIC, OnOffType.from(isPlaying && amazonMusic)); - updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID, new StringType(amazonMusicPlayListId)); - updateState(CHANNEL_RADIO_STATION_ID, new StringType(radioStationId)); - updateState(CHANNEL_RADIO, OnOffType.from(isPlaying && isRadio)); - updateState(CHANNEL_PROVIDER_DISPLAY_NAME, new StringType(providerDisplayName)); - updateState(CHANNEL_PLAYER, isPlaying ? PlayPauseType.PLAY : PlayPauseType.PAUSE); - updateState(CHANNEL_IMAGE_URL, new StringType(imageUrl)); - updateState(CHANNEL_TITLE, new StringType(title)); - if (volume != null) { - updateState(CHANNEL_VOLUME, new PercentType(volume)); + try { + PlayerStateTO playerState = connection.getPlayerState(device); + updateMediaPlayerState(playerState.playerInfo, connection.isSequenceNodeQueueRunning(), 1000); + } catch (ConnectionException e) { + // ignored } - updateState(CHANNEL_SUBTITLE1, new StringType(subTitle1)); - updateState(CHANNEL_SUBTITLE2, new StringType(subTitle2)); + + // handle bluetooth if (bluetoothState != null) { + String bluetoothMAC = ""; + String bluetoothDeviceName = ""; + boolean bluetoothIsConnected = false; + for (BluetoothPairedDeviceTO paired : bluetoothState.pairedDeviceList) { + String pairedAddress = paired.address; + if (paired.connected && pairedAddress != null) { + bluetoothIsConnected = true; + bluetoothMAC = pairedAddress; + lastKnownBluetoothMAC = pairedAddress; + bluetoothDeviceName = paired.friendlyName; + if (bluetoothDeviceName == null || bluetoothDeviceName.isEmpty()) { + bluetoothDeviceName = pairedAddress; + } + break; + } + } + createBluetoothMACStateDescription(bluetoothState); updateState(CHANNEL_BLUETOOTH, OnOffType.from(bluetoothIsConnected)); updateState(CHANNEL_BLUETOOTH_MAC, new StringType(bluetoothMAC)); updateState(CHANNEL_BLUETOOTH_DEVICE_NAME, new StringType(bluetoothDeviceName)); } - - updateState(CHANNEL_ASCENDING_ALARM, - ascendingAlarm != null ? OnOffType.from(ascendingAlarm) : UnDefType.UNDEF); - - final Integer notificationVolumeLevel = this.notificationVolumeLevel; - if (notificationVolumeLevel != null) { - updateState(CHANNEL_NOTIFICATION_VOLUME, new PercentType(notificationVolumeLevel)); - } else { - updateState(CHANNEL_NOTIFICATION_VOLUME, UnDefType.UNDEF); - } - } catch (Exception e) { + } catch (RuntimeException e) { this.logger.debug("Handle updateState {} failed: {}", this.getThing().getUID(), e.getMessage(), e); - disableUpdate = false; - throw e; // Rethrow same exception } } @@ -1138,48 +898,24 @@ private void updateEqualizerState() { return; } - Connection connection = findConnection(); - if (connection == null) { - return; - } - Device device = findDevice(); + DeviceTO device = this.device; if (device == null) { return; + } - Integer bass = null; - Integer midrange = null; - Integer treble = null; - try { - JsonEqualizer equalizer = connection.getEqualizer(device); - if (equalizer != null) { - bass = equalizer.bass; - midrange = equalizer.mid; - treble = equalizer.treble; - } - this.lastKnownEqualizer = equalizer; - } catch (IOException | URISyntaxException | HttpException | ConnectionException | InterruptedException e) { - logger.debug("Get equalizer failes", e); - return; - } - if (bass != null) { - updateState(CHANNEL_EQUALIZER_BASS, new DecimalType(bass)); - } - if (midrange != null) { - updateState(CHANNEL_EQUALIZER_MIDRANGE, new DecimalType(midrange)); - } - if (treble != null) { - updateState(CHANNEL_EQUALIZER_TREBLE, new DecimalType(treble)); - } - } - private void updateMediaProgress() { - updateMediaProgress(false); + findConnection().flatMap(connection -> connection.getEqualizer(device)).ifPresent(equalizer -> { + updateState(CHANNEL_EQUALIZER_BASS, new DecimalType(equalizer.bass)); + updateState(CHANNEL_EQUALIZER_MIDRANGE, new DecimalType(equalizer.mid)); + updateState(CHANNEL_EQUALIZER_TREBLE, new DecimalType(equalizer.treble)); + this.lastKnownEqualizer = equalizer; + }); } private void updateMediaProgress(boolean updateMediaLength) { synchronized (progressLock) { - if (mediaStartMs > 0) { - long currentPlayTimeMs = isPlaying ? System.currentTimeMillis() - mediaStartMs : mediaProgressMs; + if (isPlaying && mediaStartMs > 0) { + long currentPlayTimeMs = System.currentTimeMillis() - mediaStartMs; if (mediaLengthMs > 0) { int progressPercent = (int) Math.min(100, Math.round((double) currentPlayTimeMs / (double) mediaLengthMs * 100)); @@ -1202,73 +938,129 @@ private void updateMediaProgress(boolean updateMediaLength) { } } - public void handlePushActivity(Activity pushActivity) { - if ("DISCARDED_NON_DEVICE_DIRECTED_INTENT".equals(pushActivity.activityStatus)) { + public synchronized void handlePushActivity(CustomerHistoryRecordTO customerHistoryRecord) { + long recordTimestamp = customerHistoryRecord.timestamp; + if (recordTimestamp <= lastCustomerHistoryRecordTimestamp) { return; } - Description description = pushActivity.parseDescription(); - String firstUtteranceId = description.firstUtteranceId; - if (firstUtteranceId == null || firstUtteranceId.isEmpty() - || firstUtteranceId.toLowerCase().startsWith("textclient:")) { - return; - } - String firstStreamId = description.firstStreamId; - if (firstStreamId == null || firstStreamId.isEmpty()) { - return; + lastCustomerHistoryRecordTimestamp = recordTimestamp; + List voiceHistoryRecordItems = customerHistoryRecord.voiceHistoryRecordItems; + for (CustomerHistoryRecordVoiceTO voiceHistoryRecordItem : voiceHistoryRecordItems) { + String recordItemType = voiceHistoryRecordItem.recordItemType; + if ("CUSTOMER_TRANSCRIPT".equals(recordItemType) || "ASR_REPLACEMENT_TEXT".equals(recordItemType)) { + String customerTranscript = voiceHistoryRecordItem.transcriptText; + if (!customerTranscript.isEmpty()) { + // REMOVE WAKE WORD + String wakeWordPrefix = this.wakeWord; + if (wakeWordPrefix != null + && customerTranscript.toLowerCase().startsWith(wakeWordPrefix.toLowerCase())) { + customerTranscript = customerTranscript.substring(wakeWordPrefix.length()).trim(); + // STOP IF WAKE WORD ONLY + if (customerTranscript.isEmpty()) { + return; + } + } + updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(customerTranscript)); + } + } else if ("ALEXA_RESPONSE".equals(recordItemType) || "TTS_REPLACEMENT_TEXT".equals(recordItemType)) { + String alexaResponse = voiceHistoryRecordItem.transcriptText; + if (alexaResponse != null && !alexaResponse.isEmpty()) { + updateState(CHANNEL_LAST_SPOKEN_TEXT, new StringType(alexaResponse)); + } + } } - String spokenText = description.summary; - if (spokenText != null && !spokenText.isEmpty()) { - // remove wake word - String wakeWordPrefix = this.wakeWord; - if (wakeWordPrefix != null) { - wakeWordPrefix += " "; - if (spokenText.toLowerCase().startsWith(wakeWordPrefix.toLowerCase())) { - spokenText = spokenText.substring(wakeWordPrefix.length()); + } + + public void handleNowPlayingUpdated(PlayerStateInfoTO playerState) { + findConnection().ifPresent(connection -> { + if (currentlyPlayingQueueId.equals(playerState.queueId)) { + // update when the queueId is the same + updateMediaPlayerState(playerState, connection.isSequenceNodeQueueRunning(), 1); + } + }); + } + + public void updateMediaSessions() { + findConnection().ifPresent(connection -> { + DeviceTO device = this.device; + if (device == null || !isPlaying) { + return; + } + List mediaSessions = connection.getMediaSessions(device); + for (MediaSessionTO mediaSession : mediaSessions) { + if (findIn(mediaSession.endpointList, e -> e.id.deviceSerialNumber, device.serialNumber).isPresent()) { + updateMediaPlayerState(mediaSession.nowPlayingData, connection.isSequenceNodeQueueRunning(), 1000); } } + }); + } - if (lastSpokenText.isEmpty() || lastSpokenText.equals(spokenText)) { - updateState(CHANNEL_LAST_VOICE_COMMAND, StringType.EMPTY); + private void refreshAudioPlayerState() { + findConnection().ifPresent(connection -> { + try { + DeviceTO device = this.device; + if (device != null) { + PlayerStateTO playerState = connection.getPlayerState(device); + updateMediaPlayerState(playerState.playerInfo, connection.isSequenceNodeQueueRunning(), 1000); + } + } catch (ConnectionException ignored) { } - lastSpokenText = spokenText; - updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(spokenText)); - } + }); } public void handlePushCommand(String command, String payload) { this.logger.debug("Handle push command {}", command); + Connection connection = this.findConnection().orElse(null); + switch (command) { case "PUSH_VOLUME_CHANGE": - JsonCommandPayloadPushVolumeChange volumeChange = Objects - .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushVolumeChange.class)); - Connection connection = this.findConnection(); - Integer volumeSetting = volumeChange.volumeSetting; - Boolean muted = volumeChange.isMuted; - if (muted != null && muted) { + PushVolumeChangeTO volumeChange = Objects + .requireNonNull(gson.fromJson(payload, PushVolumeChangeTO.class)); + + if (volumeChange.isMuted) { updateState(CHANNEL_VOLUME, new PercentType(0)); } - if (volumeSetting != null && connection != null && !connection.isSequenceNodeQueueRunning()) { - lastKnownVolume = volumeSetting; + if (connection != null && !connection.isSequenceNodeQueueRunning()) { + lastKnownVolume = volumeChange.volumeSetting; updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume)); } break; case "PUSH_EQUALIZER_STATE_CHANGE": - updateEqualizerState(); + PushEqualizerStateChangeTO equalizerStateChange = Objects + .requireNonNull(gson.fromJson(payload, PushEqualizerStateChangeTO.class)); + updateState(CHANNEL_EQUALIZER_BASS, new DecimalType(equalizerStateChange.bass)); + updateState(CHANNEL_EQUALIZER_MIDRANGE, new DecimalType(equalizerStateChange.midrange)); + updateState(CHANNEL_EQUALIZER_TREBLE, new DecimalType(equalizerStateChange.treble)); + break; + case "PUSH_AUDIO_PLAYER_STATE": + PushAudioPlayerStateTO audioPlayerState = Objects + .requireNonNull(gson.fromJson(payload, PushAudioPlayerStateTO.class)); + // FINISHED is emitted when the track finished, but the player continues with the next track + // PLAYING is emitted when a track starts (either first nextAlarmTime or next track) + // INTERRUPTED is emitted when the player finally stops + if (audioPlayerState.audioPlayerState == INTERRUPTED + || (!isPlaying && audioPlayerState.audioPlayerState == PLAYING) + || ("SPOTIFY".equals(musicProviderId))) { + // we only need to update the state when the player stops or starts, not on track changes + // except for spotify + refreshAudioPlayerState(); + } + break; + case "PUSH_MEDIA_QUEUE_CHANGE": + // update the media state with a request to get the new queue id + refreshAudioPlayerState(); break; default: - AccountHandler account = this.account; - Device device = this.device; - if (account != null && device != null) { + DeviceTO device = this.device; + if (device != null) { this.disableUpdate = false; - updateState(account, device, null, null, null, null, null, null); + updateState(device, null, null, null, null, null); } } } - public void updateNotifications(ZonedDateTime currentTime, ZonedDateTime now, - @Nullable JsonCommandPayloadPushNotificationChange pushPayload, - List notifications) { - Device device = this.device; + public void updateNotifications(List notifications) { + DeviceTO device = this.device; if (device == null) { return; } @@ -1277,53 +1069,30 @@ public void updateNotifications(ZonedDateTime currentTime, ZonedDateTime now, ZonedDateTime nextAlarm = null; ZonedDateTime nextMusicAlarm = null; ZonedDateTime nextTimer = null; - for (JsonNotificationResponse notification : notifications) { - if (Objects.equals(notification.deviceSerialNumber, device.serialNumber)) { - // notification for this device - if ("ON".equals(notification.status)) { - if ("Reminder".equals(notification.type)) { - String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString(); - String date = notification.originalDate != null ? notification.originalDate - : ZonedDateTime.now().toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE); - String time = notification.originalTime != null ? notification.originalTime : "00:00:00"; - ZonedDateTime alarmTime = ZonedDateTime.parse(date + "T" + time + offset, - DateTimeFormatter.ISO_DATE_TIME); - String recurringPattern = notification.recurringPattern; - if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) { - continue; // Ignore recurring entry if alarm time is before now - } - if (nextReminder == null || alarmTime.isBefore(nextReminder)) { - nextReminder = alarmTime; - } - } else if ("Timer".equals(notification.type)) { - // use remaining time - ZonedDateTime alarmTime = currentTime.plus(notification.remainingTime, ChronoUnit.MILLIS); - if (nextTimer == null || alarmTime.isBefore(nextTimer)) { - nextTimer = alarmTime; - } - } else if ("Alarm".equals(notification.type)) { - String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString(); - ZonedDateTime alarmTime = ZonedDateTime - .parse(notification.originalDate + "T" + notification.originalTime + offset); - String recurringPattern = notification.recurringPattern; - if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) { - continue; // Ignore recurring entry if alarm time is before now + for (Notification notification : notifications) { + if (Objects.equals(notification.deviceSerial(), device.serialNumber)) { + switch (notification.type()) { + case "Reminder": + if (nextReminder == null || notification.nextAlarmTime().isBefore(nextReminder)) { + nextReminder = notification.nextAlarmTime(); } - if (nextAlarm == null || alarmTime.isBefore(nextAlarm)) { - nextAlarm = alarmTime; + break; + case "Timer": + if (nextTimer == null || notification.nextAlarmTime().isBefore(nextTimer)) { + nextTimer = notification.nextAlarmTime(); } - } else if ("MusicAlarm".equals(notification.type)) { - String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString(); - ZonedDateTime alarmTime = ZonedDateTime - .parse(notification.originalDate + "T" + notification.originalTime + offset); - String recurringPattern = notification.recurringPattern; - if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) { - continue; // Ignore recurring entry if alarm time is before now + break; + case "Alarm": + if (nextAlarm == null || notification.nextAlarmTime().isBefore(nextAlarm)) { + nextAlarm = notification.nextAlarmTime(); } - if (nextMusicAlarm == null || alarmTime.isBefore(nextMusicAlarm)) { - nextMusicAlarm = alarmTime; + break; + case "MusicAlarm": + if (nextMusicAlarm == null || notification.nextAlarmTime().isBefore(nextMusicAlarm)) { + nextMusicAlarm = notification.nextAlarmTime(); } - } + break; + default: } } } @@ -1336,7 +1105,8 @@ public void updateNotifications(ZonedDateTime currentTime, ZonedDateTime now, } @Override - public void updateChannelState(String channelId, State state) { - updateState(channelId, state); + protected void updateState(String channelId, State state) { + stateCache.put(channelId, () -> state); + super.updateState(channelId, state); } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/FlashBriefingProfileHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/FlashBriefingProfileHandler.java index 94347aa894db3..8ede5c91cab5c 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/FlashBriefingProfileHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/FlashBriefingProfileHandler.java @@ -12,16 +12,19 @@ */ package org.openhab.binding.amazonechocontrol.internal.handler; +import static org.eclipse.jetty.util.StringUtil.isBlank; import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*; +import java.util.List; import java.util.Map; -import java.util.concurrent.ScheduledFuture; +import java.util.Objects; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.DeviceTO; +import org.openhab.binding.amazonechocontrol.internal.dto.EnabledFeedTO; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; import org.openhab.core.storage.Storage; @@ -29,12 +32,17 @@ 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.ThingStatusInfo; 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; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + /** * The {@link FlashBriefingProfileHandler} is responsible for storing and loading of a flash briefing configuration * @@ -42,51 +50,72 @@ */ @NonNullByDefault public class FlashBriefingProfileHandler extends BaseThingHandler { + @SuppressWarnings("unchecked") + public static final TypeToken> LIST_TYPE_TOKEN = (TypeToken>) TypeToken + .getParameterized(List.class, EnabledFeedTO.class); + public static final String ENABLED_FEEDS = "enabledFeeds"; private final Logger logger = LoggerFactory.getLogger(FlashBriefingProfileHandler.class); + private final Gson gson; - @Nullable - AccountHandler accountHandler; - Storage stateStorage; - boolean updatePlayOnDevice = true; - String currentConfigurationJson = ""; - private @Nullable ScheduledFuture updateStateJob; + private @Nullable AccountHandler accountHandler; + private final Storage stateStorage; + private List ourFeeds = List.of(); - public FlashBriefingProfileHandler(Thing thing, Storage storage) { + public FlashBriefingProfileHandler(Thing thing, Storage storage, Gson gson) { super(thing); this.stateStorage = storage; - } - - public @Nullable AccountHandler findAccountHandler() { - return this.accountHandler; + this.gson = gson; } @Override public void initialize() { - updatePlayOnDevice = true; - logger.info("{} initialized", getClass().getSimpleName()); - if (!this.currentConfigurationJson.isEmpty()) { - updateStatus(ThingStatus.ONLINE); - } else { - updateStatus(ThingStatus.UNKNOWN); - Bridge bridge = this.getBridge(); - if (bridge != null) { - AccountHandler account = (AccountHandler) bridge.getHandler(); - if (account != null) { - account.addFlashBriefingProfileHandler(this); - } + @SuppressWarnings("unchecked") + List restoredFeeds = (List) stateStorage.get(ENABLED_FEEDS); + if (restoredFeeds == null) { + // convert from old + String configurationJson = (String) stateStorage.get("configurationJson"); + if (!isBlank(configurationJson)) { + restoredFeeds = Objects.requireNonNullElse(gson.fromJson(configurationJson, LIST_TYPE_TOKEN), + List.of()); + stateStorage.put("enabledFeeds", restoredFeeds); + } else { + restoredFeeds = List.of(); } } + + if (restoredFeeds.isEmpty()) { + saveCurrentProfile(); + } else { + ourFeeds = restoredFeeds; + } + + Bridge bridge = this.getBridge(); + if (bridge != null) { + accountHandler = (AccountHandler) bridge.getHandler(); + } + if (accountHandler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Bridge handler not found."); + return; + } + + AccountHandler handler = this.accountHandler; + if (handler != null) { + bridgeStatusChanged(handler.getThing().getStatusInfo()); + } } @Override - public void dispose() { - ScheduledFuture updateStateJob = this.updateStateJob; - this.updateStateJob = null; - if (updateStateJob != null) { - updateStateJob.cancel(false); + public void bridgeStatusChanged(ThingStatusInfo bridgeStatus) { + if (bridgeStatus.getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + return; + } + if (ourFeeds.isEmpty()) { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Waiting for feed configuration"); + } else { + updateStatus(ThingStatus.ONLINE); } - super.dispose(); } @Override @@ -95,104 +124,63 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (accountHandler == null) { return; } - int waitForUpdate = -1; - ScheduledFuture updateStateJob = this.updateStateJob; - this.updateStateJob = null; - if (updateStateJob != null) { - updateStateJob.cancel(false); - } String channelId = channelUID.getId(); if (command instanceof RefreshType) { - waitForUpdate = 0; - } - if (channelId.equals(CHANNEL_SAVE)) { - if (command.equals(OnOffType.ON)) { - saveCurrentProfile(accountHandler); - waitForUpdate = 500; + accountHandler.updateFlashBriefingHandlers(); + } else if (channelId.equals(CHANNEL_SAVE) && command.equals(OnOffType.ON)) { + saveCurrentProfile(); + accountHandler.updateFlashBriefingHandlers(); + } else if (channelId.equals(CHANNEL_ACTIVE) && command.equals(OnOffType.ON)) { + if (!ourFeeds.isEmpty()) { + accountHandler.setEnabledFlashBriefing(ourFeeds); } - } - if (channelId.equals(CHANNEL_ACTIVE)) { - if (command.equals(OnOffType.ON)) { - String currentConfigurationJson = this.currentConfigurationJson; - if (!currentConfigurationJson.isEmpty()) { - accountHandler.setEnabledFlashBriefingsJson(currentConfigurationJson); - updateState(CHANNEL_ACTIVE, OnOffType.ON); - waitForUpdate = 500; + } else if (channelId.equals(CHANNEL_PLAY_ON_DEVICE) && command instanceof StringType) { + String deviceSerialOrName = command.toFullString(); + if (!ourFeeds.isEmpty()) { + DeviceTO device = accountHandler.findDeviceBySerialOrName(deviceSerialOrName); + if (device == null) { + logger.warn("Device '{}' not found", deviceSerialOrName); + return; } - } - } - if (channelId.equals(CHANNEL_PLAY_ON_DEVICE)) { - if (command instanceof StringType) { - String deviceSerialOrName = command.toFullString(); - String currentConfigurationJson = this.currentConfigurationJson; - if (!currentConfigurationJson.isEmpty()) { - String old = accountHandler.getEnabledFlashBriefingsJson(); - accountHandler.setEnabledFlashBriefingsJson(currentConfigurationJson); - Device device = accountHandler.findDeviceJsonBySerialOrName(deviceSerialOrName); - if (device == null) { - logger.warn("Device '{}' not found", deviceSerialOrName); - } else { - @Nullable - Connection connection = accountHandler.findConnection(); - if (connection == null) { - logger.warn("Connection for '{}' not found", accountHandler.getThing().getUID().getId()); - } else { - connection.executeSequenceCommand(device, "Alexa.FlashBriefing.Play", Map.of()); - - scheduler.schedule(() -> accountHandler.setEnabledFlashBriefingsJson(old), 1000, - TimeUnit.MILLISECONDS); - - updateState(CHANNEL_ACTIVE, OnOffType.ON); - } - } - updatePlayOnDevice = true; - waitForUpdate = 1000; + + List oldEnabledFeeds = accountHandler.getEnabledFlashBriefings(); + accountHandler.setEnabledFlashBriefing(ourFeeds); + + Connection connection = accountHandler.getConnection(); + if (!connection.isLoggedIn()) { + logger.warn("Can't execute command when account is logged out."); + } else { + connection.executeSequenceCommand(device, "Alexa.FlashBriefing.Play", Map.of()); + scheduler.schedule(() -> accountHandler.setEnabledFlashBriefing(oldEnabledFeeds), 1000, + TimeUnit.MILLISECONDS); } } } - if (waitForUpdate >= 0) { - this.updateStateJob = scheduler.schedule(() -> accountHandler.updateFlashBriefingHandlers(), waitForUpdate, - TimeUnit.MILLISECONDS); - } } - @SuppressWarnings("PMD.CompareObjectsWithEquals") - public boolean initialize(AccountHandler handler, String currentConfigurationJson) { - updateState(CHANNEL_SAVE, OnOffType.OFF); - if (updatePlayOnDevice) { - updateState(CHANNEL_PLAY_ON_DEVICE, new StringType("")); - } - if (this.accountHandler != handler) { - this.accountHandler = handler; - String configurationJson = this.stateStorage.get("configurationJson"); - if (configurationJson == null || configurationJson.isEmpty()) { - this.currentConfigurationJson = saveCurrentProfile(handler); - } else { - this.currentConfigurationJson = configurationJson; - } - if (!this.currentConfigurationJson.isEmpty()) { - updateStatus(ThingStatus.ONLINE); - - } else { - updateStatus(ThingStatus.UNKNOWN); - } - } - if (this.currentConfigurationJson.equals(currentConfigurationJson)) { - updateState(CHANNEL_ACTIVE, OnOffType.ON); - } else { - updateState(CHANNEL_ACTIVE, OnOffType.OFF); - } - return this.currentConfigurationJson.equals(currentConfigurationJson); + /** + * Update the current profile's state and check if it is the same + * + * @param configurationJson a flash briefing profile configuration + * @return {@code true} if the provided feed configuration is the same as this thing's configuration + */ + public boolean updateAndCheck(List configurationJson) { + boolean isSame = ourFeeds.equals(configurationJson); + updateState(CHANNEL_ACTIVE, OnOffType.from(isSame)); + return isSame; } - private String saveCurrentProfile(AccountHandler connection) { - String configurationJson = ""; - configurationJson = connection.getEnabledFlashBriefingsJson(); - this.currentConfigurationJson = configurationJson; - if (!configurationJson.isEmpty()) { - this.stateStorage.put("configurationJson", configurationJson); + private void saveCurrentProfile() { + AccountHandler accountHandler = this.accountHandler; + if (accountHandler == null) { + ourFeeds = List.of(); + } else { + List newFeeds = accountHandler.getEnabledFlashBriefings(); + if (!newFeeds.isEmpty()) { + ourFeeds = newFeeds; + stateStorage.put(ENABLED_FEEDS, newFeeds); + } } - return configurationJson; } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java index c5a78880c6bf5..89924091a438a 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java @@ -17,11 +17,9 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -29,18 +27,19 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroupIdentifiers; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroupIdentity; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroups.SmartHomeGroup; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeTags; -import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlCommandDescriptionProvider; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlStateDescriptionProvider; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeGroupIdentifiers; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeGroupIdentity; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeGroups.SmartHomeGroup; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeTags; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.SmartHomeBaseDevice; +import org.openhab.binding.amazonechocontrol.internal.smarthome.ChannelInfo; import org.openhab.binding.amazonechocontrol.internal.smarthome.Constants; -import org.openhab.binding.amazonechocontrol.internal.smarthome.HandlerBase; -import org.openhab.binding.amazonechocontrol.internal.smarthome.HandlerBase.ChannelInfo; -import org.openhab.binding.amazonechocontrol.internal.smarthome.HandlerBase.UpdateChannelResult; +import org.openhab.binding.amazonechocontrol.internal.smarthome.InterfaceHandler; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; @@ -49,10 +48,11 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; import org.openhab.core.thing.binding.builder.ChannelBuilder; import org.openhab.core.thing.binding.builder.ThingBuilder; -import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; +import org.openhab.core.types.CommandOption; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; import org.openhab.core.types.StateDescription; @@ -62,7 +62,6 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; -import com.google.gson.JsonNull; import com.google.gson.JsonObject; /** @@ -71,88 +70,112 @@ @NonNullByDefault public class SmartHomeDeviceHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(SmartHomeDeviceHandler.class); + private final AmazonEchoControlCommandDescriptionProvider dynamicCommandDescriptionProvider; + private final AmazonEchoControlStateDescriptionProvider dynamicStateDescriptionProvider; - private @Nullable SmartHomeBaseDevice smartHomeBaseDevice; private final Gson gson; - private final Map handlers = new HashMap<>(); + private final Map interfaceHandlers = new HashMap<>(); private final Map lastStates = new HashMap<>(); - public SmartHomeDeviceHandler(Thing thing, Gson gson) { + private @Nullable SmartHomeBaseDevice smartHomeBaseDevice; + private String deviceId = ""; + + public SmartHomeDeviceHandler(Thing thing, Gson gson, + AmazonEchoControlCommandDescriptionProvider dynamicCommandDescriptionProvider, + AmazonEchoControlStateDescriptionProvider dynamicStateDescriptionProvider) { super(thing); this.gson = gson; + this.dynamicCommandDescriptionProvider = dynamicCommandDescriptionProvider; + this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider; } public synchronized void setDeviceAndUpdateThingState(AccountHandler accountHandler, @Nullable SmartHomeBaseDevice smartHomeBaseDevice) { if (smartHomeBaseDevice == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Can't find smarthomeBaseDevice"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Can't find smartHomeBaseDevice"); return; } + boolean changed = this.smartHomeBaseDevice == null; this.smartHomeBaseDevice = smartHomeBaseDevice; Set unusedChannels = new HashSet<>(); thing.getChannels().forEach(channel -> unusedChannels.add(channel.getUID().getId())); - Set unusedHandlers = new HashSet<>(handlers.keySet()); + Set unusedHandlers = new HashSet<>(interfaceHandlers.keySet()); - Map> capabilities = new HashMap<>(); - getCapabilities(capabilities, accountHandler, smartHomeBaseDevice); + Map> capabilities = getCapabilities(accountHandler, smartHomeBaseDevice); ThingBuilder thingBuilder = editThing(); - for (String interfaceName : capabilities.keySet()) { - HandlerBase handler = handlers.get(interfaceName); + for (Map.Entry> capability : capabilities.entrySet()) { + String interfaceName = capability.getKey(); + InterfaceHandler handler = interfaceHandlers.get(interfaceName); if (handler != null) { unusedHandlers.remove(interfaceName); } else { - Function creator = Constants.HANDLER_FACTORY.get(interfaceName); + Function creator = Constants.HANDLER_FACTORY + .get(interfaceName); if (creator != null) { handler = creator.apply(this); - handlers.put(interfaceName, handler); + interfaceHandlers.put(interfaceName, handler); } } if (handler != null) { - Collection required = handler - .initialize(capabilities.getOrDefault(interfaceName, List.of())); + Collection required = handler.initialize(capability.getValue()); + ThingHandlerCallback callback = getCallback(); + if (callback == null) { + logger.warn("Trying to modify {} but no callback present.", thing.getUID()); + return; + } for (ChannelInfo channelInfo : required) { unusedChannels.remove(channelInfo.channelId); - if (addChannelToDevice(thingBuilder, channelInfo.channelId, channelInfo.itemType, - channelInfo.channelTypeUID)) { + ChannelUID channelUID = new ChannelUID(thing.getUID(), channelInfo.channelId); + Channel channel = thing.getChannel(channelUID); + if (channel == null || !channelInfo.channelTypeUID.equals(channel.getChannelTypeUID())) { + channel = addChannelToDevice(thingBuilder, callback, channelInfo); changed = true; } + + List commandOptions = handler.getCommandDescription(channel); + if (commandOptions != null) { + dynamicCommandDescriptionProvider.setCommandOptions(channelUID, commandOptions); + } + + StateDescription stateDescription = handler.getStateDescription(channel); + if (stateDescription != null) { + dynamicStateDescriptionProvider.setDescription(channelUID, stateDescription); + } } } } - unusedHandlers.forEach(handlers::remove); + unusedHandlers.forEach(interfaceHandlers::remove); if (!unusedChannels.isEmpty()) { changed = true; - unusedChannels.stream().map(id -> new ChannelUID(thing.getUID(), id)).forEach(thingBuilder::withoutChannel); + unusedChannels.forEach(channelId -> thingBuilder.withoutChannel(new ChannelUID(thing.getUID(), channelId))); } if (changed) { updateThing(thingBuilder.build()); updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Thing has changed."); - accountHandler.forceDelayedSmartHomeStateUpdate(getId()); - } - } - - public String getId() { - String id = (String) getConfig().get(DEVICE_PROPERTY_ID); - if (id == null) { - return ""; + accountHandler.forceDelayedSmartHomeStateUpdate(deviceId); } - return id; } @Override public void updateState(String channelId, State state) { - super.updateState(new ChannelUID(thing.getUID(), channelId), state); + super.updateState(channelId, state); } @Override public void initialize() { + deviceId = Objects.requireNonNullElse((String) getConfig().get(DEVICE_PROPERTY_ID), ""); + if (deviceId.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "id not set"); + return; + } + AccountHandler accountHandler = getAccountHandler(); if (accountHandler != null) { accountHandler.addSmartHomeDeviceHandler(this); @@ -162,95 +185,116 @@ public void initialize() { } } - private boolean addChannelToDevice(ThingBuilder thingBuilder, String channelId, String itemType, - ChannelTypeUID channelTypeUID) { - Channel channel = thing.getChannel(channelId); - if (channel != null) { - if (channelTypeUID.equals(channel.getChannelTypeUID()) && itemType.equals(channel.getAcceptedItemType())) { - // channel exist with the same settings - return false; - } - // channel exist with other settings, remove it first - thingBuilder.withoutChannel(channel.getUID()); + @Override + public void dispose() { + dynamicCommandDescriptionProvider.removeCommandDescriptionForThing(thing.getUID()); + dynamicStateDescriptionProvider.removeDescriptionsForThing(thing.getUID()); + } + + public String getId() { + return deviceId; + } + + private Channel addChannelToDevice(ThingBuilder thingBuilder, ThingHandlerCallback callback, + ChannelInfo channelInfo) { + ChannelUID channelUID = new ChannelUID(thing.getUID(), channelInfo.channelId); + thingBuilder.withoutChannel(channelUID); + + ChannelBuilder channelBuilder = callback.createChannelBuilder( + new ChannelUID(thing.getUID(), channelInfo.channelId), channelInfo.channelTypeUID); + String label = channelInfo.label; + if (label != null) { + channelBuilder.withLabel(label); } - thingBuilder.withChannel(ChannelBuilder.create(new ChannelUID(thing.getUID(), channelId), itemType) - .withType(channelTypeUID).build()); - return true; + Channel channel = channelBuilder.build(); + thingBuilder.withChannel(channel); + + return channel; } public void updateChannelStates(List allDevices, Map applianceIdToCapabilityStates) { - logger.trace("Updating {} with {}", allDevices, applianceIdToCapabilityStates); + logger.trace("Updating allDevices={} with states={}", allDevices, applianceIdToCapabilityStates); AccountHandler accountHandler = getAccountHandler(); SmartHomeBaseDevice smartHomeBaseDevice = this.smartHomeBaseDevice; if (smartHomeBaseDevice == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Can't find smarthomeBaseDevice"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Can't find smartHomeBaseDevice"); return; } - boolean stateFound = false; Map> mapInterfaceToStates = new HashMap<>(); - SmartHomeDevice firstDevice = null; - for (SmartHomeDevice smartHomeDevice : getSupportedSmartHomeDevices(smartHomeBaseDevice, allDevices)) { + Set smartHomeDevices = getSupportedSmartHomeDevices(smartHomeBaseDevice, allDevices); + logger.trace("Search for smartHomeBaseDevice='{}' resulted in '{}'", smartHomeBaseDevice, smartHomeDevices); + if (smartHomeDevices.isEmpty()) { + logger.debug("Did not find a supported smartHomeDevice."); + return; + } + + for (JsonSmartHomeDevice smartHomeDevice : smartHomeDevices) { String applianceId = smartHomeDevice.applianceId; + logger.trace("applianceId={}, group={}, keys={}", applianceId, smartHomeDevice.isGroup(), + applianceIdToCapabilityStates.keySet()); if (applianceId == null) { + logger.debug("applianceId is null in smartHomeDevice={}", smartHomeDevice); continue; } - JsonArray states = applianceIdToCapabilityStates.get(applianceId); - if (states != null) { - stateFound = true; - if (smartHomeBaseDevice.isGroup()) { - // for groups, store the last state of all devices - lastStates.put(applianceId, states); - } - } else { - states = lastStates.get(applianceId); - if (states == null) { - continue; - } + JsonArray states = applianceIdToCapabilityStates.getOrDefault(applianceId, + lastStates.getOrDefault(applianceId, new JsonArray())); + if (states.isEmpty()) { + logger.trace("No states array found for applianceId={}.", applianceId); + continue; } - if (firstDevice == null) { - firstDevice = smartHomeDevice; + if (smartHomeBaseDevice.isGroup()) { + // for groups, store the last state of all devices + lastStates.put(applianceId, states); } + logger.trace("Found states array={} for applianceId={}", states, applianceId); + for (JsonElement stateElement : states) { String stateJson = stateElement.getAsString(); if (stateJson.startsWith("{") && stateJson.endsWith("}")) { JsonObject state = Objects.requireNonNull(gson.fromJson(stateJson, JsonObject.class)); - String interfaceName = Objects.requireNonNullElse(state.get("namespace"), JsonNull.INSTANCE) - .getAsString(); - Objects.requireNonNull(mapInterfaceToStates.computeIfAbsent(interfaceName, k -> new ArrayList<>())) - .add(state); + JsonElement interfaceName = state.get("namespace"); + if (interfaceName != null) { + Objects.requireNonNull(mapInterfaceToStates.computeIfAbsent(interfaceName.getAsString(), + k -> new ArrayList<>())).add(state); + } } } } - for (HandlerBase handlerBase : handlers.values()) { - UpdateChannelResult result = new UpdateChannelResult(); - for (String interfaceName : handlerBase.getSupportedInterface()) { + if (mapInterfaceToStates.isEmpty()) { + logger.trace("Found no matching states."); + return; + } + logger.trace("mapInterfaceToState='{}'", mapInterfaceToStates); + + for (InterfaceHandler interfaceHandler : interfaceHandlers.values()) { + InterfaceHandler.UpdateChannelResult result = new InterfaceHandler.UpdateChannelResult(); + for (String interfaceName : interfaceHandler.getSupportedInterface()) { List stateList = mapInterfaceToStates.get(interfaceName); if (stateList != null) { try { - handlerBase.updateChannels(interfaceName, stateList, result); - } catch (Exception e) { + interfaceHandler.updateChannels(interfaceName, stateList, result); + } catch (RuntimeException e) { // We catch all exceptions, otherwise all other things are not updated! logger.debug("Updating states failed", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - e.getLocalizedMessage()); + "RuntimeException while processing updates"); } } } - if (result.needSingleUpdate && smartHomeBaseDevice instanceof SmartHomeDevice smartHomeDevice + if (result.needSingleUpdate && smartHomeBaseDevice instanceof JsonSmartHomeDevice smartHomeDevice && accountHandler != null) { - accountHandler.forceDelayedSmartHomeStateUpdate(smartHomeDevice.findId()); + String applianceId = smartHomeDevice.applianceId; + if (applianceId != null) { + accountHandler.forceDelayedSmartHomeStateUpdate(applianceId); + } } } - if (stateFound) { - updateStatus(ThingStatus.ONLINE); - } else { - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "State not found"); - } + updateStatus(ThingStatus.ONLINE); } private @Nullable AccountHandler getAccountHandler() { @@ -272,38 +316,37 @@ public void handleCommand(ChannelUID channelUID, Command command) { logger.debug("accountHandler is null in {}", thing.getUID()); return; } - Connection connection = accountHandler.findConnection(); - if (connection == null) { + Connection connection = accountHandler.getConnection(); + if (!connection.isLoggedIn()) { logger.debug("connection is null in {}", thing.getUID()); return; } try { if (command instanceof RefreshType) { - accountHandler.forceDelayedSmartHomeStateUpdate(getId()); + accountHandler.forceDelayedSmartHomeStateUpdate(deviceId); return; } SmartHomeBaseDevice smartHomeBaseDevice = this.smartHomeBaseDevice; if (smartHomeBaseDevice == null) { - logger.debug("smarthomeBaseDevice is null in {}", thing.getUID()); + logger.debug("smartHomeBaseDevice is null in {}", thing.getUID()); return; } - Set devices = getSupportedSmartHomeDevices(smartHomeBaseDevice, + Set devices = getSupportedSmartHomeDevices(smartHomeBaseDevice, accountHandler.getLastKnownSmartHomeDevices()); String channelId = channelUID.getId(); - for (String interfaceName : handlers.keySet()) { - HandlerBase handlerBase = handlers.get(interfaceName); - if (handlerBase == null || !handlerBase.hasChannel(channelId)) { + for (InterfaceHandler interfaceHandler : interfaceHandlers.values()) { + if (!interfaceHandler.hasChannel(channelId)) { continue; } - for (SmartHomeDevice smartHomeDevice : devices) { + for (JsonSmartHomeDevice smartHomeDevice : devices) { String entityId = smartHomeDevice.entityId; if (entityId == null) { continue; } accountHandler.forceDelayedSmartHomeStateUpdate(getId()); // block updates - if (handlerBase.handleCommand(connection, smartHomeDevice, entityId, + if (interfaceHandler.handleCommand(connection, smartHomeDevice, entityId, smartHomeDevice.getCapabilities(), channelUID.getId(), command)) { accountHandler.forceDelayedSmartHomeStateUpdate(getId()); // force update again to restart // update timer @@ -316,32 +359,36 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - private static void getCapabilities(Map> result, AccountHandler accountHandler, + private Map> getCapabilities(AccountHandler accountHandler, SmartHomeBaseDevice device) { - if (device instanceof SmartHomeDevice smartHomeDevice) { - for (SmartHomeCapability capability : smartHomeDevice.getCapabilities()) { + Map> capabilities = new HashMap<>(); + if (device instanceof JsonSmartHomeDevice smartHomeDevice) { + for (JsonSmartHomeCapability capability : smartHomeDevice.getCapabilities()) { String interfaceName = capability.interfaceName; if (interfaceName != null) { - Objects.requireNonNull(result.computeIfAbsent(interfaceName, name -> new ArrayList<>())) + Objects.requireNonNull(capabilities.computeIfAbsent(interfaceName, name -> new ArrayList<>())) .add(capability); } } - } - if (device instanceof SmartHomeGroup) { - for (SmartHomeDevice smartHomeDevice : getSupportedSmartHomeDevices(device, + } else if (device instanceof SmartHomeGroup) { + for (JsonSmartHomeDevice smartHomeDevice : getSupportedSmartHomeDevices(device, accountHandler.getLastKnownSmartHomeDevices())) { - getCapabilities(result, accountHandler, smartHomeDevice); + getCapabilities(accountHandler, smartHomeDevice).forEach((interfaceName, caps) -> Objects + .requireNonNull(capabilities.computeIfAbsent(interfaceName, name -> new ArrayList<>())) + .addAll(caps)); } } + + return capabilities; } - public static Set getSupportedSmartHomeDevices(@Nullable SmartHomeBaseDevice baseDevice, + public static Set getSupportedSmartHomeDevices(@Nullable SmartHomeBaseDevice baseDevice, List allDevices) { if (baseDevice == null) { - return Collections.emptySet(); + return Set.of(); } - Set result = new HashSet<>(); - if (baseDevice instanceof SmartHomeDevice smartHomeDevice) { + Set result = new HashSet<>(); + if (baseDevice instanceof JsonSmartHomeDevice smartHomeDevice) { if (smartHomeDevice.getCapabilities().stream().map(capability -> capability.interfaceName) .anyMatch(SUPPORTED_INTERFACES::contains)) { result.add(smartHomeDevice); @@ -349,7 +396,7 @@ public static Set getSupportedSmartHomeDevices(@Nullable SmartH } else { SmartHomeGroup smartHomeGroup = (SmartHomeGroup) baseDevice; for (SmartHomeBaseDevice device : allDevices) { - if (device instanceof SmartHomeDevice smartHomeDevice) { + if (device instanceof JsonSmartHomeDevice smartHomeDevice) { JsonSmartHomeTags.JsonSmartHomeTag tags = smartHomeDevice.tags; if (tags != null) { JsonSmartHomeGroupIdentity.SmartHomeGroupIdentity tagNameToValueSetMap = tags.tagNameToValueSetMap; @@ -372,15 +419,4 @@ public static Set getSupportedSmartHomeDevices(@Nullable SmartH } return result; } - - public @Nullable StateDescription findStateDescription(Channel channel, StateDescription originalStateDescription, - @Nullable Locale locale) { - String channelId = channel.getUID().getId(); - for (HandlerBase handler : handlers.values()) { - if (handler.hasChannel(channelId)) { - return handler.findStateDescription(channelId, originalStateDescription, locale); - } - } - return null; - } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonActivities.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonActivities.java deleted file mode 100644 index ab8240da19a58..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonActivities.java +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; -import java.util.Objects; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -/** - * The {@link JsonActivities} encapsulate the GSON data of the push command for push activity - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonActivities { - - public @Nullable List activities; - - public static class Activity { - public @Nullable String activityStatus; - public @Nullable Long creationTimestamp; - public @Nullable String description; - public @Nullable Object domainAttributes; - public @Nullable Object domainType; - public @Nullable Object feedbackAttributes; - public @Nullable String id; - public @Nullable String intentType; - public @Nullable String providerInfoDescription; - public @Nullable String registeredCustomerId; - public @Nullable Object sourceActiveUsers; - public @Nullable List sourceDeviceIds; - public @Nullable String utteranceId; - public @Nullable Long version; - - public List getSourceDeviceIds() { - return Objects.requireNonNullElse(sourceDeviceIds, List.of()); - } - - public static class SourceDeviceId { - public @Nullable String deviceAccountId; - public @Nullable String deviceType; - public @Nullable String serialNumber; - } - - public static class Description { - public @Nullable String summary; - public @Nullable String firstUtteranceId; - public @Nullable String firstStreamId; - } - - public Description parseDescription() { - String description = this.description; - if (description == null || description.isEmpty() || !description.startsWith("{") - || !description.endsWith("}")) { - return new Description(); - } - Gson gson = new Gson(); - try { - Description description1 = gson.fromJson(description, Description.class); - return description1 != null ? description1 : new Description(); - } catch (JsonSyntaxException e) { - return new Description(); - } - } - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAnnouncementContent.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAnnouncementContent.java deleted file mode 100644 index 5dcb15b549f24..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAnnouncementContent.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; - -/** - * The {@link JsonAnnouncementContent} encapsulate the GSON data of the sequence command AlexaAnnouncement for sending - * announcements - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonAnnouncementContent { - public String locale = ""; - public Display display; - public Speak speak; - - public JsonAnnouncementContent(Connection.AnnouncementWrapper announcement) { - display = new Display(announcement.bodyText, announcement.title); - speak = new Speak(announcement.speak); - } - - public static class Display { - public String title; - public String body; - - public Display(String body, @Nullable String title) { - this.body = body; - this.title = (title == null || title.isEmpty() ? "openHAB" : title); - } - } - - public static class Speak { - public String type; - public String value; - - public Speak(String speakText) { - type = (speakText.startsWith("") && speakText.endsWith("")) ? "ssml" : "text"; - value = speakText; - } - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAnnouncementTarget.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAnnouncementTarget.java deleted file mode 100644 index f989c9b64d73a..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAnnouncementTarget.java +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; -import java.util.stream.Collectors; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonAnnouncementTarget} encapsulate the GSON data of the sequence command AlexaAnnouncement for - * announcement target - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonAnnouncementTarget { - - public @Nullable String customerId; - public List devices; - - public JsonAnnouncementTarget(List deviceList) { - customerId = deviceList.get(0).deviceOwnerCustomerId; - devices = deviceList.stream().map(TargetDevice::new).collect(Collectors.toList()); - } - - public static class TargetDevice { - public @Nullable String deviceSerialNumber; - public @Nullable String deviceTypeId; - - public TargetDevice(JsonDevices.Device device) { - deviceSerialNumber = device.serialNumber; - deviceTypeId = device.deviceType; - } - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAscendingAlarm.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAscendingAlarm.java deleted file mode 100644 index 9cb51cf2c4638..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAscendingAlarm.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonAscendingAlarm} encapsulate the GSON data of the /api/ascending-alarm response - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonAscendingAlarm { - - public @Nullable List ascendingAlarmModelList; - - public static class AscendingAlarmModel { - public @Nullable Boolean ascendingAlarmEnabled; - public @Nullable String deviceSerialNumber; - public @Nullable String deviceType; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAutomation.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAutomation.java deleted file mode 100644 index df648bee01a45..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAutomation.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; -import java.util.TreeMap; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonAutomation} encapsulate the GSON data of automation query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonAutomation { - public @Nullable String automationId; - public @Nullable String name; - public @Nullable List triggers; - public @Nullable TreeMap sequence; - public @Nullable String status; - public long creationTimeEpochMillis; - public long lastUpdatedTimeEpochMillis; - - public static class Trigger { - public @Nullable Payload payload; - public @Nullable String id; - public @Nullable String type; - } - - public static class Payload { - public @Nullable String customerId; - public @Nullable String utterance; - public @Nullable String locale; - public @Nullable String marketplaceId; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java deleted file mode 100644 index cdf0c85c6023b..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; -import java.util.Objects; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; - -/** - * The {@link JsonBluetoothStates} encapsulate the GSON data of bluetooth state - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonBluetoothStates { - - public @Nullable BluetoothState findStateByDevice(@Nullable Device device) { - if (device == null) { - return null; - } - @Nullable - BluetoothState @Nullable [] bluetoothStates = this.bluetoothStates; - if (bluetoothStates == null) { - return null; - } - for (BluetoothState state : bluetoothStates) { - if (state != null && Objects.equals(state.deviceSerialNumber, device.serialNumber)) { - return state; - } - } - return null; - } - - public @Nullable BluetoothState @Nullable [] bluetoothStates; - - public static class PairedDevice { - public @Nullable String address; - public boolean connected; - public @Nullable String deviceClass; - public @Nullable String friendlyName; - public @Nullable List profiles; - } - - public static class BluetoothState { - public @Nullable String deviceSerialNumber; - public @Nullable String deviceType; - public @Nullable String friendlyName; - public boolean gadgetPaired; - public boolean online; - public @Nullable List pairedDeviceList; - - public List getPairedDeviceList() { - return Objects.requireNonNullElse(pairedDeviceList, List.of()); - } - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBootstrapResult.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBootstrapResult.java deleted file mode 100644 index 6b585ca8e8ed1..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBootstrapResult.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonBluetoothStates} encapsulate the bootstrap result - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonBootstrapResult { - - public @Nullable Authentication authentication; - - public static class Authentication { - public boolean authenticated; - public @Nullable Boolean canAccessPrimeMusicContent; - public @Nullable String customerEmail; - public @Nullable String customerId; - public @Nullable String customerName; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColorTemperature.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColorTemperature.java deleted file mode 100644 index 18aa7911f129c..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColorTemperature.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * @author Lukas Knoeller - Initial contribution - */ -@NonNullByDefault -public class JsonColorTemperature { - public @Nullable String temperatureName; - - public JsonColorTemperature(String temperature) { - temperatureName = temperature; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushActivity.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushActivity.java deleted file mode 100644 index bf2ed4a834e5e..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushActivity.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonCommandPayloadPushActivity} encapsulate the GSON data of the push command with device information - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonCommandPayloadPushActivity { - - public @Nullable String destinationUserId; - public @Nullable Long timestamp; - - public @Nullable Key key; - - public static class Key { - public @Nullable String entryId; - public @Nullable String registeredUserId; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushAudioPlayerState.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushAudioPlayerState.java deleted file mode 100644 index c963eced16204..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushAudioPlayerState.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonCommandPayloadPushAudioPlayerState} encapsulate the GSON data of automation query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonCommandPayloadPushAudioPlayerState extends JsonCommandPayloadPushDevice { - public @Nullable String destinationUserId; - public @Nullable String mediaReferenceId; - public @Nullable Boolean error; - public @Nullable String audioPlayerState; - public @Nullable String errorMessage; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushBluetoothStateChange.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushBluetoothStateChange.java deleted file mode 100644 index 14638547f8e91..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushBluetoothStateChange.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonCommandPayloadPushBluetoothStateChange} encapsulate the GSON data of automation query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonCommandPayloadPushBluetoothStateChange extends JsonCommandPayloadPushDevice { - public @Nullable String destinationUserId; - public @Nullable String bluetoothEvent; - public @Nullable String bluetoothEventPayload; - public @Nullable Boolean bluetoothEventSuccess; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushContentFocusChange.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushContentFocusChange.java deleted file mode 100644 index 6e0d5a5a68085..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushContentFocusChange.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonCommandPayloadPushContentFocusChange} encapsulate the GSON data of automation query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonCommandPayloadPushContentFocusChange extends JsonCommandPayloadPushDevice { - public @Nullable String destinationUserId; - public @Nullable String clientId; - public @Nullable String deviceComponent; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushDevice.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushDevice.java deleted file mode 100644 index 121eabba28113..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushDevice.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonCommandPayloadPushDevice} encapsulate the GSON data of the push command with device information - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonCommandPayloadPushDevice { - - public @Nullable DopplerId dopplerId; - - public static class DopplerId { - public @Nullable String deviceSerialNumber; - public @Nullable String deviceType; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushDopllerConnectionChange.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushDopllerConnectionChange.java deleted file mode 100644 index f9a189a7e20da..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushDopllerConnectionChange.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonCommandPayloadPushDopllerConnectionChange} encapsulate the GSON data of automation query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonCommandPayloadPushDopllerConnectionChange extends JsonCommandPayloadPushDevice { - public @Nullable String destinationUserId; - public @Nullable String dopplerConnectionState; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushEqualizerChange.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushEqualizerChange.java deleted file mode 100644 index 10be6095566ca..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushEqualizerChange.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonCommandPayloadPushEqualizerChange} encapsulate the GSON data of automation query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonCommandPayloadPushEqualizerChange extends JsonCommandPayloadPushDevice { - public @Nullable String destinationUserId; - public @Nullable Integer bass; - public @Nullable Integer treble; - public @Nullable Integer midrange; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushMediaQueueChange.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushMediaQueueChange.java deleted file mode 100644 index 2743f23a46d06..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushMediaQueueChange.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonCommandPayloadPushMediaQueueChange} encapsulate the GSON data of automation query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonCommandPayloadPushMediaQueueChange extends JsonCommandPayloadPushDevice { - public @Nullable String destinationUserId; - public @Nullable String changeType; - public @Nullable String playBackOrder; - public @Nullable Boolean trackOrderChanged; - public @Nullable Object loopMode; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushNotificationChange.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushNotificationChange.java deleted file mode 100644 index a93e85f06e483..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushNotificationChange.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonCommandPayloadPushNotificationChange} encapsulate the GSON data of automation query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonCommandPayloadPushNotificationChange extends JsonCommandPayloadPushDevice { - public @Nullable String destinationUserId; - public @Nullable String eventType; - public @Nullable String notificationId; - public @Nullable Integer notificationVersion; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushVolumeChange.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushVolumeChange.java deleted file mode 100644 index 6bdadf201eee3..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonCommandPayloadPushVolumeChange.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonCommandPayloadPushVolumeChange} encapsulate the GSON data of automation query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonCommandPayloadPushVolumeChange extends JsonCommandPayloadPushDevice { - public @Nullable String destinationUserId; - public @Nullable Boolean isMuted; - public @Nullable Integer volumeSetting; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDeviceNotificationState.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDeviceNotificationState.java deleted file mode 100644 index c41a678f59ab5..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDeviceNotificationState.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonDeviceNotificationState} encapsulate the GSON data of the /api/device-notification-state response - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonDeviceNotificationState { - - public @Nullable List deviceNotificationStates; - - public static class DeviceNotificationState { - public @Nullable String deviceSerialNumber; - public @Nullable String deviceType; - public @Nullable String softwareVersion; - public @Nullable Integer volumeLevel; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java deleted file mode 100644 index 76c7ca14da3c4..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; -import java.util.Objects; -import java.util.Set; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonDevices} encapsulate the GSON data of device list - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonDevices { - - public static class Device { - public @Nullable String accountName; - public @Nullable String serialNumber; - public @Nullable String deviceOwnerCustomerId; - public @Nullable String deviceAccountId; - public @Nullable String deviceFamily; - public @Nullable String deviceType; - public @Nullable String softwareVersion; - public boolean online; - public @Nullable Set capabilities; - - public Set getCapabilities() { - return Objects.requireNonNullElse(capabilities, Set.of()); - } - - @Override - public String toString() { - return "Device{" + "accountName='" + accountName + '\'' + ", serialNumber='" + serialNumber + '\'' - + ", deviceOwnerCustomerId='" + deviceOwnerCustomerId + '\'' + ", deviceAccountId='" - + deviceAccountId + '\'' + ", deviceFamily='" + deviceFamily + '\'' + ", deviceType='" + deviceType - + '\'' + ", softwareVersion='" + softwareVersion + '\'' + ", online=" + online + ", capabilities=" - + capabilities + '}'; - } - } - - public List devices = List.of(); -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEnabledFeeds.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEnabledFeeds.java deleted file mode 100644 index d7eb1521c74d0..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEnabledFeeds.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonEnabledFeeds} encapsulate the GSON data of the enabled feeds list - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonEnabledFeeds { - public @Nullable List enabledFeeds; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEqualizer.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEqualizer.java deleted file mode 100644 index 65f0070195936..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEqualizer.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonEqualizer} encapsulate the GSON data of the get equalizer command - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonEqualizer { - public @Nullable Integer bass = 0; - public @Nullable Integer mid = 0; - public @Nullable Integer treble = 0; - - public JsonEqualizer createClone() { - JsonEqualizer result = new JsonEqualizer(); - result.bass = this.bass; - result.mid = this.mid; - result.treble = this.treble; - return result; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonExchangeTokenResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonExchangeTokenResponse.java deleted file mode 100644 index f73f7005265ed..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonExchangeTokenResponse.java +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -import com.google.gson.annotations.SerializedName; - -/** - * The {@link JsonExchangeTokenResponse} encapsulate the GSON response data of the token exchange - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonExchangeTokenResponse { - public @Nullable Response response; - - public static class Response { - public @Nullable Tokens tokens; - } - - public static class Tokens { - public @Nullable Map cookies; - } - - public static class Cookie { - @SerializedName("Path") - public @Nullable String path; - @SerializedName("Secure") - public @Nullable Boolean secure; - @SerializedName("Value") - public @Nullable String value; - @SerializedName("Expires") - public @Nullable String expires; - @SerializedName("HttpOnly") - public @Nullable Boolean httpOnly; - @SerializedName("Name") - public @Nullable String name; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java deleted file mode 100644 index 21a875ec2cd33..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonFeed} encapsulate the GSON data of feed - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonFeed { - public @Nullable Object feedId; - public @Nullable String name; - public @Nullable String skillId; - public @Nullable String imageUrl; - - public JsonFeed(@Nullable Object feedId, @Nullable String skillId) { - this.feedId = feedId; - this.skillId = skillId; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java deleted file mode 100644 index ad33153c25b21..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonMediaState} encapsulate the GSON data of the current media state - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonMediaState { - - public @Nullable String clientId; - public @Nullable String contentId; - public @Nullable String contentType; - public @Nullable String currentState; - public @Nullable String imageURL; - public boolean isDisliked; - public boolean isLiked; - public boolean looping; - public @Nullable String mediaOwnerCustomerId; - public boolean muted; - public @Nullable String programId; - public int progressSeconds; - public @Nullable String providerId; - public @Nullable List queue; - public @Nullable String queueId; - public @Nullable Integer queueSize; - public @Nullable String radioStationId; - public int radioVariety; - public @Nullable String referenceId; - public @Nullable String service; - public boolean shuffling; - // public long timeLastShuffled; parsing fails with some values, so do not use it - public int volume; - - public static class QueueEntry { - public @Nullable String album; - public @Nullable String albumAsin; - public @Nullable String artist; - public @Nullable String asin; - public @Nullable String cardImageURL; - public @Nullable String contentId; - public @Nullable String contentType; - public int durationSeconds; - public boolean feedbackDisabled; - public @Nullable String historicalId; - public @Nullable String imageURL; - public int index; - public boolean isAd; - public boolean isDisliked; - public boolean isFreeWithPrime; - public boolean isLiked; - public @Nullable String programId; - public @Nullable String programName; - public @Nullable String providerId; - public @Nullable String queueId; - public @Nullable String radioStationCallSign; - public @Nullable String radioStationId; - public @Nullable String radioStationLocation; - public @Nullable String radioStationSlogan; - public @Nullable String referenceId; - public @Nullable String service; - public @Nullable String startTime; - public @Nullable String title; - public @Nullable String trackId; - public @Nullable String trackStatus; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMusicProvider.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMusicProvider.java deleted file mode 100644 index 15408d91cb0f2..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMusicProvider.java +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonMusicProvider} encapsulate the GSON returned for a music provider - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonMusicProvider { - public @Nullable String displayName; - public List @Nullable [] supportedTriggers; - public @Nullable String icon; - public @Nullable List supportedProperties; - public @Nullable String id; - public @Nullable String availability; - public @Nullable String description; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java deleted file mode 100644 index e908f55e2b84c..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonNotificationRequest} encapsulate the GSON data for a notification request - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonNotificationRequest { - public @Nullable String type = "Reminder"; // "Reminder", "Alarm" - public @Nullable String status = "ON"; - public long alarmTime; - public @Nullable String originalTime; - public @Nullable String originalDate; - public @Nullable String timeZoneId; - public @Nullable String reminderIndex; - public @Nullable JsonNotificationSound sound; - public @Nullable String deviceSerialNumber; - public @Nullable String deviceType; - public @Nullable String recurringPattern; - public @Nullable String reminderLabel; - public boolean isSaveInFlight = true; - public @Nullable String id = "createReminder"; - public boolean isRecurring = false; - public long createdDate; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java deleted file mode 100644 index fdf749a25f1ab..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonNotificationResponse} encapsulate the GSON data for the result of a notification request - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonNotificationResponse { - // This is only a partial definition, see the example JSON below - public long alarmTime; - public long createdDate; - public @Nullable String deviceSerialNumber; - public @Nullable String deviceType; - public @Nullable String id; - public @Nullable String status; - public @Nullable String type; - public long remainingTime; - public @Nullable String recurringPattern; - public @Nullable String originalDate; - public @Nullable String originalTime; -} - -/* - * Example JSON: - * { - *    "alarmTime":1518864868060, - *    "createdDate":1518864863801, - *    "deviceSerialNumber":"XXXXXXXXXX", - *    "deviceType":"XXXXXXXXXX", - *    "id":"XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX", - *    "musicAlarmId":null, - *    "musicEntity":null, - *    "notificationIndex":"XXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX", - *    "originalDate":null, - *    "originalTime":"11:54:28.060", - *    "provider":null, - *    "recurringPattern":null, - *    "remainingTime":0, - *    "reminderLabel":null, - *    "sound":{ - *       "displayName":"Clarity", - *       "folder":null, - *       "id":"system_alerts_melodic_05", - *       "providerId":"ECHO", - *       "sampleUrl":"https://s3.amazonaws.com/deeappservice.prod.notificationtones/system_alerts_melodic_05.mp3" - *    }, - *    "status":"OFF", - *    "timeZoneId":null, - *    "timerLabel":null, - *    "triggerTime":0, - *    "type":"Alarm", - *    "version":"2", - *    "alarmIndex":null, - *    "isSaveInFlight":true - * } - * - */ diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java deleted file mode 100644 index 2850daf29ab58..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonNotificationSound} encapsulate the GSON data for a notification sound - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonNotificationSound { - public @Nullable String displayName; - public @Nullable String folder; - public @Nullable String id = "system_alerts_melodic_01"; - public @Nullable String providerId = "ECHO"; - public @Nullable String sampleUrl; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationsResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationsResponse.java deleted file mode 100644 index 3a5b6eac499a9..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationsResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonNotificationsResponse} encapsulate the GSON data for the result of a notifications request - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonNotificationsResponse { - public @Nullable List notifications; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaySearchPhraseOperationPayload.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaySearchPhraseOperationPayload.java deleted file mode 100644 index 173483cce2374..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaySearchPhraseOperationPayload.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonPlaySearchPhraseOperationPayload} encapsulate the GSON for validation requests and results - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonPlaySearchPhraseOperationPayload { - public @Nullable String deviceType = "ALEXA_CURRENT_DEVICE_TYPE"; - public @Nullable String deviceSerialNumber = "ALEXA_CURRENT_DSN"; - public @Nullable String locale = "ALEXA_CURRENT_LOCALE"; - public @Nullable String customerId; - public @Nullable String searchPhrase; - public @Nullable String sanitizedSearchPhrase; - public @Nullable String musicProviderId = "ALEXA_CURRENT_DSN"; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayValidationResult.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayValidationResult.java deleted file mode 100644 index ed1e53f261985..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayValidationResult.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonPlayValidationResult} encapsulate the GSON for validation result - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonPlayValidationResult { - public @Nullable JsonPlaySearchPhraseOperationPayload operationPayload; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java deleted file mode 100644 index bfce8620e8b7b..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonPlayerState} encapsulate the GSON data of the player state - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonPlayerState { - public @Nullable PlayerInfo playerInfo; - - public static class PlayerInfo { - public @Nullable String state; - public @Nullable InfoText infoText; - public @Nullable InfoText miniInfoText; - public @Nullable Provider provider; - public @Nullable Volume volume; - public @Nullable MainArt mainArt; - - public @Nullable String queueId; - public @Nullable String mediaId; - - public @Nullable Progress progress; - - public static class InfoText { - public boolean multiLineMode; - public @Nullable String subText1; - public @Nullable String subText2; - public @Nullable String title; - } - - public static class Provider { - public @Nullable String providerDisplayName; - public @Nullable String providerName; - } - - public static class Volume { - public boolean muted; - public int volume; - } - - public static class MainArt { - public @Nullable String altText; - public @Nullable String artType; - public @Nullable String contentType; - public @Nullable String url; - } - - public static class Progress { - public @Nullable Boolean allowScrubbing; - public @Nullable Object locationInfo; - public @Nullable Long mediaLength; - public @Nullable Long mediaProgress; - public @Nullable Boolean showTiming; - public @Nullable Boolean visible; - } - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaylists.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaylists.java deleted file mode 100644 index 7d54ba370a960..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaylists.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonPlayerState} encapsulate the GSON data of playlist query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonPlaylists { - - public @Nullable Map playlists; - - public static class PlayList { - public @Nullable String playlistId; - public @Nullable String title; - public int trackCount; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppRequest.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppRequest.java deleted file mode 100644 index 8eb46eff17a09..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppRequest.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -import com.google.gson.annotations.SerializedName; - -/** - * The {@link JsonRegisterAppRequest} encapsulate the GSON data of register application request - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonRegisterAppRequest { - - public JsonRegisterAppRequest(String serial, @Nullable String accessToken, String frc, - List webSiteCookies) { - registrationData.deviceSerial = serial; - authData.accessToken = accessToken; - userContextMap.frc = frc; - cookies.webSiteCookies = webSiteCookies; - } - - @SerializedName("requested_extensions") - public String[] requestedExtensions = { "device_info", "customer_info" }; - - public Cookies cookies = new Cookies(); - @SerializedName("registration_data") - public RegistrationData registrationData = new RegistrationData(); - @SerializedName("auth_data") - public AuthData authData = new AuthData(); - @SerializedName("user_context_map") - public UserContextMap userContextMap = new UserContextMap(); - @SerializedName("requested_token_type") - public String[] requestedTokenType = { "bearer", "mac_dms", "website_cookies" }; - - public static class Cookies { - @SerializedName("website_cookies") - public List webSiteCookies = List.of(); - public @Nullable String domain = ".amazon.com"; - } - - public static class RegistrationData { - public String domain = "Device"; - @SerializedName("app_version") - public String appVersion = "2.2.223830.0"; - @SerializedName("device_type") - public String deviceType = "A2IVLV5VM2W81"; - @SerializedName("device_name") - public String deviceName = "%FIRST_NAME%'s%DUPE_STRATEGY_1ST%openHAB Alexa Binding"; - @SerializedName("os_version") - public String osVersion = "11.4.1"; - @SerializedName("device_serial") - public @Nullable String deviceSerial; - @SerializedName("device_model") - public String deviceModel = "iPhone"; - @SerializedName("app_name") - public String appName = "openHAB Alexa Binding"; - @SerializedName("software_version") - public String softwareVersion = "1"; - } - - public static class AuthData { - @SerializedName("access_token") - public @Nullable String accessToken; - } - - public static class UserContextMap { - public String frc = ""; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppResponse.java deleted file mode 100644 index a5de32659cf31..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRegisterAppResponse.java +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -import com.google.gson.annotations.SerializedName; - -/** - * The {@link JsonRegisterAppResponse} encapsulate the GSON data of response from the register command - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonRegisterAppResponse { - - public @Nullable Response response; - - @SerializedName("request_id") - public @Nullable String requestId; - - public static class Response { - public @Nullable Success success; - } - - public static class Success { - public @Nullable Extensions extensions; - - public @Nullable Tokens tokens; - - @SerializedName("customer_id") - public @Nullable String customerId; - } - - public static class Extensions { - @SerializedName("device_info") - public @Nullable DeviceInfo deviceInfo; - - @SerializedName("customer_info") - public @Nullable CustomerInfo customerInfo; - - @SerializedName("customer_id") - public @Nullable String customerId; - } - - public static class DeviceInfo { - @SerializedName("device_name") - public @Nullable String deviceName; - - @SerializedName("device_serial_number") - public @Nullable String deviceSerialNumber; - - @SerializedName("device_type") - public @Nullable String deviceType; - } - - public static class CustomerInfo { - @SerializedName("account_pool") - public @Nullable String accountPool; - - @SerializedName("user_id") - public @Nullable String userId; - - @SerializedName("home_region") - public @Nullable String homeRegion; - - public @Nullable String name; - - @SerializedName("given_name") - public @Nullable String givenName; - } - - public static class Tokens { - @SerializedName("website_cookies") - public @Nullable Object websiteCookies; - - @SerializedName("mac_dms") - public @Nullable MacDms macDms; - - public @Nullable Bearer bearer; - } - - public static class MacDms { - @SerializedName("device_private_key") - public @Nullable String devicePrivateKey; - - @SerializedName("adp_token") - public @Nullable String adpToken; - } - - public static class Bearer { - @SerializedName("access_token") - public @Nullable String accessToken; - - @SerializedName("refresh_token") - public @Nullable String refreshToken; - - @SerializedName("expires_in") - public @Nullable String expiresIn; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRenewTokenResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRenewTokenResponse.java deleted file mode 100644 index 5e3643800a1d2..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonRenewTokenResponse.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -import com.google.gson.annotations.SerializedName; - -/** - * The {@link JsonRenewTokenResponse} encapsulate the GSON response of the renew token request - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonRenewTokenResponse { - @SerializedName("access_token") - public @Nullable String accessToken; - @SerializedName("token_type") - public @Nullable String tokenType; - @SerializedName("expires_in") - public @Nullable Long expiresIn; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeCapabilities.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeCapabilities.java deleted file mode 100644 index af0706e88d8fb..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeCapabilities.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * @author Lukas Knoeller - Initial contribution - */ -@NonNullByDefault -public class JsonSmartHomeCapabilities { - - public static class SmartHomeCapability { - public @Nullable String capabilityType; - public @Nullable String type; - public @Nullable String version; - public @Nullable String interfaceName; - public @Nullable Properties properties; - - @Override - public String toString() { - return "SmartHomeCapability{" + "capabilityType='" + capabilityType + '\'' + ", type='" + type + '\'' - + ", version='" + version + '\'' + ", interfaceName='" + interfaceName + '\'' + ", properties=" - + properties + '}'; - } - } - - public static class Properties { - public @Nullable List supported; - - @Override - public String toString() { - return "Properties{" + "supported=" + supported + '}'; - } - } - - public static class Property { - public @Nullable String name; - } - - @Override - public String toString() { - return "JsonSmartHomeCapabilities{" + "capabilites=" + capabilites + '}'; - } - - public @Nullable List capabilites; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java deleted file mode 100644 index 1718ebbaea6c1..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; -import java.util.Objects; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDeviceNetworkState.SmartHomeDeviceNetworkState; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeTags.JsonSmartHomeTag; - -/** - * @author Lukas Knoeller - Initial contribution - */ -@NonNullByDefault -public class JsonSmartHomeDevices { - public static class SmartHomeDevice implements SmartHomeBaseDevice { - public @Nullable Integer updateIntervalInSeconds; - public @Nullable String applianceId; - public @Nullable String manufacturerName; - public @Nullable String friendlyDescription; - public @Nullable String modelName; - public @Nullable String friendlyName; - public @Nullable String reachability; - public @Nullable String entityId; - public @Nullable SmartHomeDeviceNetworkState applianceNetworkState; - public @Nullable List capabilities; - public @Nullable JsonSmartHomeTag tags; - public @Nullable List applianceTypes; - public @Nullable List aliases; - public @Nullable List groupDevices; - public @Nullable String connectedVia; - public @Nullable DriverIdentity driverIdentity; - public @Nullable List mergedApplianceIds; - public @Nullable List smarthomeDevices; - - public List getCapabilities() { - return Objects.requireNonNullElse(capabilities, List.of()); - } - - @Override - public @Nullable String findId() { - return applianceId; - } - - @Override - public @Nullable String findEntityId() { - return entityId; - } - - @Override - public boolean isGroup() { - return false; - } - - @Override - public String toString() { - return "SmartHomeDevice{" + "updateIntervalInSeconds=" + updateIntervalInSeconds + ", applianceId='" - + applianceId + '\'' + ", manufacturerName='" + manufacturerName + '\'' + ", friendlyDescription='" - + friendlyDescription + '\'' + ", modelName='" + modelName + '\'' + ", friendlyName='" - + friendlyName + '\'' + ", reachability='" + reachability + '\'' + ", entityId='" + entityId + '\'' - + ", applianceNetworkState=" + applianceNetworkState + ", capabilities=" + capabilities + ", tags=" - + tags + ", applianceTypes=" + applianceTypes + ", aliases=" + aliases + ", groupDevices=" - + groupDevices + ", connectedVia='" + connectedVia + '\'' + ", driverIdentity=" + driverIdentity - + ", mergedApplianceIds=" + mergedApplianceIds + ", smarthomeDevices=" + smarthomeDevices + '}'; - } - } - - public static class DriverIdentity { - public @Nullable String namespace; - public @Nullable String identifier; - - @Override - public String toString() { - return "DriverIdentity{" + "namespace='" + namespace + '\'' + ", identifier='" + identifier + '\'' + '}'; - } - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java deleted file mode 100644 index d739a5f4521e7..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonStartRoutineRequest} encapsulate the GSON for starting a routine - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonStartRoutineRequest { - public @Nullable String behaviorId = "PREVIEW"; - public @Nullable String sequenceJson; - public @Nullable String status = "ENABLED"; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonUsersMeResponse.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonUsersMeResponse.java deleted file mode 100644 index 2130625641d3a..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonUsersMeResponse.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonUsersMeResponse} encapsulate the GSON data of the users me response - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonUsersMeResponse { - public @Nullable String countryOfResidence; - public @Nullable String effectiveMarketPlaceId; - public @Nullable String email; - public @Nullable Boolean eulaAcceptance; - public @Nullable List features; - public @Nullable String fullName; - public @Nullable Boolean hasActiveDopplers; - public @Nullable String id; - public @Nullable String marketPlaceDomainName; - public @Nullable String marketPlaceId; - public @Nullable String marketPlaceLocale; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonWakeWords.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonWakeWords.java deleted file mode 100644 index c4e298e07ccc6..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonWakeWords.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonWakeWords} encapsulate the GSON data of the wake word request - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonWakeWords { - public @Nullable List wakeWords; - - public static class WakeWord { - public @Nullable Boolean active; - public @Nullable String deviceSerialNumber; - public @Nullable String deviceType; - public @Nullable Object midFieldState; - public @Nullable String wakeWord; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonWebSiteCookie.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonWebSiteCookie.java deleted file mode 100644 index 721a4a2f1611c..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonWebSiteCookie.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -import com.google.gson.annotations.SerializedName; - -/** - * The {@link JsonWebSiteCookie} encapsulate the GSON data of register cookie array - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonWebSiteCookie { - public JsonWebSiteCookie(String name, String value) { - this.name = name; - this.value = value; - } - - @SerializedName("Value") - public @Nullable String value; - @SerializedName("Name") - public @Nullable String name; -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/push/PushConnection.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/push/PushConnection.java new file mode 100644 index 0000000000000..98eced18b847b --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/push/PushConnection.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.amazonechocontrol.internal.push; + +import static org.eclipse.jetty.http.HttpHeader.*; +import static org.eclipse.jetty.http.HttpMethod.GET; +import static org.eclipse.jetty.http.HttpVersion.HTTP_2; +import static org.openhab.binding.amazonechocontrol.internal.push.PushConnection.State.*; + +import java.net.InetSocketAddress; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.PingFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.openhab.binding.amazonechocontrol.internal.dto.push.PushCommandTO; +import org.openhab.binding.amazonechocontrol.internal.dto.push.PushMessageTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * The {@link PushConnection} handles the HTTP/2 push connection + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PushConnection implements PushSessionHandler.Listener, PushStreamAdapter.Listener { + private static final long CONNECTION_TIMEOUT = 10; + private static final int PING_TIMEOUT = 10; + private static final String PUSH_STREAM_ID = "push-stream"; + + private final Logger logger = LoggerFactory.getLogger(PushConnection.class); + private final HTTP2Client http2Client; + private final Gson gson; + private final Listener listener; + private final ScheduledExecutorService scheduler; + + private State state = CLOSED; + private @Nullable Session session; + private @Nullable ScheduledFuture waitForPing; + + public PushConnection(HTTP2Client http2Client, Gson gson, Listener listener, ScheduledExecutorService scheduler) { + this.http2Client = http2Client; + this.gson = gson; + this.listener = listener; + this.scheduler = scheduler; + } + + public State getState() { + return state; + } + + private void setState(State newState) { + this.state = newState; + listener.onPushConnectionStateChange(state); + } + + public void open(String amazonSite, String accessToken) { + cancelWaitForPing(); + Session session = this.session; + if (state != CLOSED || session != null) { + logger.warn( + "Tried to open a new session, but the the current state is {} - session hash {}. Please enable TRACE logging and report a bug.", + state, session != null ? session.hashCode() : ""); + return; + } + setState(State.CONNECTING); + + String host = switch (amazonSite) { + case "amazon.com" -> "bob-dispatch-prod-na.amazon.com"; + case "amazon.com.br" -> "bob-dispatch-prod-na.amazon.com"; + case "amazon.co.jp" -> "bob-dispatch-prod-fe.amazon.com"; + default -> "bob-dispatch-prod-eu.amazon.com"; + }; + + InetSocketAddress address = new InetSocketAddress(host, 443); + PushSessionHandler sessionHandler = new PushSessionHandler(this); + Promise.Completable sessionPromise = new Promise.Completable<>(); + sessionPromise.orTimeout(CONNECTION_TIMEOUT, TimeUnit.SECONDS).handle((newSession, throwable) -> { + if (throwable != null) { + logger.warn("Failed to create session: {}", throwable.getMessage()); + setState(FAILED); + close(); + } else { + logger.trace("Created session with hash {}.", newSession.hashCode()); + this.session = newSession; + openPushStream(newSession, host, accessToken); + } + return null; + }); + http2Client.connect(http2Client.getBean(SslContextFactory.class), address, sessionHandler, sessionPromise); + } + + public void sendPing() { + Session session = this.session; + // we need to be connected, have a non-closes session and no running ping-pong + if (state == CONNECTED && session != null && !session.isClosed() && waitForPing == null) { + logger.trace("Sending ping in session {}", session.hashCode()); + waitForPing = scheduler.schedule(this::close, PING_TIMEOUT, TimeUnit.SECONDS); + session.ping(new PingFrame(false), Callback.NOOP); + } else if (state != CLOSED && session != null && session.isClosed()) { + close(); + } + } + + public void close() { + cancelWaitForPing(); + setState(State.DISCONNECTING); + Session session = this.session; + if (session != null && !session.isClosed()) { + session.getStreams().stream().filter(s -> s.getAttribute(PUSH_STREAM_ID) != null && !s.isReset()).forEach( + s -> s.reset(new ResetFrame(s.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP)); + session.close(ErrorCode.NO_ERROR.code, null, Callback.NOOP); + } + this.session = null; + setState(CLOSED); + } + + private void openPushStream(Session session, String host, String accessToken) { + HttpFields headerFields = new HttpFields(); + headerFields.put(USER_AGENT, "okhttp/4.3.2-SNAPSHOT"); + headerFields.put(AUTHORIZATION, "Bearer " + accessToken); + HttpURI uri = new HttpURI("https://" + host + "/v20160207/directives"); + HeadersFrame headers = new HeadersFrame(new MetaData.Request(GET.asString(), uri, HTTP_2, headerFields), null, + false); + + PushStreamAdapter eventListener = new PushStreamAdapter(gson, session, this); + Promise.Completable streamPromise = new Promise.Completable<>(); + streamPromise.orTimeout(CONNECTION_TIMEOUT, TimeUnit.SECONDS).handle((stream, throwable) -> { + if (throwable != null) { + logger.warn("Failed to open stream in session {}: {}", session.hashCode(), throwable.getMessage()); + setState(FAILED); + } else { + logger.debug("Successfully initiated stream for session {}.", session.hashCode()); + stream.setIdleTimeout(0); + stream.setAttribute(PUSH_STREAM_ID, session); + setState(State.CONNECTED); + } + return null; + }); + session.newStream(headers, streamPromise, eventListener); + } + + @Override + public void onSessionClosed(int sessionHashCode) { + cancelWaitForPing(); + Session currentSession = session; + if (currentSession != null && currentSession.hashCode() == sessionHashCode) { + setState(CLOSED); + this.session = null; + } else { + logger.debug("Received a session closed for session {}, but the current session is {}", sessionHashCode, + currentSession != null ? currentSession.hashCode() : ""); + } + } + + @Override + public void onSessionFailed(int sessionHashCode) { + cancelWaitForPing(); + Session currentSession = session; + if (currentSession != null && currentSession.hashCode() == sessionHashCode) { + setState(FAILED); + } else { + logger.debug("Received a session failed for session {}, but the current session is {}", sessionHashCode, + currentSession != null ? currentSession.hashCode() : ""); + } + } + + @Override + public void onPushMessageReceived(PushMessageTO.RenderingUpdateTO renderingUpdate) { + PushCommandTO pushCommand = gson.fromJson(renderingUpdate.resourceMetadata, PushCommandTO.class); + if (pushCommand != null) { + listener.onPushCommandReceived(pushCommand); + } + } + + @Override + public void onSessionPingReceived() { + logger.trace("Cancelling pingWaitJob"); + cancelWaitForPing(); + } + + private void cancelWaitForPing() { + ScheduledFuture waitForPing = this.waitForPing; + if (waitForPing != null) { + waitForPing.cancel(true); + this.waitForPing = null; + } + } + + public interface Listener { + void onPushConnectionStateChange(State state); + + void onPushCommandReceived(PushCommandTO pushCommand); + } + + public enum State { + CLOSED, + CONNECTING, + CONNECTED, + DISCONNECTING, + FAILED + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/push/PushSessionHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/push/PushSessionHandler.java new file mode 100644 index 0000000000000..82547a8554fe8 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/push/PushSessionHandler.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.push; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.frames.GoAwayFrame; +import org.eclipse.jetty.http2.frames.PingFrame; +import org.eclipse.jetty.util.Callback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PushSessionHandler} handles the HTTP/2 push session + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PushSessionHandler extends Session.Listener.Adapter { + private final Logger logger = LoggerFactory.getLogger(PushSessionHandler.class); + private final Listener listener; + + public PushSessionHandler(Listener listener) { + this.listener = listener; + } + + @Override + public void onClose(@NonNullByDefault({}) Session session, @NonNullByDefault({}) GoAwayFrame frame) { + logger.debug("Session {} closed, reason {}", session.hashCode(), frame.getError()); + listener.onSessionClosed(session.hashCode()); + } + + @Override + public void onFailure(@NonNullByDefault({}) Session session, @NonNullByDefault({}) Throwable failure) { + logger.warn("Session {} failed: {}", session.hashCode(), failure.getMessage()); + listener.onSessionFailed(session.hashCode()); + } + + @Override + public void onPing(@NonNullByDefault({}) Session session, @NonNullByDefault({}) PingFrame frame) { + logger.trace("Session {} received pingFrame (reply={})", session.hashCode(), frame.isReply()); + if (!frame.isReply()) { + // answer only if this is not a reply + session.ping(new PingFrame(true), Callback.NOOP); + } else { + listener.onSessionPingReceived(); + } + } + + public interface Listener { + void onSessionClosed(int sessionHashCode); + + void onSessionFailed(int sessionHashCode); + + void onSessionPingReceived(); + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/push/PushStreamAdapter.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/push/PushStreamAdapter.java new file mode 100644 index 0000000000000..7dbd7d179faf3 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/push/PushStreamAdapter.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.push; + +import static org.eclipse.jetty.http.HttpHeader.CONTENT_TYPE; + +import java.io.BufferedReader; +import java.io.StringReader; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.PingFrame; +import org.eclipse.jetty.util.Callback; +import org.openhab.binding.amazonechocontrol.internal.dto.push.PushMessageTO; +import org.openhab.binding.amazonechocontrol.internal.util.HttpUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * The {@link PushStreamAdapter} handles the HTTP/2 push stream + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class PushStreamAdapter extends Stream.Listener.Adapter { + private final Logger logger = LoggerFactory.getLogger(PushStreamAdapter.class); + private final Gson gson; + private final Session session; + private final Listener listener; + private String boundary = ""; + + public PushStreamAdapter(Gson gson, Session session, Listener listener) { + this.gson = gson; + this.session = session; + this.listener = listener; + } + + @Override + public void onHeaders(@NonNullByDefault({}) Stream stream, @NonNullByDefault({}) HeadersFrame frame) { + HttpFields headers = frame.getMetaData().getFields(); + if (logger.isTraceEnabled()) { + logger.trace("Received headers: {}", HttpUtil.logToString(headers)); + } + String contentType = headers.get(CONTENT_TYPE); + if (contentType == null || contentType.isBlank()) { + logger.warn("Headers of HTTP/2 stream don't contain content-type"); + return; + } + int boundaryStart = contentType.indexOf("boundary="); + int boundaryEnd = contentType.indexOf(";", boundaryStart); + boundary = contentType.substring(boundaryStart + 9, boundaryEnd); + } + + @Override + public void onData(@NonNullByDefault({}) Stream stream, @NonNullByDefault({}) DataFrame frame, + @NonNullByDefault({}) Callback callback) { + byte[] contentBuffer = new byte[frame.remaining()]; + frame.getData().get(contentBuffer); + String contentString = new String(contentBuffer); + logger.trace("Received raw data {}", contentString); + + // process + try { + if (boundary.isBlank()) { + logger.debug("Discarding message because boundary is not set"); + return; + } + BufferedReader contentReader = new BufferedReader(new StringReader(contentString)); + List content = contentReader.lines().filter(line -> !line.isBlank()).toList(); + + if (content.isEmpty()) { + return; + } + + if (!content.get(content.size() - 1).endsWith(boundary)) { + logger.debug("Discarding incomplete message, boundary not found"); + } + + if (content.size() == 1) { + // only boundary requires a PING response + logger.debug("Sending ping"); + session.ping(new PingFrame(false), Callback.NOOP); + } else if (content.get(0).equals("Content-Type: application/json")) { + // parse the message + PushMessageTO parsedMessage = Objects + .requireNonNullElse(gson.fromJson(content.get(1), PushMessageTO.class), new PushMessageTO()); + parsedMessage.directive.payload.renderingUpdates.forEach(listener::onPushMessageReceived); + } else { + logger.warn("Don't know how to handle frame starting with {}", content.get(0)); + } + } catch (RuntimeException e) { + logger.warn("Exception while processing message", e); + } + } + + public interface Listener { + void onPushMessageReceived(PushMessageTO.RenderingUpdateTO renderingUpdate); + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/AbstractInterfaceHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/AbstractInterfaceHandler.java new file mode 100644 index 0000000000000..24fb75f32a03e --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/AbstractInterfaceHandler.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability.Properties; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.openhab.core.thing.Channel; +import org.openhab.core.types.Command; +import org.openhab.core.types.CommandOption; +import org.openhab.core.types.State; +import org.openhab.core.types.StateDescription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonObject; + +/** + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractInterfaceHandler implements InterfaceHandler { + private final Logger logger = LoggerFactory.getLogger(AbstractInterfaceHandler.class); + + private final List interfaces; + + protected SmartHomeDeviceHandler smartHomeDeviceHandler; + protected Map channels = new HashMap<>(); + + public AbstractInterfaceHandler(SmartHomeDeviceHandler smartHomeDeviceHandler, List interfaces) { + this.smartHomeDeviceHandler = smartHomeDeviceHandler; + this.interfaces = interfaces; + } + + @Override + public @Nullable List getCommandDescription(Channel channel) { + // return null if not used + return null; + } + + @Override + public @Nullable StateDescription getStateDescription(Channel channel) { + // return null if not used + return null; + } + + protected abstract Set findChannelInfos(JsonSmartHomeCapability capability, String property); + + public abstract void updateChannels(String interfaceName, List stateList, UpdateChannelResult result); + + public abstract boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) + throws IOException, InterruptedException; + + public boolean hasChannel(String channelId) { + return channels.containsKey(channelId); + } + + public List getSupportedInterface() { + return interfaces; + } + + SmartHomeDeviceHandler getSmartHomeDeviceHandler() { + return smartHomeDeviceHandler; + } + + public Collection initialize(List capabilities) { + // TODO: reduce or remove + Map channels = new HashMap<>(); + for (JsonSmartHomeCapability capability : capabilities) { + Properties properties = capability.properties; + if (properties != null) { + List supported = Objects.requireNonNullElse(properties.supported, List.of()); + for (Properties.Property property : supported) { + String name = property.name; + if (name != null) { + findChannelInfos(capability, name).forEach(c -> channels.put(c.channelId, c)); + } + } + } + } + logger.trace("Handler '{}' has capabilities '{}' and uses channels '{}'", capabilities, + smartHomeDeviceHandler.getId(), channels); + this.channels = channels; + return channels.values(); + } + + protected boolean containsCapabilityProperty(List capabilities, String propertyName) { + for (JsonSmartHomeCapability capability : capabilities) { + Properties properties = capability.properties; + if (properties != null) { + List supported = Objects.requireNonNullElse(properties.supported, List.of()); + if (supported.stream().anyMatch(p -> propertyName.equals(p.name))) { + return true; + } + } + } + return false; + } + + public void updateState(String channelId, State state) { + getSmartHomeDeviceHandler().updateState(channelId, state); + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/AlexaColor.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/AlexaColor.java new file mode 100644 index 0000000000000..a9a29a57a5073 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/AlexaColor.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.amazonechocontrol.internal.smarthome; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; +import org.openhab.core.library.types.HSBType; + +/** + * The {@link AlexaColor} defines the Alexa color names + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class AlexaColor { + + public final String colorName; + final HSBType value; + private final double[] lab; + + public AlexaColor(String colorName, HSBType value) { + this.colorName = colorName; + this.value = value; + this.lab = getLabFromHSB(value); + } + + /** + * get the closest Alexa color + * + * @param value a given HSB color + * @return the name of the closest pre-defined Alexa color + */ + public static String getClosestColorName(HSBType value) { + double[] lab = getLabFromHSB(value); + String colorName = ""; + double smallestDistance = Double.MAX_VALUE; + for (AlexaColor color : AmazonEchoControlBindingConstants.ALEXA_COLORS) { + double distance = color.getEuclideanDistance(lab); + if (distance < smallestDistance) { + colorName = color.colorName; + smallestDistance = distance; + } + } + return colorName; + } + + private double getEuclideanDistance(double[] value) { + double deltaL = value[0] - lab[0]; + double deltaA = value[1] - lab[1]; + double deltaB = value[2] - lab[2]; + + return Math.sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB); + } + + private static double[] getLabFromHSB(HSBType value) { + double r = value.getRed().doubleValue() / 100.0; + double g = value.getGreen().doubleValue() / 100.0; + double b = value.getBlue().doubleValue() / 100.0; + + // D65, 10 degree + double xn = 94.811; + double yn = 100.0; + double zn = 107.304; + + double x = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b; + double y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b; + double z = 0.0193339 * r + 0.1191920 * g + 0.9503041 * b; + + double ls = 116.0 * labRoot(y / yn) - 16.0; + double as = 500.0 * (labRoot(x / xn) - labRoot(y / yn)); + double bs = 200.0 * (labRoot(y / yn) - labRoot(z / zn)); + + return new double[] { ls, as, bs }; + } + + private static double labRoot(double value) { + if (value < 216.0 / 24389.0) { + return (1.0 / 116.0) * ((24389.0 / 27.0) * value + 16.0); + } else { + return Math.pow(value, 1.0 / 3.0); + } + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/ChannelInfo.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/ChannelInfo.java new file mode 100644 index 0000000000000..30dcbbbc6ac9a --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/ChannelInfo.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.amazonechocontrol.internal.smarthome; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.type.ChannelTypeUID; + +/** + * The {@link ChannelInfo} holds the information for a single channel + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class ChannelInfo { + public final String propertyName; + public final String channelId; + public final String propertyNameSend; + public final ChannelTypeUID channelTypeUID; + public final @Nullable String label; + + public ChannelInfo(String propertyNameReceive, String propertyNameSend, String channelId, + ChannelTypeUID channelTypeUID, @Nullable String label) { + this.propertyName = propertyNameReceive; + this.propertyNameSend = propertyNameSend; + this.channelId = channelId; + this.channelTypeUID = channelTypeUID; + this.label = label; + } + + public ChannelInfo(String propertyNameReceive, String propertyNameSend, String channelId, + ChannelTypeUID channelTypeUID) { + this(propertyNameReceive, propertyNameSend, channelId, channelTypeUID, null); + } + + public ChannelInfo(String propertyName, String channelId, ChannelTypeUID channelTypeUID) { + this(propertyName, propertyName, channelId, channelTypeUID); + } + + public ChannelInfo(String propertyName, String channelId, ChannelTypeUID channelTypeUID, String label) { + this(propertyName, propertyName, channelId, channelTypeUID, label); + } + + @Override + public String toString() { + return "ChannelInfo{" + "propertyName='" + propertyName + "', channelId='" + channelId + "', propertyNameSend='" + + propertyNameSend + "', channelTypeUID=" + channelTypeUID + "label='" + label + "'}"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColors.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/ColorChannelConfig.java similarity index 60% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColors.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/ColorChannelConfig.java index 5b64ef72ebabd..c323f90251c00 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonColors.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/ColorChannelConfig.java @@ -10,19 +10,16 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.smarthome; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; /** - * @author Lukas Knoeller - Initial contribution + * The {@link ColorChannelConfig} holds the configuration for a color channel + * + * @author Jan N. Klug - Initial contribution */ @NonNullByDefault -public class JsonColors { - public @Nullable String colorName; - - public JsonColors(String colorName) { - this.colorName = colorName; - } +public class ColorChannelConfig { + public boolean matchColors = false; } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/Constants.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/Constants.java index 2abf24bd0287e..c9737a1055365 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/Constants.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/Constants.java @@ -12,12 +12,13 @@ */ package org.openhab.binding.amazonechocontrol.internal.smarthome; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.BINDING_ID; + import java.util.Map; import java.util.Set; import java.util.function.Function; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; import org.openhab.core.thing.type.ChannelTypeUID; @@ -26,46 +27,60 @@ */ @NonNullByDefault public class Constants { - public static final Map> HANDLER_FACTORY = Map.ofEntries( + public static final Map> HANDLER_FACTORY = Map.ofEntries( Map.entry(HandlerPowerController.INTERFACE, HandlerPowerController::new), Map.entry(HandlerBrightnessController.INTERFACE, HandlerBrightnessController::new), Map.entry(HandlerColorController.INTERFACE, HandlerColorController::new), Map.entry(HandlerColorTemperatureController.INTERFACE, HandlerColorTemperatureController::new), Map.entry(HandlerSecurityPanelController.INTERFACE, HandlerSecurityPanelController::new), Map.entry(HandlerAcousticEventSensor.INTERFACE, HandlerAcousticEventSensor::new), + Map.entry(HandlerHumiditySensor.INTERFACE, HandlerHumiditySensor::new), Map.entry(HandlerTemperatureSensor.INTERFACE, HandlerTemperatureSensor::new), Map.entry(HandlerThermostatController.INTERFACE, HandlerThermostatController::new), Map.entry(HandlerPercentageController.INTERFACE, HandlerPercentageController::new), Map.entry(HandlerPowerLevelController.INTERFACE, HandlerPowerLevelController::new), - Map.entry(HandlerHumiditySensor.INTERFACE, HandlerHumiditySensor::new)); + Map.entry(HandlerRangeController.INTERFACE, HandlerRangeController::new), + Map.entry(HandlerMotionSensor.INTERFACE, HandlerMotionSensor::new), + Map.entry(HandlerContactSensor.INTERFACE, HandlerContactSensor::new), + Map.entry(HandlerLocation.INTERFACE, HandlerLocation::new), + Map.entry(HandlerEndpointHealth.INTERFACE, HandlerEndpointHealth::new), + Map.entry(HandlerLockController.INTERFACE, HandlerLockController::new)); public static final Set SUPPORTED_INTERFACES = HANDLER_FACTORY.keySet(); // channel types - public static final ChannelTypeUID CHANNEL_TYPE_TEMPERATURE = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "temperature"); - public static final ChannelTypeUID CHANNEL_TYPE_HUMIDITY = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "relativeHumidity"); - public static final ChannelTypeUID CHANNEL_TYPE_TARGETSETPOINT = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "targetSetpoint"); - public static final ChannelTypeUID CHANNEL_TYPE_LOWERSETPOINT = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "lowerSetpoint"); - public static final ChannelTypeUID CHANNEL_TYPE_UPPERSETPOINT = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "upperSetpoint"); - public static final ChannelTypeUID CHANNEL_TYPE_THERMOSTATMODE = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "thermostatMode"); - public static final ChannelTypeUID CHANNEL_TYPE_FAN_OPERATION = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "fanOperation"); - public static final ChannelTypeUID CHANNEL_TYPE_COOLER_OPERATION = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "coolerOperation"); - - // List of Item types - public static final String ITEM_TYPE_SWITCH = "Switch"; - public static final String ITEM_TYPE_DIMMER = "Dimmer"; - public static final String ITEM_TYPE_STRING = "String"; - public static final String ITEM_TYPE_NUMBER = "Number"; - public static final String ITEM_TYPE_NUMBER_TEMPERATURE = "Number:Temperature"; - public static final String ITEM_TYPE_HUMIDITY = "Number:Dimensionless"; - public static final String ITEM_TYPE_CONTACT = "Contact"; - public static final String ITEM_TYPE_COLOR = "Color"; + public static final ChannelTypeUID CHANNEL_TYPE_TEMPERATURE = new ChannelTypeUID(BINDING_ID, "temperature"); + public static final ChannelTypeUID CHANNEL_TYPE_TARGETSETPOINT = new ChannelTypeUID(BINDING_ID, "targetSetpoint"); + public static final ChannelTypeUID CHANNEL_TYPE_LOWERSETPOINT = new ChannelTypeUID(BINDING_ID, "lowerSetpoint"); + public static final ChannelTypeUID CHANNEL_TYPE_UPPERSETPOINT = new ChannelTypeUID(BINDING_ID, "upperSetpoint"); + public static final ChannelTypeUID CHANNEL_TYPE_THERMOSTATMODE = new ChannelTypeUID(BINDING_ID, "thermostatMode"); + public static final ChannelTypeUID CHANNEL_TYPE_AIR_QUALITY_INDOOR_AIR_QUALITY = new ChannelTypeUID(BINDING_ID, + "indoorAirQuality"); + public static final ChannelTypeUID CHANNEL_TYPE_AIR_QUALITY_HUMIDITY = new ChannelTypeUID(BINDING_ID, "humidity"); + public static final ChannelTypeUID CHANNEL_TYPE_AIR_QUALITY_PM25 = new ChannelTypeUID(BINDING_ID, "pm25"); + public static final ChannelTypeUID CHANNEL_TYPE_AIR_QUALITY_CARBON_MONOXIDE = new ChannelTypeUID(BINDING_ID, + "carbonMonoxide"); + public static final ChannelTypeUID CHANNEL_TYPE_AIR_QUALITY_VOC = new ChannelTypeUID(BINDING_ID, "voc"); + public static final ChannelTypeUID CHANNEL_TYPE_FAN_SPEED = new ChannelTypeUID(BINDING_ID, "fanSpeed"); + public static final ChannelTypeUID CHANNEL_TYPE_POWER_STATE = new ChannelTypeUID(BINDING_ID, "powerState"); + public static final ChannelTypeUID CHANNEL_TYPE_LOCK_STATE = new ChannelTypeUID(BINDING_ID, "lockState"); + public static final ChannelTypeUID CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION = new ChannelTypeUID(BINDING_ID, + "acousticEventDetectionState"); + public static final ChannelTypeUID CHANNEL_TYPE_BRIGHTNESS = new ChannelTypeUID(BINDING_ID, "brightness"); + public static final ChannelTypeUID CHANNEL_TYPE_COLOR_NAME = new ChannelTypeUID(BINDING_ID, "colorName"); + public static final ChannelTypeUID CHANNEL_TYPE_COLOR = new ChannelTypeUID(BINDING_ID, "color"); + public static final ChannelTypeUID CHANNEL_TYPE_COLOR_TEMPERATURE_NAME = new ChannelTypeUID(BINDING_ID, + "colorTemperatureName"); + public static final ChannelTypeUID CHANNEL_TYPE_PERCENTAGE = new ChannelTypeUID(BINDING_ID, "percentage"); + public static final ChannelTypeUID CHANNEL_TYPE_POWER_LEVEL = new ChannelTypeUID(BINDING_ID, "powerLevel"); + public static final ChannelTypeUID CHANNEL_TYPE_ARM_STATE = new ChannelTypeUID(BINDING_ID, "armState"); + public static final ChannelTypeUID CHANNEL_TYPE_BURGLARY_ALARM = new ChannelTypeUID(BINDING_ID, "burglaryAlarm"); + public static final ChannelTypeUID CHANNEL_TYPE_CARBON_MONOXIDE_ALARM = new ChannelTypeUID(BINDING_ID, + "carbonMonoxideAlarm"); + public static final ChannelTypeUID CHANNEL_TYPE_FIRE_ALARM = new ChannelTypeUID(BINDING_ID, "fireAlarm"); + public static final ChannelTypeUID CHANNEL_TYPE_WATER_ALARM = new ChannelTypeUID(BINDING_ID, "waterAlarm"); + public static final ChannelTypeUID CHANNEL_TYPE_MOTION_DETECTED = new ChannelTypeUID(BINDING_ID, "motionDetected"); + public static final ChannelTypeUID CHANNEL_TYPE_CONTACT_STATUS = new ChannelTypeUID(BINDING_ID, "contact"); + public static final ChannelTypeUID CHANNEL_TYPE_GEOLOCATION = new ChannelTypeUID(BINDING_ID, "geoLocation"); + public static final ChannelTypeUID CHANNEL_TYPE_CONNECTIVITY = new ChannelTypeUID(BINDING_ID, "connectivity"); } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/DynamicStateDescriptionSmartHome.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/DynamicStateDescriptionSmartHome.java deleted file mode 100644 index 0fe02c28b06df..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/DynamicStateDescriptionSmartHome.java +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.smarthome; - -import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.BINDING_ID; - -import java.util.Locale; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.core.thing.Channel; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingRegistry; -import org.openhab.core.thing.binding.ThingHandler; -import org.openhab.core.thing.type.ChannelTypeUID; -import org.openhab.core.thing.type.DynamicStateDescriptionProvider; -import org.openhab.core.types.StateDescription; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; - -/** - * - * Dynamic channel state description provider - * Overrides the state description for the colors of the smart bulbs - * - * @author Lukas Knoeller - Initial contribution - * - */ - -@Component(service = { DynamicStateDescriptionProvider.class, DynamicStateDescriptionSmartHome.class }) -@NonNullByDefault -public class DynamicStateDescriptionSmartHome implements DynamicStateDescriptionProvider { - private final ThingRegistry thingRegistry; - - @Activate - public DynamicStateDescriptionSmartHome(@Reference ThingRegistry thingRegistry) { - this.thingRegistry = thingRegistry; - } - - public @Nullable SmartHomeDeviceHandler findHandler(Channel channel) { - Thing thing = thingRegistry.get(channel.getUID().getThingUID()); - if (thing == null) { - return null; - } - ThingHandler handler = thing.getHandler(); - if (handler instanceof SmartHomeDeviceHandler smartHomeHandler) { - return smartHomeHandler; - } - return null; - } - - @Override - public @Nullable StateDescription getStateDescription(Channel channel, - @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { - ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); - if (channelTypeUID == null || !BINDING_ID.equals(channelTypeUID.getBindingId())) { - return null; - } - if (originalStateDescription == null) { - return null; - } - SmartHomeDeviceHandler handler = findHandler(channel); - if (handler != null) { - return handler.findStateDescription(channel, originalStateDescription, locale); - } - return null; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerAcousticEventSensor.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerAcousticEventSensor.java index 9a98f2d7c0589..42d30f105adc1 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerAcousticEventSensor.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerAcousticEventSensor.java @@ -12,107 +12,100 @@ */ package org.openhab.binding.amazonechocontrol.internal.smarthome; -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_CONTACT; - import java.io.IOException; import java.util.List; -import java.util.Locale; +import java.util.Map; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.core.library.types.OpenClosedType; -import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; -import org.openhab.core.types.StateDescription; -import org.openhab.core.types.UnDefType; import com.google.gson.JsonObject; /** - * The {@link HandlerAcousticEventSensor} is responsible for the Alexa.PowerControllerInterface + * The {@link HandlerAcousticEventSensor} is responsible for the Alexa.AcousticEventSensor interface * * @author Lukas Knoeller - Initial contribution * @author Michael Geramb - Initial contribution + * @author Jan N. Klug - refactoring and new channels */ @NonNullByDefault -public class HandlerAcousticEventSensor extends HandlerBase { - // Interface +public class HandlerAcousticEventSensor extends AbstractInterfaceHandler { public static final String INTERFACE = "Alexa.AcousticEventSensor"; - // Channel types - private static final ChannelTypeUID CHANNEL_TYPE_GLASS_BREAK_DETECTION_STATE = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "glassBreakDetectionState"); - private static final ChannelTypeUID CHANNEL_TYPE_SMOKE_ALARM_DETECTION_STATE = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "smokeAlarmDetectionState"); - - // Channel definitions - private static final ChannelInfo GLASS_BREAK_DETECTION_STATE = new ChannelInfo( - "glassBreakDetectionState" /* propertyName */ , "glassBreakDetectionState" /* ChannelId */, - CHANNEL_TYPE_GLASS_BREAK_DETECTION_STATE /* Channel Type */ , ITEM_TYPE_CONTACT /* Item Type */); - private static final ChannelInfo SMOKE_ALARM_DETECTION_STATE = new ChannelInfo( - "smokeAlarmDetectionState" /* propertyName */ , "smokeAlarmDetectionState" /* ChannelId */, - CHANNEL_TYPE_SMOKE_ALARM_DETECTION_STATE /* Channel Type */ , ITEM_TYPE_CONTACT /* Item Type */); + private static final Map PROPERTY_NAME_TO_CHANNEL_INFO = Map.ofEntries( + Map.entry("glassBreakDetectionState", + new ChannelInfo("glassBreakDetectionState", "glassBreakDetectionState", + Constants.CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION, "Glass Break")), + Map.entry("beepingApplianceDetectionState", + new ChannelInfo("beepingApplianceDetectionState", "beepingApplianceDetectionState", + Constants.CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION, "Beeping Appliance")), + Map.entry("runningWaterDetectionState", + new ChannelInfo("runningWaterDetectionState", "runningWaterDetectionState", + Constants.CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION, "Running Water")), + Map.entry("dogBarkDetectionState", + new ChannelInfo("dogBarkDetectionState", "dogBarkDetectionState", + Constants.CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION, "Dog Bark")), + Map.entry("humanPresenceDetectionState", + new ChannelInfo("humanPresenceDetectionState", "humanPresenceDetectionState", + Constants.CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION, "Human Presence")), + Map.entry("smokeSirenDetectionState", + new ChannelInfo("smokeSirenDetectionState", "smokeSirenDetectionState", + Constants.CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION, "Smoke Siren")), + Map.entry("snoreDetectionState", + new ChannelInfo("snoreDetectionState", "snoreDetectionState", + Constants.CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION, "Snore")), + Map.entry("waterSoundsDetectionState", + new ChannelInfo("waterSoundsDetectionState", "waterSoundsDetectionState", + Constants.CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION, "Water Sounds")), + Map.entry("coughDetectionState", + new ChannelInfo("coughDetectionState", "coughDetectionState", + Constants.CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION, "Cough")), + Map.entry("carbonMonoxideSirenDetectionState", + new ChannelInfo("carbonMonoxideSirenDetectionState", "carbonMonoxideSirenDetectionState", + Constants.CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION, "Carbon Monoxide Siren")), + Map.entry("babyCryDetectionState", new ChannelInfo("babyCryDetectionState", "babyCryDetectionState", + Constants.CHANNEL_TYPE_UID_ACOUSTIC_EVENT_DETECTION, "Baby Cry"))); public HandlerAcousticEventSensor(SmartHomeDeviceHandler smartHomeDeviceHandler) { - super(smartHomeDeviceHandler); - } - - private ChannelInfo[] getAlarmChannels() { - return new ChannelInfo[] { GLASS_BREAK_DETECTION_STATE, SMOKE_ALARM_DETECTION_STATE }; - } - - @Override - public String[] getSupportedInterface() { - return new String[] { INTERFACE }; + super(smartHomeDeviceHandler, List.of(INTERFACE)); } @Override - protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { - for (ChannelInfo channelInfo : getAlarmChannels()) { - if (channelInfo.propertyName.equals(property)) { - return new ChannelInfo[] { channelInfo }; - } + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { + if (property == null) { + return Set.of(); + } + ChannelInfo channelInfo = PROPERTY_NAME_TO_CHANNEL_INFO.get(property); + if (channelInfo != null) { + return Set.of(channelInfo); } - return null; + return Set.of(); } @Override public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { - Boolean glassBreakDetectionStateValue = null; - Boolean smokeAlarmDetectionStateValue = null; for (JsonObject state : stateList) { - if (GLASS_BREAK_DETECTION_STATE.propertyName.equals(state.get("name").getAsString())) { - if (glassBreakDetectionStateValue == null) { - glassBreakDetectionStateValue = !"NOT_DETECTED" - .equals(state.get("value").getAsJsonObject().get("value").getAsString()); - } - } else if (SMOKE_ALARM_DETECTION_STATE.propertyName.equals(state.get("name").getAsString())) { - if (smokeAlarmDetectionStateValue == null) { - smokeAlarmDetectionStateValue = !"NOT_DETECTED" - .equals(state.get("value").getAsJsonObject().get("value").getAsString()); - } + String propertyName = state.get("name").getAsString(); + ChannelInfo channelInfo = PROPERTY_NAME_TO_CHANNEL_INFO.get(propertyName); + if (channelInfo != null) { + smartHomeDeviceHandler.updateState(channelInfo.channelId, + !"NOT_DETECTED".equals(state.get("value").getAsJsonObject().get("value").getAsString()) + ? OpenClosedType.CLOSED + : OpenClosedType.OPEN); } } - updateState(GLASS_BREAK_DETECTION_STATE.channelId, glassBreakDetectionStateValue == null ? UnDefType.UNDEF - : (glassBreakDetectionStateValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); - updateState(SMOKE_ALARM_DETECTION_STATE.channelId, smokeAlarmDetectionStateValue == null ? UnDefType.UNDEF - : (smokeAlarmDetectionStateValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); } @Override - public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) throws IOException { + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) throws IOException { return false; } - - @Override - public @Nullable StateDescription findStateDescription(String channelUID, StateDescription originalStateDescription, - @Nullable Locale locale) { - return null; - } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java deleted file mode 100644 index 7ab099ed5070d..0000000000000 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.amazonechocontrol.internal.smarthome; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; -import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.Properties; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.Property; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; -import org.openhab.core.thing.type.ChannelTypeUID; -import org.openhab.core.types.Command; -import org.openhab.core.types.State; -import org.openhab.core.types.StateDescription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.JsonObject; - -/** - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public abstract class HandlerBase { - // Logger - private final Logger logger = LoggerFactory.getLogger(HandlerBase.class); - - protected SmartHomeDeviceHandler smartHomeDeviceHandler; - protected Map channels = new HashMap<>(); - - public HandlerBase(SmartHomeDeviceHandler smartHomeDeviceHandler) { - this.smartHomeDeviceHandler = smartHomeDeviceHandler; - } - - protected abstract ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property); - - public abstract void updateChannels(String interfaceName, List stateList, UpdateChannelResult result); - - public abstract boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) - throws IOException, InterruptedException; - - public abstract @Nullable StateDescription findStateDescription(String channelId, - StateDescription originalStateDescription, @Nullable Locale locale); - - public boolean hasChannel(String channelId) { - return channels.containsKey(channelId); - } - - public abstract String[] getSupportedInterface(); - - SmartHomeDeviceHandler getSmartHomeDeviceHandler() { - return smartHomeDeviceHandler; - } - - public Collection initialize(List capabilities) { - Map channels = new HashMap<>(); - for (SmartHomeCapability capability : capabilities) { - Properties properties = capability.properties; - if (properties != null) { - List supported = Objects.requireNonNullElse(properties.supported, - List.of()); - logger.trace("{} | {}", capability.toString(), supported.toString()); - for (Property property : supported) { - String name = property.name; - if (name != null) { - ChannelInfo[] channelInfos = findChannelInfos(capability, name); - if (channelInfos != null) { - for (ChannelInfo channelInfo : channelInfos) { - if (channelInfo != null) { - channels.put(channelInfo.channelId, channelInfo); - } - } - } - } - - } - } - } - this.channels = channels; - return channels.values(); - } - - protected boolean containsCapabilityProperty(List capabilities, String propertyName) { - for (SmartHomeCapability capability : capabilities) { - Properties properties = capability.properties; - if (properties != null) { - List supported = Objects.requireNonNullElse(properties.supported, - List.of()); - if (supported.stream().anyMatch(p -> propertyName.equals(p.name))) { - return true; - } - } - } - return false; - } - - public void updateState(String channelId, State state) { - getSmartHomeDeviceHandler().updateState(channelId, state); - } - - public static class ChannelInfo { - public final String propertyName; - public final String channelId; - public final String itemType; - public ChannelTypeUID channelTypeUID; - - public ChannelInfo(String propertyName, String channelId, ChannelTypeUID channelTypeUID, String itemType) { - this.propertyName = propertyName; - this.channelId = channelId; - this.itemType = itemType; - this.channelTypeUID = channelTypeUID; - } - } - - public static class UpdateChannelResult { - public boolean needSingleUpdate; - } -} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java index 83735393d1051..014dba9e88590 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java @@ -12,66 +12,50 @@ */ package org.openhab.binding.amazonechocontrol.internal.smarthome; -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_DIMMER; - import java.io.IOException; import java.util.List; -import java.util.Locale; +import java.util.Map; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; -import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; -import org.openhab.core.types.StateDescription; import org.openhab.core.types.UnDefType; import com.google.gson.JsonObject; /** - * The {@link HandlerBrightnessController} is responsible for the Alexa.PowerControllerInterface + * The {@link HandlerBrightnessController} is responsible for the Alexa.PowerController interface * * @author Lukas Knoeller - Initial contribution * @author Michael Geramb - Initial contribution */ @NonNullByDefault -public class HandlerBrightnessController extends HandlerBase { - // Interface +public class HandlerBrightnessController extends AbstractInterfaceHandler { public static final String INTERFACE = "Alexa.BrightnessController"; - // Channel types - private static final ChannelTypeUID CHANNEL_TYPE_BRIGHTNESS = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "brightness"); - - // Channel definitions - private static final ChannelInfo BRIGHTNESS = new ChannelInfo("brightness" /* propertyName */ , - "brightness" /* ChannelId */, CHANNEL_TYPE_BRIGHTNESS /* Channel Type */ , - ITEM_TYPE_DIMMER /* Item Type */); + private static final ChannelInfo BRIGHTNESS = new ChannelInfo("brightness", "brightness", + Constants.CHANNEL_TYPE_BRIGHTNESS); private @Nullable Integer lastBrightness; public HandlerBrightnessController(SmartHomeDeviceHandler smartHomeDeviceHandler) { - super(smartHomeDeviceHandler); - } - - @Override - public String[] getSupportedInterface() { - return new String[] { INTERFACE }; + super(smartHomeDeviceHandler, List.of(INTERFACE)); } @Override - protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { if (BRIGHTNESS.propertyName.equals(property)) { - return new ChannelInfo[] { BRIGHTNESS }; + return Set.of(BRIGHTNESS); } - return null; + return Set.of(); } @Override @@ -91,12 +75,13 @@ public void updateChannels(String interfaceName, List stateList, Upd if (brightnessValue != null) { lastBrightness = brightnessValue; } - updateState(BRIGHTNESS.channelId, brightnessValue == null ? UnDefType.UNDEF : new PercentType(brightnessValue)); + smartHomeDeviceHandler.updateState(BRIGHTNESS.channelId, + brightnessValue == null ? UnDefType.UNDEF : new PercentType(brightnessValue)); } @Override - public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) throws IOException, InterruptedException { if (channelId.equals(BRIGHTNESS.channelId)) { if (containsCapabilityProperty(capabilities, BRIGHTNESS.propertyName)) { @@ -108,7 +93,8 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String newValue = 100; } this.lastBrightness = newValue; - connection.smartHomeCommand(entityId, "setBrightness", BRIGHTNESS.propertyName, newValue); + connection.smartHomeCommand(entityId, "setBrightness", + Map.of(BRIGHTNESS.propertyName, newValue)); return true; } } else if (command.equals(IncreaseDecreaseType.DECREASE)) { @@ -119,31 +105,26 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String newValue = 0; } this.lastBrightness = newValue; - connection.smartHomeCommand(entityId, "setBrightness", BRIGHTNESS.propertyName, newValue); + connection.smartHomeCommand(entityId, "setBrightness", + Map.of(BRIGHTNESS.propertyName, newValue)); return true; } } else if (command.equals(OnOffType.OFF)) { lastBrightness = 0; - connection.smartHomeCommand(entityId, "setBrightness", BRIGHTNESS.propertyName, 0); + connection.smartHomeCommand(entityId, "setBrightness", Map.of(BRIGHTNESS.propertyName, 0)); return true; } else if (command.equals(OnOffType.ON)) { lastBrightness = 100; - connection.smartHomeCommand(entityId, "setBrightness", BRIGHTNESS.propertyName, 100); + connection.smartHomeCommand(entityId, "setBrightness", Map.of(BRIGHTNESS.propertyName, 100)); return true; } else if (command instanceof PercentType) { lastBrightness = ((PercentType) command).intValue(); - connection.smartHomeCommand(entityId, "setBrightness", BRIGHTNESS.propertyName, - ((PercentType) command).floatValue() / 100); + connection.smartHomeCommand(entityId, "setBrightness", + Map.of(BRIGHTNESS.propertyName, ((PercentType) command).floatValue() / 100)); return true; } } } return false; } - - @Override - public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, - @Nullable Locale locale) { - return null; - } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java index c9d2e964520aa..c6870874fc116 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java @@ -12,90 +12,80 @@ */ package org.openhab.binding.amazonechocontrol.internal.smarthome; -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_COLOR; -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_STRING; - import java.io.IOException; +import java.math.BigDecimal; +import java.util.Comparator; import java.util.List; -import java.util.Locale; +import java.util.Map; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.StringType; -import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.Channel; import org.openhab.core.types.Command; +import org.openhab.core.types.CommandOption; import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragmentBuilder; import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.gson.JsonObject; /** - * The {@link HandlerColorController} is responsible for the Alexa.ColorTemperatureController + * The {@link HandlerColorController} is responsible for the Alexa.ColorTemperatureController interface * * @author Lukas Knoeller - Initial contribution * @author Michael Geramb - Initial contribution */ @NonNullByDefault -public class HandlerColorController extends HandlerBase { - // Interface +public class HandlerColorController extends AbstractInterfaceHandler { public static final String INTERFACE = "Alexa.ColorController"; public static final String INTERFACE_COLOR_PROPERTIES = "Alexa.ColorPropertiesController"; - // Channel types - private static final ChannelTypeUID CHANNEL_TYPE_COLOR_NAME = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "colorName"); - - private static final ChannelTypeUID CHANNEL_TYPE_COLOR = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "color"); + private static final ChannelInfo COLOR = new ChannelInfo("color", "color", Constants.CHANNEL_TYPE_COLOR); + private static final ChannelInfo COLOR_PROPERTIES = new ChannelInfo("colorProperties", "colorName", + Constants.CHANNEL_TYPE_COLOR_NAME); - // Channel and Properties - private static final ChannelInfo COLOR = new ChannelInfo("color" /* propertyName */, "color" /* ChannelId */, - CHANNEL_TYPE_COLOR /* Channel Type */, ITEM_TYPE_COLOR /* Item Type */); - - private static final ChannelInfo COLOR_PROPERTIES = new ChannelInfo("colorProperties" /* propertyName */, - "colorName" /* ChannelId */, CHANNEL_TYPE_COLOR_NAME /* Channel Type */, ITEM_TYPE_STRING /* Item Type */); + private final Logger logger = LoggerFactory.getLogger(HandlerColorController.class); private @Nullable HSBType lastColor; private @Nullable String lastColorName; + private boolean matchColors = false; public HandlerColorController(SmartHomeDeviceHandler smartHomeDeviceHandler) { - super(smartHomeDeviceHandler); + super(smartHomeDeviceHandler, List.of(INTERFACE, INTERFACE_COLOR_PROPERTIES)); } @Override - public String[] getSupportedInterface() { - return new String[] { INTERFACE, INTERFACE_COLOR_PROPERTIES }; - } - - @Override - protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { - if (COLOR.propertyName.contentEquals(property)) { - return new ChannelInfo[] { COLOR, COLOR_PROPERTIES }; + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { + if (COLOR.propertyName.equals(property)) { + return Set.of(COLOR, COLOR_PROPERTIES); } - return null; + return Set.of(); } @Override public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { if (INTERFACE.equals(interfaceName)) { - // WRITING TO THIS CHANNEL DOES CURRENTLY NOT WORK, BUT WE LEAVE THE CODE FOR FUTURE USE! HSBType colorValue = null; for (JsonObject state : stateList) { if (COLOR.propertyName.equals(state.get("name").getAsString())) { JsonObject value = state.get("value").getAsJsonObject(); // For groups take the maximum if (colorValue == null) { - colorValue = new HSBType(new DecimalType(value.get("hue").getAsInt()), - new PercentType(value.get("saturation").getAsInt() * 100), - new PercentType(value.get("brightness").getAsInt() * 100)); + colorValue = new HSBType(new DecimalType(value.get("hue").getAsDouble()), + new PercentType(BigDecimal.valueOf(value.get("saturation").getAsDouble() * 100.0)), + new PercentType(BigDecimal.valueOf(value.get("brightness").getAsDouble() * 100.0))); } } } @@ -105,7 +95,7 @@ public void updateChannels(String interfaceName, List stateList, Upd lastColor = colorValue; } } - updateState(COLOR.channelId, colorValue == null ? UnDefType.UNDEF : colorValue); + smartHomeDeviceHandler.updateState(COLOR.channelId, colorValue == null ? UnDefType.UNDEF : colorValue); } if (INTERFACE_COLOR_PROPERTIES.equals(interfaceName)) { String colorNameValue = null; @@ -121,25 +111,26 @@ public void updateChannels(String interfaceName, List stateList, Upd colorNameValue = lastColorName; } lastColorName = colorNameValue; - updateState(COLOR_PROPERTIES.channelId, + smartHomeDeviceHandler.updateState(COLOR_PROPERTIES.channelId, lastColorName == null ? UnDefType.UNDEF : new StringType(lastColorName)); } } @Override - public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) throws IOException, InterruptedException { if (channelId.equals(COLOR.channelId)) { - if (containsCapabilityProperty(capabilities, COLOR.propertyName)) { + if (matchColors) { if (command instanceof HSBType) { - HSBType color = ((HSBType) command); - JsonObject colorObject = new JsonObject(); - colorObject.addProperty("hue", color.getHue()); - colorObject.addProperty("saturation", color.getSaturation().floatValue() / 100); - colorObject.addProperty("brightness", color.getBrightness().floatValue() / 100); - connection.smartHomeCommand(entityId, "setColor", "value", colorObject); + HSBType color = (HSBType) command; + String colorName = AlexaColor.getClosestColorName(color); + lastColorName = colorName; + connection.smartHomeCommand(entityId, "setColor", Map.of("colorName", colorName)); + return true; } + } else { + logger.info("Discarding command to 'color' channel, read-only."); } } if (channelId.equals(COLOR_PROPERTIES.channelId)) { @@ -148,7 +139,7 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String String colorName = command.toFullString(); if (!colorName.isEmpty()) { lastColorName = colorName; - connection.smartHomeCommand(entityId, "setColor", "colorName", colorName); + connection.smartHomeCommand(entityId, "setColor", Map.of("colorName", colorName)); return true; } } @@ -158,8 +149,23 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String } @Override - public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, - @Nullable Locale locale) { + public @Nullable List getCommandDescription(Channel channel) { + String channelId = channel.getUID().getId(); + if (COLOR_PROPERTIES.channelId.equals(channelId)) { + return AmazonEchoControlBindingConstants.ALEXA_COLORS.stream() + .map(color -> new CommandOption(color.colorName, color.colorName)) + .sorted(Comparator.comparing(CommandOption::getCommand)).toList(); + } + return null; + } + + @Override + public @Nullable StateDescription getStateDescription(Channel channel) { + String channelId = channel.getUID().getId(); + if (COLOR.channelId.equals(channelId)) { + matchColors = channel.getConfiguration().as(ColorChannelConfig.class).matchColors; + return StateDescriptionFragmentBuilder.create().withReadOnly(!matchColors).build().toStateDescription(); + } return null; } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java index 85c48c266f27f..c138e15bfe546 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java @@ -12,77 +12,56 @@ */ package org.openhab.binding.amazonechocontrol.internal.smarthome; -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.*; - import java.io.IOException; import java.util.List; -import java.util.Locale; +import java.util.Map; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; import org.openhab.core.library.unit.Units; import org.openhab.core.thing.DefaultSystemChannelTypeProvider; -import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; -import org.openhab.core.types.StateDescription; import org.openhab.core.types.UnDefType; import com.google.gson.JsonObject; /** - * The {@link HandlerColorTemperatureController} is responsible for the Alexa.ColorTemperatureController + * The {@link HandlerColorTemperatureController} is responsible for the Alexa.ColorTemperatureController interface * * @author Lukas Knoeller - Initial contribution * @author Michael Geramb - Initial contribution */ @NonNullByDefault -public class HandlerColorTemperatureController extends HandlerBase { - // Interface +public class HandlerColorTemperatureController extends AbstractInterfaceHandler { public static final String INTERFACE = "Alexa.ColorTemperatureController"; public static final String INTERFACE_COLOR_PROPERTIES = "Alexa.ColorPropertiesController"; - // Channel types - private static final ChannelTypeUID CHANNEL_TYPE_COLOR_TEMPERATURE_NAME = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "colorTemperatureName"); - - private static final ChannelTypeUID CHANNEL_TYPE_COLOR_TEMPERATURE_IN_KELVIN = // - DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_COLOR_TEMPERATURE_ABS; - - // Channel and Properties - private static final ChannelInfo COLOR_TEMPERATURE_IN_KELVIN = new ChannelInfo( - "colorTemperatureInKelvin" /* propertyName */ , "colorTemperatureInKelvin" /* ChannelId */, - CHANNEL_TYPE_COLOR_TEMPERATURE_IN_KELVIN /* Channel Type */ , ITEM_TYPE_NUMBER_TEMPERATURE /* Item Type */); - - private static final ChannelInfo COLOR_TEMPERATURE_NAME = new ChannelInfo("colorProperties" /* propertyName */ , - "colorTemperatureName" /* ChannelId */, CHANNEL_TYPE_COLOR_TEMPERATURE_NAME /* Channel Type */ , - ITEM_TYPE_STRING /* Item Type */); + private static final ChannelInfo COLOR_TEMPERATURE_IN_KELVIN = new ChannelInfo("colorTemperatureInKelvin", + "colorTemperatureInKelvin", DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_COLOR_TEMPERATURE_ABS); + private static final ChannelInfo COLOR_TEMPERATURE_NAME = new ChannelInfo("colorProperties", "colorTemperatureName", + Constants.CHANNEL_TYPE_COLOR_TEMPERATURE_NAME); private @Nullable Integer lastColorTemperature; private @Nullable String lastColorName; public HandlerColorTemperatureController(SmartHomeDeviceHandler smartHomeDeviceHandler) { - super(smartHomeDeviceHandler); - } - - @Override - public String[] getSupportedInterface() { - return new String[] { INTERFACE, INTERFACE_COLOR_PROPERTIES }; + super(smartHomeDeviceHandler, List.of(INTERFACE, INTERFACE_COLOR_PROPERTIES)); } @Override - protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { - if (COLOR_TEMPERATURE_IN_KELVIN.propertyName.contentEquals(property)) { - return new ChannelInfo[] { COLOR_TEMPERATURE_IN_KELVIN, COLOR_TEMPERATURE_NAME }; + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { + if (COLOR_TEMPERATURE_IN_KELVIN.propertyName.equals(property)) { + return Set.of(COLOR_TEMPERATURE_IN_KELVIN, COLOR_TEMPERATURE_NAME); } - return null; + return Set.of(); } @Override @@ -102,8 +81,9 @@ public void updateChannels(String interfaceName, List stateList, Upd lastColorTemperature = colorTemperatureInKelvinValue; result.needSingleUpdate = true; } - updateState(COLOR_TEMPERATURE_IN_KELVIN.channelId, colorTemperatureInKelvinValue == null ? UnDefType.UNDEF - : new DecimalType(colorTemperatureInKelvinValue)); + smartHomeDeviceHandler.updateState(COLOR_TEMPERATURE_IN_KELVIN.channelId, + colorTemperatureInKelvinValue == null ? UnDefType.UNDEF + : new DecimalType(colorTemperatureInKelvinValue)); } if (INTERFACE_COLOR_PROPERTIES.equals(interfaceName)) { String colorTemperatureNameValue = null; @@ -120,14 +100,14 @@ public void updateChannels(String interfaceName, List stateList, Upd } else if (colorTemperatureNameValue == null && lastColorName != null) { colorTemperatureNameValue = lastColorName; } - updateState(COLOR_TEMPERATURE_NAME.channelId, + smartHomeDeviceHandler.updateState(COLOR_TEMPERATURE_NAME.channelId, colorTemperatureNameValue == null ? UnDefType.UNDEF : new StringType(colorTemperatureNameValue)); } } @Override - public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) throws IOException, InterruptedException { if (channelId.equals(COLOR_TEMPERATURE_IN_KELVIN.channelId)) { // WRITING TO THIS CHANNEL DOES CURRENTLY NOT WORK, BUT WE LEAVE THE CODE FOR FUTURE USE! @@ -146,7 +126,8 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String if (kelvin > 10000) { kelvin = 10000; } - connection.smartHomeCommand(entityId, "setColorTemperature", "colorTemperatureInKelvin", kelvin); + connection.smartHomeCommand(entityId, "setColorTemperature", + Map.of("colorTemperatureInKelvin", kelvin)); return true; } } @@ -157,8 +138,8 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String String colorTemperatureName = command.toFullString(); if (!colorTemperatureName.isEmpty()) { lastColorName = colorTemperatureName; - connection.smartHomeCommand(entityId, "setColorTemperature", "colorTemperatureName", - colorTemperatureName); + connection.smartHomeCommand(entityId, "setColorTemperature", + Map.of("colorTemperatureName", colorTemperatureName)); return true; } } @@ -166,10 +147,4 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String } return false; } - - @Override - public @Nullable StateDescription findStateDescription(String channelUID, StateDescription originalStateDescription, - @Nullable Locale locale) { - return null; - } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerContactSensor.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerContactSensor.java new file mode 100644 index 0000000000000..cad4424fefd5f --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerContactSensor.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.amazonechocontrol.internal.smarthome; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.types.Command; +import org.openhab.core.types.UnDefType; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerContactSensor} is responsible for the Alexa.ContactSensor interface + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class HandlerContactSensor extends AbstractInterfaceHandler { + public static final String INTERFACE = "Alexa.ContactSensor"; + + private static final ChannelInfo CONTACT_DETECTED_STATE = new ChannelInfo("detectionState", "detectionState", + Constants.CHANNEL_TYPE_CONTACT_STATUS); + + public HandlerContactSensor(SmartHomeDeviceHandler smartHomeDeviceHandler) { + super(smartHomeDeviceHandler, List.of(INTERFACE)); + } + + @Override + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { + if (CONTACT_DETECTED_STATE.propertyName.equals(property)) { + return Set.of(CONTACT_DETECTED_STATE); + } + return Set.of(); + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + OpenClosedType contactClosed = null; + for (JsonObject state : stateList) { + if (CONTACT_DETECTED_STATE.propertyName.equals(state.get("name").getAsString())) { + String value = state.get("value").getAsString(); + // For groups take true if all true + switch (value) { + case "NOT_DETECTED": + contactClosed = OpenClosedType.CLOSED; + break; + case "DETECTED": + contactClosed = OpenClosedType.OPEN; + break; + } + } + } + smartHomeDeviceHandler.updateState(CONTACT_DETECTED_STATE.channelId, + contactClosed == null ? UnDefType.UNDEF : contactClosed); + } + + @Override + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) + throws IOException, InterruptedException { + return false; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerEndpointHealth.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerEndpointHealth.java new file mode 100644 index 0000000000000..f00f1a29b9fad --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerEndpointHealth.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.DefaultSystemChannelTypeProvider; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * The {@link HandlerEndpointHealth} is responsible for the Alexa.EndpointHealth interface + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class HandlerEndpointHealth extends AbstractInterfaceHandler { + public static final String INTERFACE = "Alexa.EndpointHealth"; + + private static final ChannelInfo LOW_BATTERY_STATE = new ChannelInfo("batteryState", "lowBattery", + DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_LOW_BATTERY); + private static final ChannelInfo BATTERY_LEVEL_STATE = new ChannelInfo("batteryState", "batteryLevel", + DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_BATTERY_LEVEL); + private static final ChannelInfo CONNECTIVITY_STATE = new ChannelInfo("connectivity", "connectivity", + Constants.CHANNEL_TYPE_CONNECTIVITY); + + private final Logger logger = LoggerFactory.getLogger(HandlerEndpointHealth.class); + + public HandlerEndpointHealth(SmartHomeDeviceHandler smartHomeDeviceHandler) { + super(smartHomeDeviceHandler, List.of(INTERFACE)); + } + + @Override + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { + if (LOW_BATTERY_STATE.propertyName.equals(property)) { + return Set.of(LOW_BATTERY_STATE, BATTERY_LEVEL_STATE); + } else if (CONNECTIVITY_STATE.propertyName.equals(property)) { + return Set.of(CONNECTIVITY_STATE); + } + return Set.of(); + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + for (JsonObject state : stateList) { + String stateName = state.get("name").getAsString(); + if (LOW_BATTERY_STATE.propertyName.equals(stateName)) { + JsonObject batteryHealthObject = state.getAsJsonObject("value"); + if (batteryHealthObject != null) { + State lowBattery = UnDefType.UNDEF; + State batteryLevel = UnDefType.UNDEF; + // first try if we have health + JsonObject healthObject = batteryHealthObject.getAsJsonObject("health"); + if (healthObject != null) { + String status = healthObject.get("state").getAsString(); + lowBattery = OnOffType.from("OK".equals(status)); + } + // try if we know the percentage + JsonElement levelPercentage = batteryHealthObject.get("levelPercentage"); + if (levelPercentage != null) { + batteryLevel = new QuantityType<>(levelPercentage.getAsDouble(), Units.PERCENT); + if (UnDefType.UNDEF.equals(lowBattery)) { + lowBattery = OnOffType.from(levelPercentage.getAsInt() < 10); + } + } + updateState(LOW_BATTERY_STATE.channelId, lowBattery); + updateState(BATTERY_LEVEL_STATE.channelId, batteryLevel); + } + } else if (CONNECTIVITY_STATE.propertyName.equals(stateName)) { + JsonObject connectivityValueObject = state.get("value").getAsJsonObject(); + if (connectivityValueObject != null) { + String connectivityValue = connectivityValueObject.get("value").getAsString(); + if ("OK".equals(connectivityValue)) { + updateState(CONNECTIVITY_STATE.channelId, new StringType(connectivityValue)); + } else if (connectivityValue != null) { + String connectivityReason = "UNKNOWN"; + if (connectivityValueObject.has("reason")) { + connectivityReason = connectivityValueObject.get("reason").getAsString(); + } + updateState(CONNECTIVITY_STATE.channelId, + new StringType(connectivityValue + " / " + connectivityReason)); + } else { + updateState(CONNECTIVITY_STATE.channelId, UnDefType.UNDEF); + } + } + } + } + } + + @Override + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) + throws IOException, InterruptedException { + return false; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerHumiditySensor.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerHumiditySensor.java index 71ff0ec5a4b2c..6ba1ba815c1fc 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerHumiditySensor.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerHumiditySensor.java @@ -12,90 +12,80 @@ */ package org.openhab.binding.amazonechocontrol.internal.smarthome; -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.*; -import static org.openhab.core.library.unit.Units.*; +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.CHANNEL_TYPE_AIR_QUALITY_HUMIDITY; import java.io.IOException; +import java.math.BigDecimal; import java.util.List; -import java.util.Locale; +import java.util.Set; -import javax.measure.Unit; +import javax.measure.quantity.Dimensionless; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; import org.openhab.core.types.Command; -import org.openhab.core.types.State; -import org.openhab.core.types.StateDescription; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; /** - * The {@link HandlerHumiditySensor} is responsible for the Alexa.HumiditySensorInterface + * The {@link HandlerHumiditySensor} is responsible for the Alexa.HumiditySensor interface * - * @author Daniel Campbell - Initial contribution + * @author Jan N. Klug - Initial contribution */ @NonNullByDefault -public class HandlerHumiditySensor extends HandlerBase { - // Logger +public class HandlerHumiditySensor extends AbstractInterfaceHandler { private final Logger logger = LoggerFactory.getLogger(HandlerHumiditySensor.class); - // Interface public static final String INTERFACE = "Alexa.HumiditySensor"; - // Channel definitions - private static final ChannelInfo HUMIDITY = new ChannelInfo("relativeHumidity" /* propertyName */ , - "relativeHumidity" /* ChannelId */, CHANNEL_TYPE_HUMIDITY /* Channel Type */ , - ITEM_TYPE_HUMIDITY /* Item Type */); - public HandlerHumiditySensor(SmartHomeDeviceHandler smartHomeDeviceHandler) { - super(smartHomeDeviceHandler); - } + private static final ChannelInfo HUMIDITY = new ChannelInfo("relativeHumidity", "relativeHumidity", + CHANNEL_TYPE_AIR_QUALITY_HUMIDITY); - @Override - public String[] getSupportedInterface() { - return new String[] { INTERFACE }; + public HandlerHumiditySensor(SmartHomeDeviceHandler smartHomeDeviceHandler) { + super(smartHomeDeviceHandler, List.of(INTERFACE)); } @Override - protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { if (HUMIDITY.propertyName.equals(property)) { - return new ChannelInfo[] { HUMIDITY }; + return Set.of(HUMIDITY); } - return null; + return Set.of(); } @Override public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + QuantityType humidityValue = null; for (JsonObject state : stateList) { - State humidityValue = null; - logger.debug("Updating {} with state: {}", interfaceName, state.toString()); - if (HUMIDITY.propertyName.equals(state.get("name").getAsString())) { - // For groups take the first - humidityValue = getQuantityTypeState(state.get("value").getAsInt(), PERCENT); - updateState(HUMIDITY.channelId, humidityValue == null ? UnDefType.UNDEF : humidityValue); + if (HUMIDITY.propertyName.equals(state.get("name").getAsString()) && humidityValue == null) { + JsonElement value = state.get("value"); + BigDecimal humidity; + if (value.isJsonObject()) { + humidity = value.getAsJsonObject().getAsBigDecimal(); + } else if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isNumber()) { + humidity = value.getAsJsonPrimitive().getAsBigDecimal(); + } else { + logger.warn("Could not properly convert {}", state); + continue; + } + humidityValue = new QuantityType<>(humidity, Units.PERCENT); } } - } - - protected State getQuantityTypeState(@Nullable Number value, Unit unit) { - return (value == null) ? UnDefType.UNDEF : new QuantityType<>(value, unit); + smartHomeDeviceHandler.updateState(HUMIDITY.channelId, humidityValue == null ? UnDefType.UNDEF : humidityValue); } @Override - public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) throws IOException { + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) throws IOException { return false; } - - @Override - public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, - @Nullable Locale locale) { - return null; - } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerLocation.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerLocation.java new file mode 100644 index 0000000000000..a4ff25665ffa6 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerLocation.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.PointType; +import org.openhab.core.types.Command; +import org.openhab.core.types.UnDefType; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * The {@link HandlerLocation} is responsible for the Alexa.Location interface + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class HandlerLocation extends AbstractInterfaceHandler { + public static final String INTERFACE = "Alexa.Location"; + + private static final ChannelInfo GEOLOCATION_STATE = new ChannelInfo("geolocation", "geoLocation", + Constants.CHANNEL_TYPE_GEOLOCATION); + + public HandlerLocation(SmartHomeDeviceHandler smartHomeDeviceHandler) { + super(smartHomeDeviceHandler, List.of(INTERFACE)); + } + + @Override + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { + if (GEOLOCATION_STATE.propertyName.equals(property)) { + return Set.of(GEOLOCATION_STATE); + } + return Set.of(); + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + for (JsonObject state : stateList) { + if (GEOLOCATION_STATE.propertyName.equals(state.get("name").getAsString())) { + JsonElement coordinateElement = state.get("value").getAsJsonObject().get("coordinate"); + if (coordinateElement != null) { + JsonObject coordinate = coordinateElement.getAsJsonObject(); + Double latitude = coordinate.has("latitudeInDegrees") + ? coordinate.get("latitudeInDegrees").getAsDouble() + : null; + Double longitude = coordinate.has("longitudeInDegrees") + ? coordinate.get("longitudeInDegrees").getAsDouble() + : null; + if (latitude != null && longitude != null) { + updateState(GEOLOCATION_STATE.channelId, + new PointType(new DecimalType(latitude), new DecimalType(longitude))); + } else { + updateState(GEOLOCATION_STATE.channelId, UnDefType.UNDEF); + } + } + } + } + } + + @Override + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) + throws IOException, InterruptedException { + return false; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerLockController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerLockController.java new file mode 100644 index 0000000000000..321379bbdc211 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerLockController.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.Command; +import org.openhab.core.types.UnDefType; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerLockController} is responsible for the Alexa.LockController interface + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class HandlerLockController extends AbstractInterfaceHandler { + public static final String INTERFACE = "Alexa.LockController"; + + private static final ChannelInfo LOCK_STATE = new ChannelInfo("lockState", "lockState", + Constants.CHANNEL_TYPE_LOCK_STATE); + + public HandlerLockController(SmartHomeDeviceHandler smartHomeDeviceHandler) { + super(smartHomeDeviceHandler, List.of(INTERFACE)); + } + + @Override + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { + if (LOCK_STATE.propertyName.equals(property)) { + return Set.of(LOCK_STATE); + } + return Set.of(); + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + Boolean lockStateValue = null; + for (JsonObject state : stateList) { + if (LOCK_STATE.propertyName.equals(state.get("name").getAsString())) { + String value = state.get("value").getAsString(); + lockStateValue = "LOCKED".equals(value); + + } + } + smartHomeDeviceHandler.updateState(LOCK_STATE.channelId, + lockStateValue == null ? UnDefType.UNDEF : OnOffType.from(lockStateValue)); + } + + @Override + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) + throws IOException, InterruptedException { + if (channelId.equals(LOCK_STATE.channelId)) { + if (containsCapabilityProperty(capabilities, LOCK_STATE.propertyName)) { + if (command.equals(OnOffType.ON)) { + connection.smartHomeCommand(entityId, "lockAction", Map.of("targetLockState.value", "LOCKED")); + return true; + } else if (command.equals(OnOffType.OFF)) { + connection.smartHomeCommand(entityId, "lockAction", Map.of("targetLockState.value", "UNLOCKED")); + return true; + } + } + } + return false; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerMotionSensor.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerMotionSensor.java new file mode 100644 index 0000000000000..a0744bd43ec2e --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerMotionSensor.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.amazonechocontrol.internal.smarthome; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.Command; +import org.openhab.core.types.UnDefType; + +import com.google.gson.JsonObject; + +/** + * The {@link HandlerMotionSensor} is responsible for the Alexa.MotionSensor interface + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class HandlerMotionSensor extends AbstractInterfaceHandler { + public static final String INTERFACE = "Alexa.MotionSensor"; + + private static final ChannelInfo MOTION_DETECTED_STATE = new ChannelInfo("detectionState", "detectionState", + Constants.CHANNEL_TYPE_MOTION_DETECTED); + + public HandlerMotionSensor(SmartHomeDeviceHandler smartHomeDeviceHandler) { + super(smartHomeDeviceHandler, List.of(INTERFACE)); + } + + @Override + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { + if (MOTION_DETECTED_STATE.propertyName.equals(property)) { + return Set.of(MOTION_DETECTED_STATE); + } + return Set.of(); + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + OnOffType motionDetectedValue = null; + for (JsonObject state : stateList) { + if (MOTION_DETECTED_STATE.propertyName.equals(state.get("name").getAsString())) { + String value = state.get("value").getAsString(); + switch (value) { + case "NOT_DETECTED": + motionDetectedValue = OnOffType.OFF; + break; + case "DETECTED": + motionDetectedValue = OnOffType.ON; + break; + } + } + } + smartHomeDeviceHandler.updateState(MOTION_DETECTED_STATE.channelId, + motionDetectedValue == null ? UnDefType.UNDEF : motionDetectedValue); + } + + @Override + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) + throws IOException, InterruptedException { + return false; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java index 49bc4d52af643..775eb675d7948 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java @@ -12,25 +12,21 @@ */ package org.openhab.binding.amazonechocontrol.internal.smarthome; -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_DIMMER; - import java.io.IOException; import java.util.List; -import java.util.Locale; +import java.util.Map; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; -import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; -import org.openhab.core.types.StateDescription; import org.openhab.core.types.UnDefType; import com.google.gson.JsonObject; @@ -42,36 +38,24 @@ * @author Michael Geramb - Initial contribution */ @NonNullByDefault -public class HandlerPercentageController extends HandlerBase { - // Interface +public class HandlerPercentageController extends AbstractInterfaceHandler { public static final String INTERFACE = "Alexa.PercentageController"; - // Channel types - private static final ChannelTypeUID CHANNEL_TYPE_PERCENTAGE = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "percentage"); - - // Channel definitions - private static final ChannelInfo PERCENTAGE = new ChannelInfo("percentage" /* propertyName */ , - "percentage" /* ChannelId */, CHANNEL_TYPE_PERCENTAGE /* Channel Type */ , - ITEM_TYPE_DIMMER /* Item Type */); + private static final ChannelInfo PERCENTAGE = new ChannelInfo("percentage", "percentage", + Constants.CHANNEL_TYPE_PERCENTAGE); private @Nullable Integer lastPercentage; public HandlerPercentageController(SmartHomeDeviceHandler smartHomeDeviceHandler) { - super(smartHomeDeviceHandler); - } - - @Override - public String[] getSupportedInterface() { - return new String[] { INTERFACE }; + super(smartHomeDeviceHandler, List.of(INTERFACE)); } @Override - protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { if (PERCENTAGE.propertyName.equals(property)) { - return new ChannelInfo[] { PERCENTAGE }; + return Set.of(PERCENTAGE); } - return null; + return Set.of(); } @Override @@ -91,12 +75,13 @@ public void updateChannels(String interfaceName, List stateList, Upd if (percentageValue != null) { lastPercentage = percentageValue; } - updateState(PERCENTAGE.channelId, percentageValue == null ? UnDefType.UNDEF : new PercentType(percentageValue)); + smartHomeDeviceHandler.updateState(PERCENTAGE.channelId, + percentageValue == null ? UnDefType.UNDEF : new PercentType(percentageValue)); } @Override - public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) throws IOException, InterruptedException { if (channelId.equals(PERCENTAGE.channelId)) { if (containsCapabilityProperty(capabilities, PERCENTAGE.propertyName)) { @@ -108,7 +93,8 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String newValue = 100; } this.lastPercentage = newValue; - connection.smartHomeCommand(entityId, "setPercentage", PERCENTAGE.propertyName, newValue); + connection.smartHomeCommand(entityId, "setPercentage", + Map.of(PERCENTAGE.propertyName, newValue)); return true; } } else if (command.equals(IncreaseDecreaseType.DECREASE)) { @@ -119,30 +105,27 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String newValue = 0; } this.lastPercentage = newValue; - connection.smartHomeCommand(entityId, "setPercentage", PERCENTAGE.propertyName, newValue); + connection.smartHomeCommand(entityId, "setPercentage", + Map.of(PERCENTAGE.propertyName, newValue)); return true; } } else if (command.equals(OnOffType.OFF)) { lastPercentage = 0; - connection.smartHomeCommand(entityId, "setPercentage", PERCENTAGE.propertyName, 0); + connection.smartHomeCommand(entityId, "setPercentage", Map.of(PERCENTAGE.propertyName, 0)); return true; } else if (command.equals(OnOffType.ON)) { lastPercentage = 100; - connection.smartHomeCommand(entityId, "setPercentage", PERCENTAGE.propertyName, 100); + connection.smartHomeCommand(entityId, "setPercentage", Map.of(PERCENTAGE.propertyName, 100)); return true; } else if (command instanceof PercentType) { - lastPercentage = ((PercentType) command).intValue(); - connection.smartHomeCommand(entityId, "setPercentage", PERCENTAGE.propertyName, lastPercentage); + Integer lastPercentage = ((PercentType) command).intValue(); + connection.smartHomeCommand(entityId, "setPercentage", + Map.of(PERCENTAGE.propertyName, lastPercentage)); + this.lastPercentage = lastPercentage; return true; } } } return false; } - - @Override - public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, - @Nullable Locale locale) { - return null; - } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java index 3804856e02d94..2b7ad6a4c0b5b 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java @@ -12,26 +12,20 @@ */ package org.openhab.binding.amazonechocontrol.internal.smarthome; -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_SWITCH; - import java.io.IOException; import java.util.List; -import java.util.Locale; +import java.util.Map; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.core.library.types.OnOffType; -import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; -import org.openhab.core.types.StateDescription; import org.openhab.core.types.UnDefType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.google.gson.JsonObject; @@ -42,41 +36,26 @@ * @author Michael Geramb - Initial contribution */ @NonNullByDefault -public class HandlerPowerController extends HandlerBase { - private final Logger logger = LoggerFactory.getLogger(HandlerPowerController.class); - - // Interface +public class HandlerPowerController extends AbstractInterfaceHandler { public static final String INTERFACE = "Alexa.PowerController"; - // Channel types - private static final ChannelTypeUID CHANNEL_TYPE_POWER_STATE = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "powerState"); - - // Channel definitions - private static final ChannelInfo POWER_STATE = new ChannelInfo("powerState" /* propertyName */ , - "powerState" /* ChannelId */, CHANNEL_TYPE_POWER_STATE /* Channel Type */ , - ITEM_TYPE_SWITCH /* Item Type */); + private static final ChannelInfo POWER_STATE = new ChannelInfo("powerState", "powerState", + Constants.CHANNEL_TYPE_POWER_STATE); public HandlerPowerController(SmartHomeDeviceHandler smartHomeDeviceHandler) { - super(smartHomeDeviceHandler); + super(smartHomeDeviceHandler, List.of(INTERFACE)); } @Override - public String[] getSupportedInterface() { - return new String[] { INTERFACE }; - } - - @Override - protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { if (POWER_STATE.propertyName.equals(property)) { - return new ChannelInfo[] { POWER_STATE }; + return Set.of(POWER_STATE); } - return null; + return Set.of(); } @Override public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { - logger.trace("{} received {}", this.smartHomeDeviceHandler.getId(), stateList); Boolean powerStateValue = null; for (JsonObject state : stateList) { if (POWER_STATE.propertyName.equals(state.get("name").getAsString())) { @@ -85,31 +64,25 @@ public void updateChannels(String interfaceName, List stateList, Upd powerStateValue = "ON".equals(value); } } - logger.trace("{} final state {}", this.smartHomeDeviceHandler.getId(), powerStateValue); - updateState(POWER_STATE.channelId, powerStateValue == null ? UnDefType.UNDEF : OnOffType.from(powerStateValue)); + smartHomeDeviceHandler.updateState(POWER_STATE.channelId, + powerStateValue == null ? UnDefType.UNDEF : OnOffType.from(powerStateValue)); } @Override - public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) throws IOException, InterruptedException { if (channelId.equals(POWER_STATE.channelId)) { if (containsCapabilityProperty(capabilities, POWER_STATE.propertyName)) { if (command.equals(OnOffType.ON)) { - connection.smartHomeCommand(entityId, "turnOn"); + connection.smartHomeCommand(entityId, "turnOn", Map.of()); return true; } else if (command.equals(OnOffType.OFF)) { - connection.smartHomeCommand(entityId, "turnOff"); + connection.smartHomeCommand(entityId, "turnOff", Map.of()); return true; } } } return false; } - - @Override - public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, - @Nullable Locale locale) { - return null; - } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java index 74356f47b1470..9228ff6937dbf 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java @@ -12,25 +12,21 @@ */ package org.openhab.binding.amazonechocontrol.internal.smarthome; -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_DIMMER; - import java.io.IOException; import java.util.List; -import java.util.Locale; +import java.util.Map; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; -import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; -import org.openhab.core.types.StateDescription; import org.openhab.core.types.UnDefType; import com.google.gson.JsonObject; @@ -42,36 +38,24 @@ * @author Michael Geramb - Initial contribution */ @NonNullByDefault -public class HandlerPowerLevelController extends HandlerBase { - // Interface +public class HandlerPowerLevelController extends AbstractInterfaceHandler { public static final String INTERFACE = "Alexa.PowerLevelController"; - // Channel types - private static final ChannelTypeUID CHANNEL_TYPE_POWER_LEVEL = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "powerLevel"); - - // Channel definitions - private static final ChannelInfo POWER_LEVEL = new ChannelInfo("powerLevel" /* propertyName */ , - "powerLevel" /* ChannelId */, CHANNEL_TYPE_POWER_LEVEL /* Channel Type */ , - ITEM_TYPE_DIMMER /* Item Type */); + private static final ChannelInfo POWER_LEVEL = new ChannelInfo("powerLevel", "powerLevel", + Constants.CHANNEL_TYPE_POWER_LEVEL); private @Nullable Integer lastPowerLevel; public HandlerPowerLevelController(SmartHomeDeviceHandler smartHomeDeviceHandler) { - super(smartHomeDeviceHandler); - } - - @Override - public String[] getSupportedInterface() { - return new String[] { INTERFACE }; + super(smartHomeDeviceHandler, List.of(INTERFACE)); } @Override - protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { if (POWER_LEVEL.propertyName.equals(property)) { - return new ChannelInfo[] { POWER_LEVEL }; + return Set.of(POWER_LEVEL); } - return null; + return Set.of(); } @Override @@ -91,13 +75,13 @@ public void updateChannels(String interfaceName, List stateList, Upd if (powerLevelValue != null) { lastPowerLevel = powerLevelValue; } - updateState(POWER_LEVEL.channelId, + smartHomeDeviceHandler.updateState(POWER_LEVEL.channelId, powerLevelValue == null ? UnDefType.UNDEF : new PercentType(powerLevelValue)); } @Override - public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) throws IOException, InterruptedException { if (channelId.equals(POWER_LEVEL.channelId)) { if (containsCapabilityProperty(capabilities, POWER_LEVEL.propertyName)) { @@ -109,7 +93,8 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String newValue = 100; } this.lastPowerLevel = newValue; - connection.smartHomeCommand(entityId, "setPowerLevel", POWER_LEVEL.propertyName, newValue); + connection.smartHomeCommand(entityId, "setPowerLevel", + Map.of(POWER_LEVEL.propertyName, newValue)); return true; } } else if (command.equals(IncreaseDecreaseType.DECREASE)) { @@ -120,31 +105,26 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String newValue = 0; } this.lastPowerLevel = newValue; - connection.smartHomeCommand(entityId, "setPowerLevel", POWER_LEVEL.propertyName, newValue); + connection.smartHomeCommand(entityId, "setPowerLevel", + Map.of(POWER_LEVEL.propertyName, newValue)); return true; } } else if (command.equals(OnOffType.OFF)) { lastPowerLevel = 0; - connection.smartHomeCommand(entityId, "setPowerLevel", POWER_LEVEL.propertyName, 0); + connection.smartHomeCommand(entityId, "setPowerLevel", Map.of(POWER_LEVEL.propertyName, 0)); return true; } else if (command.equals(OnOffType.ON)) { lastPowerLevel = 100; - connection.smartHomeCommand(entityId, "setPowerLevel", POWER_LEVEL.propertyName, 100); + connection.smartHomeCommand(entityId, "setPowerLevel", Map.of(POWER_LEVEL.propertyName, 100)); return true; } else if (command instanceof PercentType) { lastPowerLevel = ((PercentType) command).intValue(); - connection.smartHomeCommand(entityId, "setPowerLevel", POWER_LEVEL.propertyName, - ((PercentType) command).floatValue() / 100); + connection.smartHomeCommand(entityId, "setPowerLevel", + Map.of(POWER_LEVEL.propertyName, ((PercentType) command).floatValue() / 100)); return true; } } } return false; } - - @Override - public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, - @Nullable Locale locale) { - return null; - } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerRangeController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerRangeController.java new file mode 100644 index 0000000000000..d9a3963b80b44 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerRangeController.java @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.CHANNEL_TYPE_AIR_QUALITY_CARBON_MONOXIDE; +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.CHANNEL_TYPE_AIR_QUALITY_HUMIDITY; +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.CHANNEL_TYPE_AIR_QUALITY_INDOOR_AIR_QUALITY; +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.CHANNEL_TYPE_AIR_QUALITY_PM25; +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.CHANNEL_TYPE_AIR_QUALITY_VOC; +import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.CHANNEL_TYPE_FAN_SPEED; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * The {@link HandlerRangeController} is responsible for the Alexa.RangeController interface + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class HandlerRangeController extends AbstractInterfaceHandler { + public static final String INTERFACE = "Alexa.RangeController"; + + private final Logger logger = LoggerFactory.getLogger(HandlerRangeController.class); + + private static final ChannelInfo FAN_SPEED = new ChannelInfo("Alexa.Setting.FanSpeed", "fanSpeed", + CHANNEL_TYPE_FAN_SPEED); + + private static final Map CHANNEL_INFO_MAP = Map.ofEntries( + Map.entry("Alexa.AirQuality.IndoorAirQuality", + new ChannelInfo("Alexa.AirQuality.IndoorAirQuality", "indoorAirQuality", + CHANNEL_TYPE_AIR_QUALITY_INDOOR_AIR_QUALITY)), + Map.entry("Alexa.AirQuality.Humidity", + new ChannelInfo("Alexa.AirQuality.Humidity", "humidity", CHANNEL_TYPE_AIR_QUALITY_HUMIDITY)), + Map.entry("Alexa.AirQuality.ParticulateMatter", + new ChannelInfo("Alexa.AirQuality.ParticulateMatter", "pm25", CHANNEL_TYPE_AIR_QUALITY_PM25)), + Map.entry("Alexa.AirQuality.VolatileOrganicCompounds", + new ChannelInfo("Alexa.AirQuality.VolatileOrganicCompounds", "voc", CHANNEL_TYPE_AIR_QUALITY_VOC)), + Map.entry("Alexa.AirQuality.CarbonMonoxide", new ChannelInfo("Alexa.AirQuality.CarbonMonoxide", + "carbonMonoxide", CHANNEL_TYPE_AIR_QUALITY_CARBON_MONOXIDE)), + Map.entry("Alexa.Setting.FanSpeed", FAN_SPEED)); + + private static final Map> ALEXA_UNITS_TO_SMART_HOME_UNITS = Map.of( // + "Alexa.Unit.Percent", Units.PERCENT, // + "Alexa.Unit.Density.MicroGramsPerCubicMeter", Units.MICROGRAM_PER_CUBICMETRE, // + "Alexa.Unit.PartsPerMillion", Units.PARTS_PER_MILLION); + + private final Map instanceToChannelInfo = new HashMap<>(); + private final Map> instanceToUnit = new HashMap<>(); + + public HandlerRangeController(SmartHomeDeviceHandler smartHomeDeviceHandler) { + super(smartHomeDeviceHandler, List.of(INTERFACE)); + } + + @Override + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { + JsonSmartHomeCapability.Resources resources = capability.resources; + String instance = capability.instance; + if (resources == null || instance == null) { // instance is needed to identify state updates + return Set.of(); + } + + List names = resources.friendlyNames; + if (names == null) { + return Set.of(); + } + + JsonSmartHomeCapability.Resources.Names.Value nameValue = names.stream().filter(n -> "asset".equals(n.type)) + .findAny().map(n -> n.value).orElse(null); + if (nameValue == null) { + return Set.of(); + } + + String assetId = nameValue.assetId; + if (assetId == null) { + return Set.of(); + } + + ChannelInfo channelInfo = CHANNEL_INFO_MAP.get(assetId); + if (channelInfo != null) { + instanceToChannelInfo.put(instance, channelInfo); + // try to get configuration if present + JsonSmartHomeCapability.Configuration configuration = capability.configuration; + if (configuration != null) { + String alexaUnit = configuration.unitOfMeasure; + if (alexaUnit != null) { + Unit unit = ALEXA_UNITS_TO_SMART_HOME_UNITS.get(alexaUnit); + if (unit != null) { + instanceToUnit.put(instance, unit); + } + } + } + return Set.of(channelInfo); + } + + return Set.of(); + } + + @Override + public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { + for (JsonObject state : stateList) { + JsonElement instanceElement = state.get("instance"); + JsonElement valueElement = state.get("value"); + if (instanceElement == null || valueElement == null) { + logger.trace("Could not identify instance in {}, skipping update.", state); + continue; + } + String instance = instanceElement.getAsString(); + ChannelInfo channelInfo = instanceToChannelInfo.get(instance); + if (channelInfo != null) { + double value = valueElement.getAsDouble(); + Unit unit = instanceToUnit.get(instance); + if (unit != null) { + // if unit is present, use QuantityType; + smartHomeDeviceHandler.updateState(channelInfo.channelId, new QuantityType<>(value, unit)); + } else { + // fallback is DecimalType + smartHomeDeviceHandler.updateState(channelInfo.channelId, new DecimalType(value)); + } + } + } + } + + @Override + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) + throws IOException, InterruptedException { + if (FAN_SPEED.channelId.equals(channelId) && command instanceof DecimalType) { + Double value = ((DecimalType) command).doubleValue(); + JsonObject valueElement = new JsonObject(); + valueElement.addProperty("value", value); + connection.smartHomeCommand(entityId, "setRangeValue", + Map.of("instance", "FanSpeed", "rangeValue", valueElement)); + } + return false; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java index aeb0f60432c10..20494aa031c21 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java @@ -12,25 +12,20 @@ */ package org.openhab.binding.amazonechocontrol.internal.smarthome; -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_CONTACT; -import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.ITEM_TYPE_STRING; - import java.io.IOException; import java.util.List; -import java.util.Locale; +import java.util.Map; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.library.types.StringType; -import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; -import org.openhab.core.types.StateDescription; import org.openhab.core.types.UnDefType; import com.google.gson.JsonObject; @@ -42,70 +37,43 @@ * @author Michael Geramb - Initial contribution */ @NonNullByDefault -public class HandlerSecurityPanelController extends HandlerBase { - // Interface +public class HandlerSecurityPanelController extends AbstractInterfaceHandler { public static final String INTERFACE = "Alexa.SecurityPanelController"; - // Channel types - private static final ChannelTypeUID CHANNEL_TYPE_ARM_STATE = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "armState"); - - private static final ChannelTypeUID CHANNEL_TYPE_BURGLARY_ALARM = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "burglaryAlarm"); - - private static final ChannelTypeUID CHANNEL_TYPE_CARBON_MONOXIDE_ALARM = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "carbonMonoxideAlarm"); - - private static final ChannelTypeUID CHANNEL_TYPE_FIRE_ALARM = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "fireAlarm"); - - private static final ChannelTypeUID CHANNEL_TYPE_WATER_ALARM = new ChannelTypeUID( - AmazonEchoControlBindingConstants.BINDING_ID, "waterAlarm"); - // Channel definitions - private static final ChannelInfo ARM_STATE = new ChannelInfo("armState" /* propertyName */ , - "armState" /* ChannelId */, CHANNEL_TYPE_ARM_STATE /* Channel Type */ , ITEM_TYPE_STRING /* Item Type */); + private static final ChannelInfo ARM_STATE = new ChannelInfo("armState", "armState", + Constants.CHANNEL_TYPE_ARM_STATE); - private static final ChannelInfo BURGLARY_ALARM = new ChannelInfo("burglaryAlarm" /* propertyName */ , - "burglaryAlarm" /* ChannelId */, CHANNEL_TYPE_BURGLARY_ALARM /* Channel Type */ , - ITEM_TYPE_CONTACT /* Item Type */); + private static final ChannelInfo BURGLARY_ALARM = new ChannelInfo("burglaryAlarm", "burglaryAlarm", + Constants.CHANNEL_TYPE_BURGLARY_ALARM); - private static final ChannelInfo CARBON_MONOXIDE_ALARM = new ChannelInfo("carbonMonoxideAlarm" /* propertyName */ , - "carbonMonoxideAlarm" /* ChannelId */, CHANNEL_TYPE_CARBON_MONOXIDE_ALARM /* Channel Type */ , - ITEM_TYPE_CONTACT /* Item Type */); + private static final ChannelInfo CARBON_MONOXIDE_ALARM = new ChannelInfo("carbonMonoxideAlarm", + "carbonMonoxideAlarm", Constants.CHANNEL_TYPE_CARBON_MONOXIDE_ALARM); - private static final ChannelInfo FIRE_ALARM = new ChannelInfo("fireAlarm" /* propertyName */ , - "fireAlarm" /* ChannelId */, CHANNEL_TYPE_FIRE_ALARM /* Channel Type */ , - ITEM_TYPE_CONTACT /* Item Type */); + private static final ChannelInfo FIRE_ALARM = new ChannelInfo("fireAlarm", "fireAlarm", + Constants.CHANNEL_TYPE_FIRE_ALARM); - private static final ChannelInfo WATER_ALARM = new ChannelInfo("waterAlarm" /* propertyName */ , - "waterAlarm" /* ChannelId */, CHANNEL_TYPE_WATER_ALARM /* Channel Type */ , - ITEM_TYPE_CONTACT /* Item Type */); + private static final ChannelInfo WATER_ALARM = new ChannelInfo("waterAlarm", "waterAlarm", + Constants.CHANNEL_TYPE_WATER_ALARM); - public HandlerSecurityPanelController(SmartHomeDeviceHandler smartHomeDeviceHandler) { - super(smartHomeDeviceHandler); - } + private static final Set ALARM_CHANNELS = Set.of(BURGLARY_ALARM, CARBON_MONOXIDE_ALARM, FIRE_ALARM, + WATER_ALARM); - private ChannelInfo[] getAlarmChannels() { - return new ChannelInfo[] { BURGLARY_ALARM, CARBON_MONOXIDE_ALARM, FIRE_ALARM, WATER_ALARM }; - } - - @Override - public String[] getSupportedInterface() { - return new String[] { INTERFACE }; + public HandlerSecurityPanelController(SmartHomeDeviceHandler smartHomeDeviceHandler) { + super(smartHomeDeviceHandler, List.of(INTERFACE)); } @Override - protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { if (ARM_STATE.propertyName.equals(property)) { - return new ChannelInfo[] { ARM_STATE }; + return Set.of(ARM_STATE); } - for (ChannelInfo channelInfo : getAlarmChannels()) { + for (ChannelInfo channelInfo : ALARM_CHANNELS) { if (channelInfo.propertyName.equals(property)) { - return new ChannelInfo[] { channelInfo }; + return Set.of(channelInfo); } } - return null; + return Set.of(); } @Override @@ -116,50 +84,54 @@ public void updateChannels(String interfaceName, List stateList, Upd Boolean fireAlarmValue = null; Boolean waterAlarmValue = null; for (JsonObject state : stateList) { - if (ARM_STATE.propertyName.equals(state.get("name").getAsString())) { + String propertyValue = state.get("value").getAsJsonObject().get("value").getAsString(); + String propertyName = state.get("name").getAsString(); + if (ARM_STATE.propertyName.equals(propertyName)) { if (armStateValue == null) { armStateValue = state.get("value").getAsString(); } - } else if (BURGLARY_ALARM.propertyName.equals(state.get("name").getAsString())) { + } else if (BURGLARY_ALARM.propertyName.equals(propertyName)) { if (burglaryAlarmValue == null) { - burglaryAlarmValue = "ALARM".equals(state.get("value").getAsString()); + burglaryAlarmValue = "ALARM".equals(propertyValue); } - } else if (CARBON_MONOXIDE_ALARM.propertyName.equals(state.get("name").getAsString())) { + } else if (CARBON_MONOXIDE_ALARM.propertyName.equals(propertyName)) { if (carbonMonoxideAlarmValue == null) { - carbonMonoxideAlarmValue = "ALARM".equals(state.get("value").getAsString()); + carbonMonoxideAlarmValue = "ALARM".equals(propertyValue); } - } else if (FIRE_ALARM.propertyName.equals(state.get("name").getAsString())) { + } else if (FIRE_ALARM.propertyName.equals(propertyName)) { if (fireAlarmValue == null) { - fireAlarmValue = "ALARM".equals(state.get("value").getAsString()); + fireAlarmValue = "ALARM".equals(propertyValue); } - } else if (WATER_ALARM.propertyName.equals(state.get("name").getAsString())) { + } else if (WATER_ALARM.propertyName.equals(propertyName)) { if (waterAlarmValue == null) { - waterAlarmValue = "ALARM".equals(state.get("value").getAsString()); + waterAlarmValue = "ALARM".equals(propertyValue); } } } - updateState(ARM_STATE.channelId, armStateValue == null ? UnDefType.UNDEF : new StringType(armStateValue)); - updateState(BURGLARY_ALARM.channelId, burglaryAlarmValue == null ? UnDefType.UNDEF + smartHomeDeviceHandler.updateState(ARM_STATE.channelId, + armStateValue == null ? UnDefType.UNDEF : new StringType(armStateValue)); + smartHomeDeviceHandler.updateState(BURGLARY_ALARM.channelId, burglaryAlarmValue == null ? UnDefType.UNDEF : (burglaryAlarmValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); - updateState(CARBON_MONOXIDE_ALARM.channelId, carbonMonoxideAlarmValue == null ? UnDefType.UNDEF - : (carbonMonoxideAlarmValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); - updateState(FIRE_ALARM.channelId, fireAlarmValue == null ? UnDefType.UNDEF + smartHomeDeviceHandler.updateState(CARBON_MONOXIDE_ALARM.channelId, + carbonMonoxideAlarmValue == null ? UnDefType.UNDEF + : (carbonMonoxideAlarmValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); + smartHomeDeviceHandler.updateState(FIRE_ALARM.channelId, fireAlarmValue == null ? UnDefType.UNDEF : (fireAlarmValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); - updateState(WATER_ALARM.channelId, waterAlarmValue == null ? UnDefType.UNDEF + smartHomeDeviceHandler.updateState(WATER_ALARM.channelId, waterAlarmValue == null ? UnDefType.UNDEF : (waterAlarmValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN)); } @Override - public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) throws IOException, InterruptedException { if (channelId.equals(ARM_STATE.channelId)) { if (containsCapabilityProperty(capabilities, ARM_STATE.propertyName)) { if (command instanceof StringType) { String armStateValue = command.toFullString(); if (!armStateValue.isEmpty()) { - connection.smartHomeCommand(entityId, "controlSecurityPanel", ARM_STATE.propertyName, - armStateValue); + connection.smartHomeCommand(entityId, "controlSecurityPanel", + Map.of(ARM_STATE.propertyName, armStateValue)); return true; } } @@ -167,10 +139,4 @@ public boolean handleCommand(Connection connection, SmartHomeDevice shd, String } return false; } - - @Override - public @Nullable StateDescription findStateDescription(String channelUID, StateDescription originalStateDescription, - @Nullable Locale locale) { - return null; - } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerTemperatureSensor.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerTemperatureSensor.java index 88ff4c3ed38bb..26ea47e6bddfa 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerTemperatureSensor.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerTemperatureSensor.java @@ -16,66 +16,53 @@ import java.io.IOException; import java.util.List; -import java.util.Locale; +import java.util.Set; import javax.measure.quantity.Temperature; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.types.Command; -import org.openhab.core.types.StateDescription; import org.openhab.core.types.UnDefType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.google.gson.JsonObject; /** - * The {@link HandlerTemperatureSensor} is responsible for the Alexa.TemperatureSensorInterface + * The {@link HandlerTemperatureSensor} is responsible for the Alexa.TemperatureSensor interface * * @author Lukas Knoeller - Initial contribution * @author Michael Geramb - Initial contribution */ @NonNullByDefault -public class HandlerTemperatureSensor extends HandlerBase { - // Logger - private final Logger logger = LoggerFactory.getLogger(HandlerTemperatureSensor.class); - // Interface +public class HandlerTemperatureSensor extends AbstractInterfaceHandler { public static final String INTERFACE = "Alexa.TemperatureSensor"; - // Channel definitions - private static final ChannelInfo TEMPERATURE = new ChannelInfo("temperature" /* propertyName */ , - "temperature" /* ChannelId */, CHANNEL_TYPE_TEMPERATURE /* Channel Type */ , - ITEM_TYPE_NUMBER_TEMPERATURE /* Item Type */); - public HandlerTemperatureSensor(SmartHomeDeviceHandler smartHomeDeviceHandler) { - super(smartHomeDeviceHandler); - } + private static final ChannelInfo TEMPERATURE = new ChannelInfo("temperature", "temperature", + CHANNEL_TYPE_TEMPERATURE); - @Override - public String[] getSupportedInterface() { - return new String[] { INTERFACE }; + public HandlerTemperatureSensor(SmartHomeDeviceHandler smartHomeDeviceHandler) { + super(smartHomeDeviceHandler, List.of(INTERFACE)); } @Override - protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { if (TEMPERATURE.propertyName.equals(property)) { - return new ChannelInfo[] { TEMPERATURE }; + return Set.of(TEMPERATURE); } - return null; + return Set.of(); } @Override public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { QuantityType temperatureValue = null; for (JsonObject state : stateList) { - logger.debug("Updating {} with state: {}", interfaceName, state.toString()); if (TEMPERATURE.propertyName.equals(state.get("name").getAsString())) { JsonObject value = state.get("value").getAsJsonObject(); // For groups take the first @@ -90,18 +77,13 @@ public void updateChannels(String interfaceName, List stateList, Upd } } } - updateState(TEMPERATURE.channelId, temperatureValue == null ? UnDefType.UNDEF : temperatureValue); + smartHomeDeviceHandler.updateState(TEMPERATURE.channelId, + temperatureValue == null ? UnDefType.UNDEF : temperatureValue); } @Override - public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) throws IOException { + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) throws IOException { return false; } - - @Override - public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, - @Nullable Locale locale) { - return null; - } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerThermostatController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerThermostatController.java index 476b7fdd26767..5594d5202eb8d 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerThermostatController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerThermostatController.java @@ -15,178 +15,119 @@ import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.*; import java.io.IOException; +import java.util.HashMap; import java.util.List; -import java.util.Locale; - -import javax.measure.quantity.Temperature; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.types.Command; -import org.openhab.core.types.StateDescription; +import org.openhab.core.types.State; +import org.openhab.core.types.Type; import org.openhab.core.types.UnDefType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.google.gson.JsonObject; /** - * The {@link HandlerThermostatController} is responsible for the Alexa.ThermostatControllerInterface + * The {@link HandlerThermostatController} is responsible for the Alexa.ThermostatController interface * * @author Sven Killig - Initial contribution */ @NonNullByDefault -public class HandlerThermostatController extends HandlerBase { - // Logger - private final Logger logger = LoggerFactory.getLogger(HandlerThermostatController.class); - // Interface +public class HandlerThermostatController extends AbstractInterfaceHandler { public static final String INTERFACE = "Alexa.ThermostatController"; - // Channel definitions - private static final ChannelInfo TARGET_SETPOINT = new ChannelInfo("targetSetpoint" /* propertyName */ , - "targetSetpoint" /* ChannelId */, CHANNEL_TYPE_TARGETSETPOINT /* Channel Type */ , - ITEM_TYPE_NUMBER_TEMPERATURE /* Item Type */); - private static final ChannelInfo LOWER_SETPOINT = new ChannelInfo("lowerSetpoint" /* propertyName */ , - "lowerSetpoint" /* ChannelId */, CHANNEL_TYPE_LOWERSETPOINT /* Channel Type */ , - ITEM_TYPE_NUMBER_TEMPERATURE /* Item Type */); - private static final ChannelInfo UPPER_SETPOINT = new ChannelInfo("upperSetpoint" /* propertyName */ , - "upperSetpoint" /* ChannelId */, CHANNEL_TYPE_UPPERSETPOINT /* Channel Type */ , - ITEM_TYPE_NUMBER_TEMPERATURE /* Item Type */); - private static final ChannelInfo THERMOSTAT_MODE = new ChannelInfo("thermostatMode" /* propertyName */ , - "thermostatMode" /* ChannelId */, CHANNEL_TYPE_THERMOSTATMODE /* Channel Type */ , - ITEM_TYPE_STRING /* Item Type */); - public HandlerThermostatController(SmartHomeDeviceHandler smartHomeDeviceHandler) { - super(smartHomeDeviceHandler); - } + private static final ChannelInfo TARGET_SETPOINT = new ChannelInfo("targetSetpoint" /* propertyNameReceive */, + "targetTemperature" /* propertyNameSend */, "targetSetpoint", CHANNEL_TYPE_TARGETSETPOINT); + private static final ChannelInfo LOWER_SETPOINT = new ChannelInfo("lowerSetpoint", + "lowerSetTemperature" /* propertyNameSend */, "lowerSetpoint", CHANNEL_TYPE_LOWERSETPOINT); + private static final ChannelInfo UPPER_SETPOINT = new ChannelInfo("upperSetpoint", + "upperSetTemperature" /* propertyNameSend */, "upperSetpoint", CHANNEL_TYPE_UPPERSETPOINT); + private static final ChannelInfo MODE = new ChannelInfo("thermostatMode", "thermostatMode", "thermostatMode", + CHANNEL_TYPE_THERMOSTATMODE); - @Override - public String[] getSupportedInterface() { - return new String[] { INTERFACE }; + private static final Set ALL_CHANNELS = Set.of(TARGET_SETPOINT, LOWER_SETPOINT, UPPER_SETPOINT, MODE); + + private final Map setpointCache = new HashMap<>(); + + public HandlerThermostatController(SmartHomeDeviceHandler smartHomeDeviceHandler) { + super(smartHomeDeviceHandler, List.of(INTERFACE)); } @Override - protected ChannelInfo @Nullable [] findChannelInfos(SmartHomeCapability capability, String property) { - if (TARGET_SETPOINT.propertyName.equals(property)) { - return new ChannelInfo[] { TARGET_SETPOINT }; - } - if (LOWER_SETPOINT.propertyName.equals(property)) { - return new ChannelInfo[] { LOWER_SETPOINT }; - } - if (UPPER_SETPOINT.propertyName.equals(property)) { - return new ChannelInfo[] { UPPER_SETPOINT }; - } - if (THERMOSTAT_MODE.propertyName.equals(property)) { - return new ChannelInfo[] { THERMOSTAT_MODE }; - } - return null; + protected Set findChannelInfos(JsonSmartHomeCapability capability, @Nullable String property) { + return ALL_CHANNELS.stream().filter(c -> c.propertyName.equals(property)).collect(Collectors.toSet()); } @Override public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { - for (JsonObject state : stateList) { - QuantityType temperatureValue = null; - logger.debug("Updating {} with state: {}", interfaceName, state.toString()); - if (TARGET_SETPOINT.propertyName.equals(state.get("name").getAsString())) { - // For groups take the first - if (temperatureValue == null) { - JsonObject value = state.get("value").getAsJsonObject(); - float temperature = value.get("value").getAsFloat(); - String scale = value.get("scale").getAsString().toUpperCase(); - if ("CELSIUS".equals(scale)) { - temperatureValue = new QuantityType<>(temperature, SIUnits.CELSIUS); - } else { - temperatureValue = new QuantityType<>(temperature, ImperialUnits.FAHRENHEIT); - } - } - updateState(TARGET_SETPOINT.channelId, temperatureValue == null ? UnDefType.UNDEF : temperatureValue); - } - if (THERMOSTAT_MODE.propertyName.equals(state.get("name").getAsString())) { - // For groups take the first - String operation = state.get("value").getAsString().toUpperCase(); - StringType operationValue = new StringType(operation); - updateState(THERMOSTAT_MODE.channelId, operationValue); - } - if (UPPER_SETPOINT.propertyName.equals(state.get("name").getAsString())) { - // For groups take the first - if (temperatureValue == null) { - JsonObject value = state.get("value").getAsJsonObject(); - float temperature = value.get("value").getAsFloat(); - String scale = value.get("scale").getAsString().toUpperCase(); - if ("CELSIUS".equals(scale)) { - temperatureValue = new QuantityType<>(temperature, SIUnits.CELSIUS); - } else { - temperatureValue = new QuantityType<>(temperature, ImperialUnits.FAHRENHEIT); - } - } - updateState(UPPER_SETPOINT.channelId, temperatureValue == null ? UnDefType.UNDEF : temperatureValue); - } - if (LOWER_SETPOINT.propertyName.equals(state.get("name").getAsString())) { - // For groups take the first - if (temperatureValue == null) { - JsonObject value = state.get("value").getAsJsonObject(); - float temperature = value.get("value").getAsFloat(); - String scale = value.get("scale").getAsString().toUpperCase(); - if ("CELSIUS".equals(scale)) { - temperatureValue = new QuantityType<>(temperature, SIUnits.CELSIUS); + ALL_CHANNELS.forEach(channel -> { + State newState = null; + for (JsonObject state : stateList) { + if (channel.propertyName.equals(state.get("name").getAsString())) { + if ("thermostatMode".equals(channel.propertyName)) { + newState = new StringType(state.get("value").getAsString()); } else { - temperatureValue = new QuantityType<>(temperature, ImperialUnits.FAHRENHEIT); + JsonObject value = state.get("value").getAsJsonObject(); + // For groups take the first + if (newState == null) { + float temperature = value.get("value").getAsFloat(); + String scale = value.get("scale").getAsString().toUpperCase(); + if ("CELSIUS".equals(scale)) { + newState = new QuantityType<>(temperature, SIUnits.CELSIUS); + } else { + newState = new QuantityType<>(temperature, ImperialUnits.FAHRENHEIT); + } + } + setpointCache.put(channel.propertyNameSend, newState); } } - updateState(LOWER_SETPOINT.channelId, temperatureValue == null ? UnDefType.UNDEF : temperatureValue); } - } + smartHomeDeviceHandler.updateState(channel.channelId, + Objects.requireNonNullElse(newState, UnDefType.UNDEF)); + }); } @Override - public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId, - List capabilities, String channelId, Command command) + public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) throws IOException, InterruptedException { - if (channelId.equals(TARGET_SETPOINT.channelId)) { - if (containsCapabilityProperty(capabilities, TARGET_SETPOINT.propertyName)) { - if (command instanceof QuantityType) { - connection.smartHomeCommand(entityId, "setTargetTemperature", "targetTemperature", command); - return true; - } - } - } - if (channelId.equals(LOWER_SETPOINT.channelId)) { - if (containsCapabilityProperty(capabilities, LOWER_SETPOINT.propertyName)) { - if (command instanceof QuantityType) { - connection.smartHomeCommand(entityId, "setTargetTemperature", "lowerSetTemperature", command); - return true; - } - } - } - if (channelId.equals(UPPER_SETPOINT.channelId)) { - if (containsCapabilityProperty(capabilities, UPPER_SETPOINT.propertyName)) { + ChannelInfo channelInfo = ALL_CHANNELS.stream().filter(c -> c.channelId.equals(channelId)).findFirst() + .orElse(null); + if (channelInfo != null) { + if (containsCapabilityProperty(capabilities, channelInfo.propertyName)) { if (command instanceof QuantityType) { - connection.smartHomeCommand(entityId, "setTargetTemperature", "upperSetTemperature", command); + Map values = new HashMap<>(); + if ("lowerSetTemperature".equals(channelInfo.propertyNameSend)) { + values.put("lowerSetTemperature", command); + values.put("upperSetTemperature", setpointCache.getOrDefault("upperSetTemperature", command)); + } else if ("upperSetTemperature".equals(channelInfo.propertyNameSend)) { + values.put("upperSetTemperature", command); + values.put("lowerSetTemperature", setpointCache.getOrDefault("lowerSetTemperature", command)); + } else { + values.put("targetTemperature", command); + } + connection.smartHomeCommand(entityId, "setTargetTemperature", values); return true; } - } - } - if (channelId.equals(THERMOSTAT_MODE.channelId)) { - if (containsCapabilityProperty(capabilities, THERMOSTAT_MODE.propertyName)) { if (command instanceof StringType) { - connection.smartHomeCommand(entityId, "setThermostatMode", "thermostatMode", command); - return true; + connection.smartHomeCommand(entityId, "setThermostatMode", + Map.of(channelInfo.propertyNameSend, command)); } } } - return false; - } - @Override - public @Nullable StateDescription findStateDescription(String channelId, StateDescription originalStateDescription, - @Nullable Locale locale) { - return null; + return false; } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/InterfaceHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/InterfaceHandler.java new file mode 100644 index 0000000000000..b7e78eac9feb0 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/InterfaceHandler.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.amazonechocontrol.internal.smarthome; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.amazonechocontrol.internal.connection.Connection; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.core.thing.Channel; +import org.openhab.core.types.Command; +import org.openhab.core.types.CommandOption; +import org.openhab.core.types.StateDescription; + +import com.google.gson.JsonObject; + +/** + * The {@link InterfaceHandler} is an interface for Alexa interface handlers + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface InterfaceHandler { + Collection initialize(List capabilities); + + List getSupportedInterface(); + + boolean hasChannel(String channelId); + + void updateChannels(String interfaceName, List stateList, UpdateChannelResult result); + + boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String entityId, + List capabilities, String channelId, Command command) + throws IOException, InterruptedException; + + @Nullable + List getCommandDescription(Channel channel); + + @Nullable + StateDescription getStateDescription(Channel channel); + + class UpdateChannelResult { + public boolean needSingleUpdate; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/JsonNetworkDetails.java similarity index 91% rename from bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java rename to bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/JsonNetworkDetails.java index 75bf6df74b006..5e4cc963dabf7 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/JsonNetworkDetails.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.amazonechocontrol.internal.jsons; +package org.openhab.binding.amazonechocontrol.internal.smarthome; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java index 5674a7c1ad0e9..b4d15d7616258 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java @@ -20,8 +20,8 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.DriverIdentity; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice.DriverIdentity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,11 +34,11 @@ public class SmartHomeDeviceStateGroupUpdateCalculator { private final Logger logger = LoggerFactory.getLogger(SmartHomeDeviceStateGroupUpdateCalculator.class); - private static final Integer UPDATE_INTERVAL_PRIVATE_SKILLS_IN_SECONDS = 600; - private static final Integer UPDATE_INTERVAL_PRIVATE_SKILLS_IN_SECONDS_TRACE = 10; - private static final Integer UPDATE_INTERVAL_ACOUSTIC_EVENTS_IN_SECONDS = 10; - private Integer updateIntervalAmazonInSeconds; - private Integer updateIntervalSkillsInSeconds; + private static final int UPDATE_INTERVAL_PRIVATE_SKILLS_IN_SECONDS = 300; + private static final int UPDATE_INTERVAL_PRIVATE_SKILLS_IN_SECONDS_TRACE = 10; + private static final int UPDATE_INTERVAL_ACOUSTIC_EVENTS_IN_SECONDS = 10; + private final int updateIntervalAmazonInSeconds; + private final int updateIntervalSkillsInSeconds; private static class UpdateGroup { private final int intervalInSeconds; @@ -58,7 +58,7 @@ public SmartHomeDeviceStateGroupUpdateCalculator(int updateIntervalAmazonInSecon this.updateIntervalSkillsInSeconds = updateIntervalSkillsInSeconds; } - private Integer getUpdateIntervalInSeconds(SmartHomeDevice shd) { + private Integer getUpdateIntervalInSeconds(JsonSmartHomeDevice shd) { Integer updateIntervalInSeconds = shd.updateIntervalInSeconds; if (updateIntervalInSeconds != null) { return updateIntervalInSeconds; @@ -93,11 +93,11 @@ private Integer getUpdateIntervalInSeconds(SmartHomeDevice shd) { return updateIntervalInSeconds; } - public void removeDevicesWithNoUpdate(List devices) { + public void removeDevicesWithNoUpdate(List devices) { Date updateTimeStamp = new Date(); // check if new group is needed boolean syncAllGroups = false; - for (SmartHomeDevice device : devices) { + for (JsonSmartHomeDevice device : devices) { int updateIntervalInSeconds = getUpdateIntervalInSeconds(device); if (!updateGroups.containsKey(updateIntervalInSeconds)) { UpdateGroup newGroup = new UpdateGroup(updateIntervalInSeconds); diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/types/Announcement.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/types/Announcement.java new file mode 100644 index 0000000000000..33883de6d02f4 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/types/Announcement.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.types; + +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link Announcement} encapsulates the information for an announcement command via the + * {@link org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants#CHANNEL_ANNOUNCEMENT} + * channel. + * + * @author Jan N. Klug - Initial contribution + */ +public class Announcement { + public @Nullable Boolean sound; + public @Nullable String title; + public @Nullable String body; + public @Nullable String speak; + public @Nullable Integer volume; +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/types/Notification.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/types/Notification.java new file mode 100644 index 0000000000000..72e8428fc4b11 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/types/Notification.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.types; + +import java.time.ZonedDateTime; + +/** + * The {@link Notification} encapsulates a notification + * + * @author Jan N. Klug - Initial contribution + */ +public record Notification(String deviceSerial, String type, ZonedDateTime nextAlarmTime) { +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/HttpRequestBuilder.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/HttpRequestBuilder.java new file mode 100644 index 0000000000000..c90896cd0c15d --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/HttpRequestBuilder.java @@ -0,0 +1,443 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.util; + +import static org.eclipse.jetty.http.HttpHeader.*; +import static org.eclipse.jetty.http.HttpMethod.*; +import static org.eclipse.jetty.http.HttpStatus.*; +import static org.eclipse.jetty.http.HttpStatus.BAD_REQUEST_400; +import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON_UTF_8; +import static org.eclipse.jetty.http.MimeTypes.Type.FORM_ENCODED; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.API_VERSION; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.DI_OS_VERSION; +import static org.openhab.binding.amazonechocontrol.internal.util.HttpRequestBuilder.FailMode.*; + +import java.net.CookieManager; +import java.net.HttpCookie; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import javax.ws.rs.core.MediaType; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.BufferingResponseListener; +import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.amazonechocontrol.internal.ConnectionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; + +/** + * The {@link HttpRequestBuilder} creates customized requests for Alexa API requests + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class HttpRequestBuilder { + private static final String DEFAULT_USER_AGENT = "AmazonWebView/Amazon Alexa/" + API_VERSION + "/iOS/" + + DI_OS_VERSION + "/iPhone"; + + private final Logger logger = LoggerFactory.getLogger(HttpRequestBuilder.class); + + private final CookieManager cookieManager; + private final HttpClient httpClient; + private final Gson gson; + private final Lock lock = new ReentrantLock(); + private final Semaphore semaphore = new Semaphore(2, true); + + public HttpRequestBuilder(HttpClient httpClient, CookieManager cookieManager, Gson gson) { + this.httpClient = httpClient; + this.cookieManager = cookieManager; + this.gson = gson; + } + + public Builder get(String uriString) { + return new Builder(GET, uriString); + } + + public Builder post(String uriString) { + return new Builder(POST, uriString); + } + + public Builder put(String uriString) { + return new Builder(PUT, uriString); + } + + public Builder delete(String uriString) { + return new Builder(DELETE, uriString); + } + + public Builder builder(HttpMethod httpMethod, String uriString) { + return new Builder(httpMethod, uriString); + } + + private void createRequest(URI uri, RequestParams params, HttpResponseListener responseListener) { + Request request = httpClient.newRequest(uri).method(params.method()); + request.header(ACCEPT_LANGUAGE, "en-US"); + request.header("DNT", "1"); + request.header("Upgrade-Insecure-Requests", "1"); + if (!params.customHeaders().containsKey(USER_AGENT.toString())) { + request.agent(DEFAULT_USER_AGENT); + } + params.customHeaders().entrySet().stream().filter(h -> !h.getValue().isBlank()) + .forEach(h -> request.header(h.getKey(), h.getValue())); + + // handle re-directs in response listener manually + request.followRedirects(false); + + // add cookies + + if (!params.customHeaders().containsKey(COOKIE.toString())) { + for (HttpCookie cookie : cookieManager.getCookieStore().get(uri)) { + request.cookie(cookie); + if (cookie.getName().equals("csrf")) { + request.header("csrf", cookie.getValue()); + } + } + } + + if (params.requestContent() != null) { + byte[] contentBytes = params.requestContent().getBytes(StandardCharsets.UTF_8); + request.header(CONTENT_TYPE, params.json() ? APPLICATION_JSON_UTF_8.asString() : FORM_ENCODED.asString()); + request.header(CONTENT_LENGTH, Integer.toString(contentBytes.length)); + if (POST.equals(params.method())) { + request.header(EXPECT, "100-continue"); + } + request.content(new BytesContentProvider(contentBytes)); + } + + if (logger.isTraceEnabled()) { + logger.trace("> {} to {}, headers = {}, cookies = {}, content = {}", params.method(), uri, + HttpUtil.logToString(request.getHeaders()), request.getCookies(), params.requestContent()); + } + + request.send(responseListener); + } + + /** + * The {@link Builder} is used to build HTTP requests to remote servers, including managed cookies + */ + public class Builder { + private final HttpMethod httpMethod; + private final URI uri; + private final Map headers = new HashMap<>(); + private boolean retry = true; + private boolean redirect = true; + private boolean isJson = false; + private @Nullable String body; + + private Builder(HttpMethod httpMethod, String uriString) { + this.httpMethod = httpMethod; + this.uri = URI.create(uriString); + } + + /** + * Adds a single header to this request + * + * @param field the field name + * @param value the value + * @return the request builder + */ + public Builder withHeader(String field, String value) { + this.headers.put(field, value); + return this; + } + + /** + * Add multiple headers to this request + * + * @param headers a {@link Map} containing the headers + * @return the request builder + */ + public Builder withHeaders(Map headers) { + this.headers.putAll(headers); + return this; + } + + /** + * Set the retry flag + * + * @param retry {@code true} allows up to 3 retries (default), {@code false} fails immediately + * @return the request builder + */ + public Builder retry(boolean retry) { + this.retry = retry; + return this; + } + + /** + * Set the redirect flag + * + * @param redirect {@code true} allows up to 30 redirects (default), {@code false} fails on + * redirection + * @return the request builder + */ + public Builder redirect(boolean redirect) { + this.redirect = redirect; + return this; + } + + public Builder withContent(@Nullable Object content) { + if (content == null || content instanceof String) { + this.body = (String) content; + this.isJson = false; + } else if (content instanceof JsonObject) { + this.body = content.toString(); + this.isJson = true; + } else { + this.body = gson.toJson(content); + this.isJson = true; + } + return this; + } + + /** + * Override the autodetected type + *

+ * This needs to be called AFTER the content has been set + * + * @param isJson if the request content should be considered as JSON + * @return the request builder + */ + public Builder withJson(boolean isJson) { + this.isJson = isJson; + return this; + } + + public CompletableFuture send() { + RequestParams params = new RequestParams(httpMethod, body, isJson, headers); + CompletableFuture httpResponse = new CompletableFuture<>(); + HttpResponseListener responseListener = new HttpResponseListener(httpResponse, params, redirect, + retry ? RETRY : EXCEPTION); + createRequest(uri, params, responseListener); + return httpResponse; + } + + public CompletableFuture send(Class returnType) { + return send(TypeToken.get(returnType)); + } + + @SuppressWarnings("unchecked") + public CompletableFuture send(TypeToken returnType) { + return send().thenApply(response -> { + if (returnType.getRawType().equals(String.class)) { + return (T) response.content; + } + if (returnType.getRawType().equals(HttpRequestBuilder.HttpResponse.class)) { + return (T) response; + } + String contentType = response.headers.get(CONTENT_TYPE); + if (!contentType.startsWith(MediaType.APPLICATION_JSON)) { + logger.debug("JSON conversion to {} was requested but the response has a Content-Type {}", + returnType.getType().getTypeName(), contentType); + } + try { + T returnValue = gson.fromJson(response.content(), returnType); + // gson.fromJson is non-null if json is non-null and not empty + if (returnValue == null) { + throw new JsonParseException("Empty result"); + } + return returnValue; + } catch (JsonParseException e) { + logger.warn("Parsing json failed: {}", isJson, e); + throw e; + } + }); + } + + public HttpResponse syncSend() throws ConnectionException { + return syncSend(HttpResponse.class); + } + + public T syncSend(Class returnType) throws ConnectionException { + return syncSend(TypeToken.get(returnType)); + } + + public T syncSend(TypeToken returnType) throws ConnectionException { + try { + logger.debug("> {}: {} (available: {})", httpMethod, uri, semaphore.availablePermits()); + semaphore.acquire(); + return send(returnType).get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof ConnectionException connectionException) { + throw connectionException; + } else { + throw new ConnectionException("Request failed", e); + } + } catch (RuntimeException | InterruptedException e) { + logger.debug("Request to uri '{}' failed:", uri, e); + throw new ConnectionException("Request failed", e); + } finally { + semaphore.release(); + } + } + } + + private class HttpResponseListener extends BufferingResponseListener { + private static final int MAX_REDIRECTS = 30; + private static final int MAX_RETRIES = 3; + + private final Logger logger = LoggerFactory.getLogger(HttpResponseListener.class); + private final CompletableFuture httpResponse; + private final RequestParams params; + private final boolean autoRedirect; + private final FailMode failMode; + private int redirectCounter = MAX_REDIRECTS; + private int retryCounter = MAX_RETRIES; + + public HttpResponseListener(CompletableFuture httpResponse, RequestParams requestParams, + boolean autoRedirect, FailMode failMode) { + this.httpResponse = httpResponse; + this.params = requestParams; + this.autoRedirect = autoRedirect; + this.failMode = failMode; + } + + private HttpResponseListener(HttpResponseListener other, int retryCounter, int redirectCounter) { + this.httpResponse = other.httpResponse; + this.params = other.params; + this.autoRedirect = other.autoRedirect; + this.failMode = other.failMode; + this.retryCounter = retryCounter; + this.redirectCounter = redirectCounter; + } + + @Override + public void onComplete(Result result) { + Response response = result.getResponse(); + URI requestUri = response.getRequest().getURI(); + int responseStatus = response.getStatus(); + HttpFields headers = Objects.requireNonNull(response.getHeaders()); + String content = Objects.requireNonNullElse(getContentAsString(), ""); + + if (logger.isTraceEnabled()) { + logger.trace("< {} to {}: {}, headers = {}, content = {}", params.method(), requestUri, responseStatus, + HttpUtil.logToString(response.getHeaders()), content); + } else { + logger.debug("< {} to {}: {}", params.method, requestUri, responseStatus); + } + + headers.getFields(SET_COOKIE).forEach(cookieHeader -> HttpCookie.parse(cookieHeader.getValue()) + .forEach(cookie -> cookieManager.getCookieStore().add(requestUri, cookie))); + + String location = headers.get(LOCATION); + if (location != null && !location.isBlank()) { + location = requestUri.resolve(location).toString(); + if (location.toLowerCase().startsWith("http://")) { + // always use https + location = "https://" + location.substring(7); + logger.debug("Redirect corrected to {}", location); + } + } + + if (HttpStatus.isSuccess(responseStatus)) { + httpResponse.complete(new HttpResponse(responseStatus, headers, content)); + } else if (isRedirection(responseStatus) && location != null) { + logger.debug("Redirected to {}", location); + if (!autoRedirect) { + httpResponse.complete(new HttpResponse(responseStatus, headers, content)); + } + if (redirectCounter == 0) { + httpResponse.completeExceptionally(new ConnectionException("Too many redirects")); + } + createRequest(URI.create(location), params, + new HttpResponseListener(this, retryCounter, redirectCounter - 1)); + } else if (responseStatus == BAD_REQUEST_400 + && "QUEUE_EXPIRED".equals(response.getHeaders().get("x-amzn-error"))) { + // handle queue expired + httpResponse.completeExceptionally(new ConnectionException("Queue expired")); + } else { + if (failMode == EXCEPTION || retryCounter == 0) { + if (responseStatus == 0) { + httpResponse.completeExceptionally(new ConnectionException("Request aborted.")); + } + httpResponse.completeExceptionally(new ConnectionException( + requestUri + " failed with code " + responseStatus + ": " + response.getReason())); + } else if (failMode == NORMAL) { + httpResponse.complete(new HttpResponse(responseStatus, headers, content)); + } else { + logger.debug("Retrying call to {}", requestUri); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + httpResponse.completeExceptionally(new ConnectionException("Interrupted", e)); + } + createRequest(requestUri, params, + new HttpResponseListener(this, retryCounter - 1, redirectCounter)); + } + } + } + } + + private record RequestParams(HttpMethod method, @Nullable String requestContent, boolean json, + Map customHeaders) { + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof RequestParams that)) { + return false; + } + return json == that.json && method == that.method && Objects.equals(requestContent, that.requestContent) + && Objects.equals(customHeaders, that.customHeaders); + } + + public int hashCode() { + return Objects.hash(method, requestContent, json, customHeaders); + } + } + + public record HttpResponse(int statusCode, HttpFields headers, String content) { + @Override + public boolean equals(@Nullable Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + HttpResponse response = (HttpResponse) o; + return statusCode == response.statusCode && Objects.equals(headers, response.headers) + && Objects.equals(content, response.content); + } + + @Override + public int hashCode() { + return Objects.hash(statusCode, headers, content); + } + } + + public enum FailMode { + NORMAL, + EXCEPTION, + RETRY + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/HttpUtil.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/HttpUtil.java new file mode 100644 index 0000000000000..707f883053659 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/HttpUtil.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.util; + +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http.HttpFields; + +/** + * The {@link HttpUtil} implements utility methods for HTTP requests + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class HttpUtil { + + private HttpUtil() { + // prevent instantiation + } + + public static String logToString(HttpFields httpFields) { + return "[" + httpFields.stream().map(field -> { + String headerName = field.getName(); + String value = field.getValue(); + return headerName + "=" + value; + }).collect(Collectors.joining(",")) + "]"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/NonNullListTypeAdapterFactory.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/NonNullListTypeAdapterFactory.java new file mode 100644 index 0000000000000..dca1cd0f4cf3e --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/NonNullListTypeAdapterFactory.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.util; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * The {@link NonNullListTypeAdapterFactory} is a {@link TypeAdapterFactory} for allowing annotation based + * null-serialization + *

+ * Fields that shall be serialized even if they are null need a {@link SerializeNull} annotation + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class NonNullListTypeAdapterFactory implements TypeAdapterFactory { + @Override + @SuppressWarnings("unchecked") + public @Nullable TypeAdapter create(@NonNullByDefault({}) Gson gson, + @NonNullByDefault({}) TypeToken type) { + + Class rawType = (Class) type.getRawType(); + if (rawType != List.class) { + return null; + } + TypeAdapter delegateAdapter = gson.getDelegateAdapter(NonNullListTypeAdapterFactory.this, type); + + return new DeserializeNonNullTypeAdapter<>(delegateAdapter); + } + + private static class DeserializeNonNullTypeAdapter extends TypeAdapter { + private final TypeAdapter delegateTypeAdapter; + + public DeserializeNonNullTypeAdapter(TypeAdapter delegateTypeAdapter) { + this.delegateTypeAdapter = delegateTypeAdapter; + } + + @Override + public void write(JsonWriter writer, @Nullable T value) throws IOException { + delegateTypeAdapter.write(writer, value); + } + + @Override + @SuppressWarnings("unchecked") + public @Nullable T read(JsonReader reader) throws IOException { + final JsonToken peek = reader.peek(); + if (peek == JsonToken.NULL) { + reader.nextNull(); + return (T) List.of(); + } + return delegateTypeAdapter.read(reader); + } + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/ResourceUtil.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/ResourceUtil.java new file mode 100644 index 0000000000000..bc2e138c217d2 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/ResourceUtil.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ResourceUtil} is a set of utils for handling bundle resources + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class ResourceUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(ResourceUtil.class); + + private ResourceUtil() { + // prevent instantiation + } + + /** + * Get an {@link InputStream} that reads from a file in a bundle + * + * @param clazz a class contained in the bundle + * @param fileName the fileName in the resources folder (including path) + * @return the {@link InputStream} as {@link Optional} (empty if not found) + */ + public static Optional getResourceStream(Class clazz, String fileName) { + // we need the classloader of the bundle that our handler belongs to + ClassLoader classLoader = clazz.getClassLoader(); + if (classLoader == null) { + LOGGER.warn("Could not get classloader for class '{}'", clazz); + return Optional.empty(); + } + + return Optional.ofNullable(classLoader.getResourceAsStream(fileName)); + } + + /** + * Read a .properties file from a bundle + * + * @param clazz a class contained in the bundle + * @param fileName the fileName in the resources folder (including path) + * @return a {@link Map} of strings containing the file contents (empty if file not found) + */ + public static Map readProperties(Class clazz, String fileName) { + return Objects.requireNonNull(getResourceStream(clazz, fileName).map(inputStream -> { + Properties properties = new Properties(); + try { + properties.load(inputStream); + return properties.entrySet().stream() + .collect(Collectors.toMap(e -> (String) e.getKey(), e -> (String) e.getValue())); + } catch (IOException e) { + LOGGER.warn("Could not read resource file '{}', binding will probably fail: {}", fileName, + e.getMessage()); + return new HashMap(); + } + }).orElse(Map.of())); + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/SerializeNull.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/SerializeNull.java new file mode 100644 index 0000000000000..0e1e005c69fdd --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/SerializeNull.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The {@link SerializeNull} annotation is used to indicate that a field should be serialized even if it is null + * + * @author Jan N. Klug - Initial contribution + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SerializeNull { +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/SerializeNullTypeAdapterFactory.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/SerializeNullTypeAdapterFactory.java new file mode 100644 index 0000000000000..7c485746b3667 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/SerializeNullTypeAdapterFactory.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.amazonechocontrol.internal.util; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * The {@link SerializeNullTypeAdapterFactory} is a {@link TypeAdapterFactory} for allowing annotation based + * null-serialization + *

+ * Fields that shall be serialized even if they are null need a {@link SerializeNull} annotation + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class SerializeNullTypeAdapterFactory implements TypeAdapterFactory { + @Override + public @Nullable TypeAdapter create(@NonNullByDefault({}) Gson gson, + @NonNullByDefault({}) TypeToken type) { + List fields = Arrays.asList(type.getRawType().getFields()); + if (fields.stream().noneMatch(field -> field.isAnnotationPresent(SerializeNull.class))) { + // this type has no fields annotated with @SerializeNull, so we don't return a type adapter for this one + return null; + } + + List nonNullFields = fields.stream().filter(field -> !field.isAnnotationPresent(SerializeNull.class)) + .map(this::getRealName).toList(); + + TypeAdapter delegateAdapter = gson.getDelegateAdapter(SerializeNullTypeAdapterFactory.this, type); + TypeAdapter elementAdapter = gson.getAdapter(JsonElement.class); + + return new SerializeNullTypeAdapter<>(delegateAdapter, elementAdapter, nonNullFields); + } + + private String getRealName(Field field) { + SerializedName serializedName = field.getAnnotation(SerializedName.class); + return serializedName != null ? serializedName.value() : field.getName(); + } + + private static class SerializeNullTypeAdapter extends TypeAdapter { + private final TypeAdapter delegateTypeAdapter; + private final TypeAdapter elementTypeAdapter; + private final List nonNullFields; + + public SerializeNullTypeAdapter(TypeAdapter delegateTypeAdapter, TypeAdapter elementTypeAdapter, + List nonNullFields) { + this.delegateTypeAdapter = delegateTypeAdapter; + this.elementTypeAdapter = elementTypeAdapter; + this.nonNullFields = nonNullFields; + } + + @Override + public void write(JsonWriter writer, @Nullable T value) throws IOException { + JsonObject jsonObject = delegateTypeAdapter.toJsonTree(value).getAsJsonObject(); + + // remove all null-fields that are not annotated with @SerializeNull + nonNullFields.forEach(fieldName -> removeNullFields(jsonObject, fieldName)); + + writer.setSerializeNulls(true); + elementTypeAdapter.write(writer, jsonObject); + writer.setSerializeNulls(false); + } + + @Override + public T read(JsonReader reader) throws IOException { + return delegateTypeAdapter.read(reader); + } + + private void removeNullFields(JsonObject jsonObject, String fieldName) { + if (jsonObject.has(fieldName) && jsonObject.get(fieldName).isJsonNull()) { + jsonObject.remove(fieldName); + } + } + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/Util.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/Util.java new file mode 100644 index 0000000000000..5f0d5ffd6c435 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/util/Util.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.util; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link Util} contains helper methods + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class Util { + + private Util() { + // prevent instantiation + } + + public static Optional findIn(Collection collection, Function keyExtractor, + @Nullable U searchKey) { + return collection.stream().filter(e -> Objects.equals(searchKey, keyExtractor.apply(e))).findAny(); + } + + public static List filterList(List list, Function keyExtractor, U searchKey) { + return list.stream().filter(e -> Objects.equals(searchKey, keyExtractor.apply(e))).toList(); + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/i18n/amazonechocontrol.properties b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/i18n/amazonechocontrol.properties index 99837b9ea0794..e78da1f04990d 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/i18n/amazonechocontrol.properties +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/i18n/amazonechocontrol.properties @@ -19,11 +19,13 @@ thing-type.amazonechocontrol.smartHomeDevice.label = Smart Home Device thing-type.amazonechocontrol.smartHomeDevice.description = Smart home device connected to Alexa thing-type.amazonechocontrol.smartHomeDeviceGroup.label = Smart Home Device Group thing-type.amazonechocontrol.smartHomeDeviceGroup.description = Group of smart home devices in your amazon account -thing-type.amazonechocontrol.wha.label = Amazon Echo Whole House Audio Control -thing-type.amazonechocontrol.wha.description = Amazon Multiroom Music +thing-type.amazonechocontrol.wha.label = Whole House Audio Control +thing-type.amazonechocontrol.wha.description = Amazon Multiroom (Whole House Audio) Music # thing types config +thing-type.config.amazonechocontrol.account.activityRequestDelay.label = Activity Request Delay +thing-type.config.amazonechocontrol.account.activityRequestDelay.description = The number of seconds between a voice command was detected and the received command is requested from the server. thing-type.config.amazonechocontrol.account.discoverSmartHome.label = Device Discovery Mode thing-type.config.amazonechocontrol.account.discoverSmartHome.description = Defines which devices shall be discovered. thing-type.config.amazonechocontrol.account.discoverSmartHome.option.0 = No Discovery @@ -49,20 +51,12 @@ thing-type.config.amazonechocontrol.wha.serialNumber.description = The serial nu # channel types +channel-type.amazonechocontrol.acousticEventDetectionState.label = Acoustic Event channel-type.amazonechocontrol.active.label = Active channel-type.amazonechocontrol.active.description = Activate this flash briefing configuration -channel-type.amazonechocontrol.amazonMusic.label = Amazon Music -channel-type.amazonechocontrol.amazonMusic.description = Amazon Music turned on -channel-type.amazonechocontrol.amazonMusicPlayListId.label = Amazon Music Play List Id -channel-type.amazonechocontrol.amazonMusicPlayListId.description = Amazon Music play list id (Write only, no current state) -channel-type.amazonechocontrol.amazonMusicPlayListIdLastUsed.label = Amazon Music Last Selected Playlist Id -channel-type.amazonechocontrol.amazonMusicPlayListIdLastUsed.description = Id of the playlist which was started with openHAB -channel-type.amazonechocontrol.amazonMusicTrackId.label = Amazon Music Track Id -channel-type.amazonechocontrol.amazonMusicTrackId.description = Id of the amazon music track channel-type.amazonechocontrol.announcement.label = Announcement -channel-type.amazonechocontrol.announcement.description = Display the announcement message on the display (Write only). See in the tutorial section of the binding description to learn how it's possible to set the title and turn off the sound. +channel-type.amazonechocontrol.announcement.description = Display the announcement message on the display (write-only). See in the tutorial section of the binding description to learn how it's possible to set the title and turn off the sound. channel-type.amazonechocontrol.armState.label = ARM State -channel-type.amazonechocontrol.armState.description = ARM State channel-type.amazonechocontrol.ascendingAlarm.label = Ascending Alarm channel-type.amazonechocontrol.ascendingAlarm.description = Ascending alarm up to the configured volume channel-type.amazonechocontrol.bluetooth.label = Bluetooth Connection @@ -72,116 +66,101 @@ channel-type.amazonechocontrol.bluetoothDeviceName.description = Connected bluet channel-type.amazonechocontrol.bluetoothMAC.label = Bluetooth Connection channel-type.amazonechocontrol.bluetoothMAC.description = MAC-Address of the bluetooth connected device channel-type.amazonechocontrol.brightness.label = Brightness -channel-type.amazonechocontrol.brightness.description = Brightness channel-type.amazonechocontrol.burglaryAlarm.label = Burglary Alarm -channel-type.amazonechocontrol.burglaryAlarm.description = Burglary Alarm +channel-type.amazonechocontrol.carbonMonoxide.label = Carbon Monoxide channel-type.amazonechocontrol.carbonMonoxideAlarm.label = Carbon Monoxide Alarm -channel-type.amazonechocontrol.carbonMonoxideAlarm.description = Carbon Monoxide Alarm channel-type.amazonechocontrol.color.label = Color -channel-type.amazonechocontrol.color.description = Color channel-type.amazonechocontrol.colorName.label = Color Name -channel-type.amazonechocontrol.colorName.description = Color Name channel-type.amazonechocontrol.colorTemperatureName.label = Color Temperature Name -channel-type.amazonechocontrol.colorTemperatureName.description = Color Temperature Name +channel-type.amazonechocontrol.connectivity.label = Connectivity +channel-type.amazonechocontrol.contact.label = Contact +channel-type.amazonechocontrol.doNotDisturb.label = Do Not Disturb +channel-type.amazonechocontrol.doNotDisturb.description = Do Not Disturb mode enabled channel-type.amazonechocontrol.equalizerBass.label = Bass channel-type.amazonechocontrol.equalizerBass.description = Equalizer Bass channel-type.amazonechocontrol.equalizerMidrange.label = Midrange channel-type.amazonechocontrol.equalizerMidrange.description = Equalizer Midrange channel-type.amazonechocontrol.equalizerTreble.label = Treble channel-type.amazonechocontrol.equalizerTreble.description = Equalizer Treble +channel-type.amazonechocontrol.fanSpeed.label = Fan speed channel-type.amazonechocontrol.fireAlarm.label = Fire Alarm -channel-type.amazonechocontrol.fireAlarm.description = Fire Alarm -channel-type.amazonechocontrol.glassBreakDetectionState.label = Glass Break Detection State -channel-type.amazonechocontrol.glassBreakDetectionState.description = Glass Break Detection State +channel-type.amazonechocontrol.geoLocation.label = Location channel-type.amazonechocontrol.imageUrl.label = Image Url channel-type.amazonechocontrol.imageUrl.description = Url of the album image or radio station logo +channel-type.amazonechocontrol.indoorAirQuality.label = Indoor Air Quality +channel-type.amazonechocontrol.lastSpokenText.label = Last Spoken Text +channel-type.amazonechocontrol.lastSpokenText.description = Last spoken text of the device. channel-type.amazonechocontrol.lastVoiceCommand.label = Last Voice Command channel-type.amazonechocontrol.lastVoiceCommand.description = Last voice command spoken to the device. Writing to the channel starts voice output. +channel-type.amazonechocontrol.lockState.label = Lock State channel-type.amazonechocontrol.loop.label = Loop channel-type.amazonechocontrol.loop.description = Loop channel-type.amazonechocontrol.lowerSetpoint.label = Lower Setpoint -channel-type.amazonechocontrol.lowerSetpoint.description = Lower Setpoint channel-type.amazonechocontrol.mediaLength.label = Media Length -channel-type.amazonechocontrol.mediaLength.description = Media length channel-type.amazonechocontrol.mediaProgress.label = Media Progress channel-type.amazonechocontrol.mediaProgress.description = Media progress in percent channel-type.amazonechocontrol.mediaProgressTime.label = Media Play Time -channel-type.amazonechocontrol.mediaProgressTime.description = Media play time +channel-type.amazonechocontrol.motionDetected.label = Motion Detected channel-type.amazonechocontrol.musicProviderId.label = Music Provider -channel-type.amazonechocontrol.musicProviderId.description = Music provider channel-type.amazonechocontrol.nextAlarm.label = Next Alarm -channel-type.amazonechocontrol.nextAlarm.description = Next alarm channel-type.amazonechocontrol.nextMusicAlarm.label = Next Music Alarm -channel-type.amazonechocontrol.nextMusicAlarm.description = Next music alarm channel-type.amazonechocontrol.nextReminder.label = Next Reminder -channel-type.amazonechocontrol.nextReminder.description = Next Reminder channel-type.amazonechocontrol.nextTimer.label = Next Timer -channel-type.amazonechocontrol.nextTimer.description = Next timer channel-type.amazonechocontrol.notificationVolume.label = Notification Volume -channel-type.amazonechocontrol.notificationVolume.description = Notification Volume channel-type.amazonechocontrol.percentage.label = Percentage -channel-type.amazonechocontrol.percentage.description = Percentage channel-type.amazonechocontrol.playAlarmSound.label = Alarm Sound channel-type.amazonechocontrol.playAlarmSound.description = Plays an alarm sound channel-type.amazonechocontrol.playMusicVoiceCommand.label = Music Voice Command -channel-type.amazonechocontrol.playMusicVoiceCommand.description = Voice command as text. E.g. 'Yesterday from the Beatles' (Write only) +channel-type.amazonechocontrol.playMusicVoiceCommand.description = Voice command as text. E.g. 'Yesterday from the Beatles' (write-only) channel-type.amazonechocontrol.playOnDevice.label = Play On Device channel-type.amazonechocontrol.playOnDevice.description = Plays the briefing on the device (serial number or name, write only) channel-type.amazonechocontrol.player.label = Player channel-type.amazonechocontrol.player.description = Music Player +channel-type.amazonechocontrol.pm25.label = PM2.5 +channel-type.amazonechocontrol.pm25.description = Particulate Matter PM2.5 density. channel-type.amazonechocontrol.powerLevel.label = Power Level -channel-type.amazonechocontrol.powerLevel.description = Power Level channel-type.amazonechocontrol.powerState.label = Power State -channel-type.amazonechocontrol.powerState.description = Power State channel-type.amazonechocontrol.providerDisplayName.label = Provider Name channel-type.amazonechocontrol.providerDisplayName.description = Name of music provider -channel-type.amazonechocontrol.radio.label = TuneIn Radio -channel-type.amazonechocontrol.radio.description = Radio turned on -channel-type.amazonechocontrol.radioStationId.label = TuneIn Radio Station Id -channel-type.amazonechocontrol.radioStationId.description = Id of the radio station +channel-type.amazonechocontrol.refreshActivity.label = Refresh Activity +channel-type.amazonechocontrol.refreshActivity.description = A command send to this channel refreshes the customer history activity (write-only) channel-type.amazonechocontrol.relativeHumidity.label = Humidity -channel-type.amazonechocontrol.relativeHumidity.description = Relative humidity measured by the thermostat. channel-type.amazonechocontrol.remind.label = Remind channel-type.amazonechocontrol.remind.description = Speak the reminder and send a notification to the Alexa app channel-type.amazonechocontrol.save.label = Save -channel-type.amazonechocontrol.save.description = Save the current flash briefing configuration (Write only) +channel-type.amazonechocontrol.save.description = Save the current flash briefing configuration (write-only) channel-type.amazonechocontrol.sendMessage.label = Send Message -channel-type.amazonechocontrol.sendMessage.description = Sends a message to the Echo devices (Write only). +channel-type.amazonechocontrol.sendMessage.description = Sends a message to the Echo devices (write-only). channel-type.amazonechocontrol.shuffle.label = Shuffle channel-type.amazonechocontrol.shuffle.description = Shuffle play -channel-type.amazonechocontrol.smokeAlarmDetectionState.label = Smoke Alarm Detection State -channel-type.amazonechocontrol.smokeAlarmDetectionState.description = Smoke Alarm Detection State channel-type.amazonechocontrol.startCommand.label = Start -channel-type.amazonechocontrol.startCommand.description = Start information (Write only) -channel-type.amazonechocontrol.startCommand.state.option.Weather = Weather -channel-type.amazonechocontrol.startCommand.state.option.Traffic = Traffic -channel-type.amazonechocontrol.startCommand.state.option.GoodMorning = Good morning -channel-type.amazonechocontrol.startCommand.state.option.SingASong = Song -channel-type.amazonechocontrol.startCommand.state.option.TellStory = Story -channel-type.amazonechocontrol.startCommand.state.option.FlashBriefing = Flash briefing +channel-type.amazonechocontrol.startCommand.description = Start information (write-only) channel-type.amazonechocontrol.startRoutine.label = Start a Routine -channel-type.amazonechocontrol.startRoutine.description = The command which must be spoken to active the routing without the preceding "Alexa," (Write Only) +channel-type.amazonechocontrol.startRoutine.description = The command which must be spoken to active the routing without the preceding "Alexa," (write-only) channel-type.amazonechocontrol.subtitle1.label = Subtitle 1 -channel-type.amazonechocontrol.subtitle1.description = Subtitle 1 channel-type.amazonechocontrol.subtitle2.label = Subtitle 2 -channel-type.amazonechocontrol.subtitle2.description = Subtitle 2 channel-type.amazonechocontrol.targetSetpoint.label = Target Setpoint -channel-type.amazonechocontrol.targetSetpoint.description = Target Setpoint channel-type.amazonechocontrol.temperature.label = Temperature -channel-type.amazonechocontrol.temperature.description = Temperature channel-type.amazonechocontrol.textCommand.label = TextCommand -channel-type.amazonechocontrol.textCommand.description = Run a command (Write only). The command can run like a spoken command. +channel-type.amazonechocontrol.textCommand.description = Run a command (write-only). The command can run like a spoken command. channel-type.amazonechocontrol.textToSpeech.label = Speak -channel-type.amazonechocontrol.textToSpeech.description = Speak the text (Write only). It is possible to use plain text or SSML: I want to tell you a secret.I am not a real human..Can you believe it? +channel-type.amazonechocontrol.textToSpeech.description = Speak the text (write-only). It is possible to use plain text or SSML: I want to tell you a secret.I am not a real human..Can you believe it? channel-type.amazonechocontrol.textToSpeechVolume.label = Speak Volume channel-type.amazonechocontrol.textToSpeechVolume.description = Volume of the Speak channel. If 0, the current volume will be used. channel-type.amazonechocontrol.thermostatMode.label = Thermostat Mode -channel-type.amazonechocontrol.thermostatMode.description = Thermostat Mode +channel-type.amazonechocontrol.thermostatMode.state.option.AUTO = Auto +channel-type.amazonechocontrol.thermostatMode.state.option.HEAT = Heat +channel-type.amazonechocontrol.thermostatMode.state.option.COOL = Cool +channel-type.amazonechocontrol.thermostatMode.state.option.OFF = Off +channel-type.amazonechocontrol.thermostatMode.state.option.ECO = Eco channel-type.amazonechocontrol.title.label = Title -channel-type.amazonechocontrol.title.description = Title channel-type.amazonechocontrol.upperSetpoint.label = Upper Setpoint -channel-type.amazonechocontrol.upperSetpoint.description = Upper Setpoint +channel-type.amazonechocontrol.voc.label = VOC channel-type.amazonechocontrol.volume.label = Volume channel-type.amazonechocontrol.volume.description = Volume of the sound channel-type.amazonechocontrol.waterAlarm.label = Water Alarm -channel-type.amazonechocontrol.waterAlarm.description = Water Alarm + +# channel types config + +channel-type.config.amazonechocontrol.color.matchColors.label = Match Colors +channel-type.config.amazonechocontrol.color.matchColors.description = The API does not support sending HSB values. Enabling this allows the binding to try to get the most similar pre-defined color and send that instead. diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/thing/thing-types.xml index de40415c35316..6c5a83e6d5828 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/thing/thing-types.xml @@ -7,7 +7,11 @@ Amazon Account where the amazon echo devices are registered. + + + 1 + @@ -39,6 +43,12 @@ 120 + + + The number of seconds between a voice command was detected and the received command is requested from + the server. + 10 + @@ -65,11 +75,6 @@ - - - - - @@ -85,9 +90,14 @@ + + + + 3 + serialNumber @@ -120,11 +130,6 @@ - - - - - @@ -140,9 +145,14 @@ + + + + 3 + serialNumber @@ -175,11 +185,6 @@ - - - - - @@ -195,9 +200,14 @@ + + + + 3 + serialNumber @@ -210,8 +220,8 @@ - - Amazon Multiroom Music + + Amazon Multiroom (Whole House Audio) Music @@ -223,12 +233,10 @@ - - - - - + + 1 + serialNumber @@ -256,7 +264,7 @@ Smart home device connected to Alexa - 1 + 2 id @@ -283,10 +291,11 @@ - + Switch - Save the current flash briefing configuration (Write only) + Save the current flash briefing configuration (write-only) + veto Switch @@ -297,6 +306,7 @@ String Plays the briefing on the device (serial number or name, write only) + veto String @@ -304,46 +314,24 @@ Connected bluetooth device - - String - - Id of the radio station - String Speak the reminder and send a notification to the Alexa app + veto String - The command which must be spoken to active the routing without the preceding "Alexa," (Write Only) + The command which must be spoken to active the routing without the preceding "Alexa," (write-only) + veto String Plays an alarm sound - - - String - - Id of the amazon music track - - - Switch - - Amazon Music turned on - - - String - - Amazon Music play list id (Write only, no current state) - - - String - - Id of the playlist which was started with openHAB + veto String @@ -365,26 +353,18 @@ String - Title String - Subtitle 1 String - Subtitle 2 - - Switch - - Radio turned on - Switch @@ -431,233 +411,237 @@ String - Music provider String - Voice command as text. E.g. 'Yesterday from the Beatles' (Write only) + Voice command as text. E.g. 'Yesterday from the Beatles' (write-only) + veto - + String - Sends a message to the Echo devices (Write only). + Sends a message to the Echo devices (write-only). + veto + + + Switch + + A command send to this channel refreshes the customer history activity (write-only) + veto String - Display the announcement message on the display (Write only). See in the tutorial section of the binding + Display the announcement message on the display (write-only). See in the tutorial section of the binding description to learn how it's possible to set the title and turn off the sound. + veto String - Speak the text (Write only). It is possible to use plain text or SSML: <speak>I want to tell you a + Speak the text (write-only). It is possible to use plain text or SSML: <speak>I want to tell you a secret.<amazon:effect name="whispered">I am not a real human.</amazon:effect>.Can you believe it?</speak> + veto Dimmer Volume of the Speak channel. If 0, the current volume will be used. - + String - Run a command (Write only). The command can run like a spoken command. + Run a command (write-only). The command can run like a spoken command. + veto String Last voice command spoken to the device. Writing to the channel starts voice output. - - + + String + + Last spoken text of the device. + + + Dimmer Media progress in percent - Number:Time - Media play time - + Number:Time - Media length String - Start information (Write only) - - - - - - - - - - + Start information (write-only) + veto Dimmer - Notification Volume - Switch Ascending alarm up to the configured volume - + + + Switch + + Do Not Disturb mode enabled DateTime - Next Reminder DateTime - Next alarm DateTime - Next music alarm DateTime - Next timer - - Contact - - Glass Break Detection State - - - + Contact - - Smoke Alarm Detection State + Dimmer - Brightness Color - Color + + + + The API does not support sending HSB values. Enabling this allows the binding to try to get the most + similar pre-defined color and send that instead. + false + + String - Color Name String - Color Temperature Name Dimmer - Percentage Switch - Power State + + + + Switch + Dimmer - Power Level String - ARM State Contact - Burglary Alarm Contact - Carbon Monoxide Alarm Contact - Fire Alarm Contact - Water Alarm Number:Temperature - Temperature - - - Number:Dimensionless - - Relative humidity measured by the thermostat. - Humidity - - Measurement - Temperature - - + + + Switch + + + + + + String + + + + + + Contact + + + + + + Location + + Number:Temperature - Target Setpoint Number:Temperature - Upper Setpoint Temperature Setpoint @@ -668,7 +652,6 @@ Number:Temperature - Lower Setpoint Temperature Setpoint @@ -679,7 +662,52 @@ String - Thermostat Mode - + + + + + + + + + + + + + Number + + + + + Number:Dimensionless + + Humidity + + Measurement + Temperature + + + + + Number:Density + + Particulate Matter PM2.5 density. + + + + Number:Dimensionless + + + + + Number + + + + + Number + + + diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/update/instructions.xml b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/update/instructions.xml index a8a36fe8da957..84dae71f91966 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/update/instructions.xml +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/update/instructions.xml @@ -3,12 +3,95 @@ xmlns:update="https://openhab.org/schemas/update-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd"> + + + + amazonechocontrol:refreshActivity + + A command send to this channel refreshes the customer history activity (write-only) + + + + + + + + amazonechocontrol:lastSpokenText + + + + + amazonechocontrol:doNotDisturb + + + + + + + + + + + + + + + amazonechocontrol:lastSpokenText + + + + + amazonechocontrol:doNotDisturb + + + + + + + + + + + + + + + amazonechocontrol:lastSpokenText + + + + + amazonechocontrol:doNotDisturb + + + + + + + + + + + system:color-temperature-abs + + + + + + amazonechocontrol:relativeHumidity + + + Measurement + Temperature + + + @@ -19,4 +102,13 @@ + + + + + + + + + diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/account-detail.vm b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/account-detail.vm new file mode 100644 index 0000000000000..6610d83f266a8 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/account-detail.vm @@ -0,0 +1,95 @@ +#* @vtlvariable name="connection" type="org.openhab.binding.amazonechocontrol.internal.connection.Connection" *# +#* @vtlvariable name="accountPath" type="java.lang.String" *# +#* @vtlvariable name="servletPath" type="java.lang.String" *# +#* @vtlvariable name="account" type="org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler" *# +#* @vtlvariable name="devices" type="java.util.List" *# +#* @vtlvariable name="DEVICE_TYPES" type="java.util.Map" *# + + + + AmazonEchoControl - Account details + + + +

+ Logout + Logout and Re-register + Binding Overview +

+

+ Account "$account.thing.label" + ($account.thing.UID) + $account.thing.status +

+ + + + + + + + + + + + + + + + + + + + + + +
App Name$connection.loginData.deviceName
Customer Name$connection.customerName
Customer Id$connection.loginData.accountCustomerId
Connected to$connection.alexaServer
Logged in since$connection.loginData.loginTime
+ +

Connected Devices

+ + #if($devices.isEmpty()) +

No devices in account.

+ #else + + + + + + + + + #foreach($device in $devices) + #set($thing = $account.getThingBySerialNumber($device.serialNumber)) + #if($device.online) + #set($deviceStatus = "ONLINE") + #else + #set($deviceStatus = "OFFLINE") + #end + + + + + + + + #end +
NameThingSerialnumberDevice TypeFamily
+ $device.accountName + $deviceStatus + + #if($thing) + $thing.label + $thing.status + #else + NONE + #end + $device.serialNumber + #if($device.deviceType) + $DEVICE_TYPES.getOrDefault($device.deviceType, $device.deviceType) + #else + unknown + #end + $device.deviceFamily
+ #end + + diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/binding.vm b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/binding.vm new file mode 100644 index 0000000000000..1388a2b891534 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/binding.vm @@ -0,0 +1,25 @@ +#* @vtlvariable name="servletPath" type="java.lang.String" *# +#* @vtlvariable name="accounts" type="java.util.List" *# + + + + AmazonEchoControl - Binding overview + + + +

+ Binding Overview +

+

Configured Accounts

+ + #foreach($account in $accounts) + + + #end +
+ $account.thing.label ($account.thing.UID) + $account.thing.status + $account.lastKnownDevices.size() child device(s)
+ + + diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/default.css b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/default.css new file mode 100644 index 0000000000000..af4ed25a20be9 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/default.css @@ -0,0 +1,93 @@ +:root { + --aec-text-color: #202070; + --aec-background-color: #f0f7ff; + --aec-border: 1px solid black; +} + +body { + font-family: Arial, Helvetica, sans-serif; + font-size: 3mm; + color: var(--aec-text-color); + background: var(--aec-background-color); + line-height: 1.5em; + margin: .75em; + padding: .75em; + +} + +code { + font-family: monospace; + font-size: 0.9em; + font-weight: normal; +} + +h1 { + padding: .75em; + background: var(--aec-text-color); + color: var(--aec-background-color); + border-radius: 5px; +} + +h2 { + padding:.75em; + background: lightblue; + color: black; + border-radius: 5px; +} + +h3 { + padding: .75em; + text-align: right; +} + +a, a:visited { + color: var(--aec-text-color); + margin-left: 0.5em; + margin-right: 0.5em; + white-space: nowrap; +} +td>a, td>a:visited { + margin-right: 0; + margin-left: 0; +} + +th, td { + text-align: left; + border: 1px solid black; + padding: .5em; +} + +.table-bordered { + border: var(--aec-border); + border-collapse: collapse; + padding: 0.5em; + margin: .75em; + font-size: 1.2em; + border-radius: 5px; + border-style: hidden; + box-shadow: 0 0 0 1px black; +} + +.status { + float: right; + border-radius: 8px; + display: inline-block; + margin-left: 2em; + padding: 3px; + font-size: .9em; + width: 4.5em; + text-align: center; + color:white; +} + +.OFFLINE { + background-color: red; +} + +.ONLINE { + background-color: green; +} + +.UNKNOWN { + background-color: grey; +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/device-detail.vm b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/device-detail.vm new file mode 100644 index 0000000000000..de40399961eb3 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/WEB-INF/device-detail.vm @@ -0,0 +1,58 @@ +#* @vtlvariable name="accountPath" type="java.lang.String" *# +#* @vtlvariable name="servletPath" type="java.lang.String" *# +#* @vtlvariable name="thing" type="org.openhab.core.thing.Thing" *# +#* @vtlvariable name="channels" type="java.util.Map>" *# +#* @vtlvariable name="capabilities" type="java.util.List" *# + + + AmazonEchoControl - $thing.label + + + +

+ Account Overview + Binding Overview +

+

+ Device "$thing.label" + ($thing.UID) +

+ #foreach($channelInfo in $channels.entrySet()) + #set($channel = $thing.getChannel($channelInfo.key)) + #if($channel != "") #* only show if channel is available on device *# +

+ Channel "$channel.label" + ($channelInfo.key) +

+ #if($channelInfo.value.isEmpty()) +

No channel options found.

+ #else + + + + + + #foreach($option in $channelInfo.value) + + + + + #end +
ValueName
$option.value$option.displayName
+ #end + #end + #end +

Device Capabilities

+ #if ($capabilities.isEmpty()) +

No capabilities detected.

+ #else + + #foreach($capability in $capabilities) + + + + #end +
$capability
+ #end + + diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/color.properties b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/color.properties new file mode 100644 index 0000000000000..d3efc3a9b8976 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/color.properties @@ -0,0 +1,105 @@ +azure = 180,6,100 +blue = 240,100,100 +cadet_blue = 182,41,100 +cornflower = 219,58,100 +cyan = 180,100,0 +deep_sky_blue = 195,100,100 +dodger_blue = 210,88,100 +light_sky_blue = 203,45,100 +medium_turquoise = 177,66,100 +pale_turquoise = 180,26,100 +powder_blue = 186,23,100 +royal_blue = 225,72,100 +steel_blue = 207,62,100 +turquoise = 173,72,100 +dark_olive_green = 82,58,100 +forest_green = 120,76,100 +sea_green = 145,68,100 +aquamarine = 159,50,100 +chartreuse = 90,100,100 +dark_sea_green = 120,24,100 +green = 120,100,100 +light_green = 120,40,100 +light_sea_green = 176,81,100 +lime_green = 120,75,100 +medium_sea_green = 146,65,100 +medium_spring_green = 156,100,100 +pale_green = 120,38,100 +spring_green = 149,100,100 +chocolate = 24,85,100 +coral = 15,69,100 +dark_goldenrod = 42,94,100 +dark_orange = 32,100,100 +goldenrod = 42,85,100 +indian_red = 0,55,100 +orange = 38,100,100 +orange_red = 15,100,100 +peru = 28,69,100 +salmon = 16,52,100 +sandy_brown = 26,60,100 +tomato = 9,72,100 +bisque = 30,23,100 +blanched_almond = 34,20,100 +burlywood = 33,39,100 +dark_khaki = 55,43,100 +dark_salmon = 14,47,100 +hot_pink = 328,59,100 +khaki = 53,41,100 +light_coral = 0,47,100 +light_pink = 349,29,100 +light_steel_blue = 213,21,100 +medium_orchid = 287,60,100 +medium_purple = 259,49,100 +moccasin = 36,29,100 +orchid = 301,48,100 +pale_goldenrod = 55,28,100 +peach_puff = 25,27,100 +pink = 348,25,100 +plum = 300,27,100 +rosy_brown = 0,24,100 +tan = 36,33,100 +thistle = 300,11,100 +violet = 300,45,100 +wheat = 38,27,100 +blue_violet = 271,81,100 +dark_orchid = 279,75,100 +dark_violet = 281,100,100 +deep_pink = 327,92,100 +indigo = 273,100,100 +magenta = 300,100,100 +medium_slate_blue = 247,56,100 +medium_violet_red = 322,90,100 +midnight_blue = 240,78,100 +purple = 276,86,100 +rebecca_purple = 269,67,100 +saddle_brown = 25,88,100 +sienna = 18,72,100 +brown = 0,76,100 +crimson = 348,90,100 +firebrick = 0,81,100 +maroon = 337,72,100 +pale_violet_red = 340,49,100 +red = 0,100,100 +alice_blue = 210,6,100 +beige = 60,10,100 +cornsilk = 47,14,100 +honeydew = 120,6,100 +ivory = 60,6,100 +lavender = 255,50,100 +lavender_blush = 341,6,100 +lemon_chiffon = 52,20,100 +light_cyan = 180,12,100 +light_goldenrod = 60,16,100 +light_yellow = 60,12,100 +linen = 26,8,100 +mint_cream = 141,4,100 +misty_rose = 5,12,100 +old_lace = 37,9,100 +papaya_whip = 37,16,100 +seashell = 22,7,100 +white = 0,0,100 +gold = 49,100,100 +green_yellow = 83,82,100 +lime = 75,88,100 +yellow = 60,100,100 +yellow_green = 79,75,100 diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/device_type.properties b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/device_type.properties new file mode 100644 index 0000000000000..7029179a6d0ac --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/device_type.properties @@ -0,0 +1,101 @@ +A10A33FOX2NUBK = Echo Spot +A10L5JEZTKKCZ8 = Vobot-Clock +A11QM4H9HGV71H = Echo Show 5 3rd Gen +A12GXV8XMS007S = FireTV +A15ERDAKK5HQQG = Sonos +A17LGWINFBUTZZ = Anker Roav Viva Alexa +A18O6U1UQFJ0XK = Echo Plus 2.Gen +A1C66CX2XD756O = Fire HD 8 +A1DL2DVDQVK3Q = Apps +A1EIANJ7PNB0Q7 = Echo Show 15 +A1ETW4IXK2PYBP = Echo Auto +A1GIZO9LR81BL5 = Yamaha ATS-1090 +A1H0CMF1XM0ZP4 = Echo Dot/Bose +A1J16TEDOYCZTN = Fire tab +A1JJ0KFC4ZPNJ3 = Echo Input +A1LOQ8ZHF4G510 = Samsung Soundbar Q990B +A1NL4BVLQ4L3N3 = Echo Show +A1NQ0LXWBGVQS9 = Samsung The Frame +A1P31Q3MOWSHOD = Anker Zalo Halo Speaker +A1Q7QCGNMXAKYW = Fire tab 7 (Partial command support) +A1QKZ9D0IJY332 = Samsung QLED (Partial command support) +A1RABVCI4QCIKC = Echo Dot 3.Gen +A1RTAM01W29CUP = Windows App +A1X7HJX9QL16M5 = Bespoken.io +A1XWJRHALS1REP = Echo Show 5 2.Gen +A1Z88NGR2BK6A2 = Echo Show 8 +A1ZB65LA390I4K = Fire HD 10 +A21Z3CGI8UIP0F = Apps +A222D4HGE48EOR = Alexa App Apple Watch +A25OJWHZA1MWNB = Samsung TV Neo +A265XOI9586NML = FireTV Strick v3 +A2825NDLA7WDZV = Apps +A2DS1Q2TPDJ48U = Echo Dot 5.Gen Clock +A2E0SNTXJVT7WK = Fire TV V1 +A2EZ3TS0L1S2KV = Sonos Beam Gen 2 +A2GFL5ZMWNE0PX = Fire TV +A2H4LV5GIZ1JFT = Echo 4 Clock +A2IS7199CJBT71 = LG TV +A2IVLV5VM2W81 = Apps +A2J0R2SD7G9LPA = Tablet +A2JKHJ0PX4J3L3 = FireTV Cube +A2JMVL7QPIXTN5 = Amazfit +A2L8KG0CT86ADW = RaspPi +A2LWARUGJLBYEW = Fire TV Stick V2 +A2M35JJZWCQOMZ = Echo Plus +A2M4YX06LWP8WI = Fire Tab +A2OSP3UA4VC85F = Sonos +A2T0P32DY3F7VB = echosim.io +A2TF17PFR55MTB = Apps +A2U21SRK4QGSE1 = Echo Dot 4.Gen +A2XPGY5LRKB9BE = Fitbit Versa 2 +A2Y04QPFCANLPQ = Bose QC35-II +A2Z8O30CD35N8F = Sonos Arc +A303PJF6ISQ7IC = Echo Auto +A30YDR2MK8HMRV = Echo Dot 3.Gen Clock +A31DTMEEVDDOIV = Fire TV Stick Lite 2020 +A32DOYMUN6DTXA = Echo Dot 3.Gen +A378ND93PD0NC4 = VR Radio +A37SHHQ3NUL7B5 = Bose Homespeaker +A38BPK7OW001EX = Raspberry Alexa +A38EHHIB10L47V = Echo Dot +A39Y3UG1XLEJLZ = Fitbit Sense +A3C9PE6TNYLTCH = Multiroom +A3EVMLQTU6WL1W = Echo Show 8 2.Gen +A3FX4UWTP28V1P = Echo 3 +A3GZUE7F9MEB4U = Fire TV Cube +A3H674413M2EKB = echosim.io +A3HF4YRA2L7XGC = Fire TV Cube +A3L2K717GERE73 = Alexa App Apple Watch +A3NPD82ABCPIDP = Sonos Beam +A3OCOCNTAPDC9O = Mi Smart Band 6 NFC +A3R8XIAIU4HJAX = Echo Show +A3R9S4ZZECZ6YL = Fire Tab HD 10 +A3RBAYBE7VM004 = Echo Studio +A3RMGO6LYLH7YN = Echo Plus 4.Gen +A3S5BH2HU6VAYF = Echo Dot 2.Gen +A3SSG6GR8UU7SN = Echo Sub +A3TCJ8RTT3NVI7 = Listens for Alexa +A3V3VA38K169FO = Fire Tab +A3VRME03NAXFUB = Echo Flex +A4ZP7ZC4PI6TO = Echo Show 5 +A4ZXE0RM7LQ7A = Echo Dot 5.Gen +A7WXQPH584YP = Echo 2.Gen +A8D2OKFFQKQ56 = Bose Smart Soundbar 900 +A8DM4FYR6D3HT = LG WebOS TV +AB72C64C86AW2 = Echo +ADVBD696BHNV5 = Fire TV Stick V1 +AHCEDGRIFN5RP = Xiaomi Smart Fire TV +AHJYKVA63YCAQ = Sonos Roam +AILBSA2LNTOYL = reverb App +AINRG27IL8AS0 = Megablast Speaker +AIPK7MM90V7TB = Echo Show 10 +AKOAGQTKAS9YB = Echo Connect +AKPGW064GI9HE = Fire Stick 4K +AO50AHDYKXRFG = Bose Headphones +AP1F6KUH00XPV = Stereo/Subwoofer Pair +ATNLRCEBX3W4P = Fire HD 10 +AVD3HM0HOJAAL = Sonos One 2.Gen +AVE5HX13UR5NO = Logitech Zero Touch +AVU7CPPF2ZRAS = Fire HD 8 +AWZZ5CVHX2CD = Echo Show 2.Gen \ No newline at end of file diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/registration_capabilities.json b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/registration_capabilities.json new file mode 100644 index 0000000000000..4d0de9f2a2d22 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/registration_capabilities.json @@ -0,0 +1,276 @@ +{ + "legacyFlags": { + "SUPPORTS_COMMS": true, + "SUPPORTS_ARBITRATION": true, + "SCREEN_WIDTH": 1170, + "SUPPORTS_SCRUBBING": true, + "SPEECH_SYNTH_SUPPORTS_TTS_URLS": false, + "SUPPORTS_HOME_AUTOMATION": true, + "SUPPORTS_DROPIN_OUTBOUND": true, + "FRIENDLY_NAME_TEMPLATE": "VOX", + "SUPPORTS_SIP_OUTBOUND_CALLING": true, + "VOICE_PROFILE_SWITCHING_DISABLED": true, + "SUPPORTS_LYRICS_IN_CARD": false, + "SUPPORTS_DATAMART_NAMESPACE": "Vox", + "SUPPORTS_VIDEO_CALLING": true, + "SUPPORTS_PFM_CHANGED": true, + "SUPPORTS_TARGET_PLATFORM": "TABLET", + "SUPPORTS_SECURE_LOCKSCREEN": false, + "AUDIO_PLAYER_SUPPORTS_TTS_URLS": false, + "SUPPORTS_KEYS_IN_HEADER": false, + "SUPPORTS_MIXING_BEHAVIOR_FOR_AUDIO_PLAYER": false, + "AXON_SUPPORT": true, + "SUPPORTS_TTS_SPEECHMARKS": true + }, + "envelopeVersion": "20160207", + "capabilities": [ + { + "version": "0.1", + "interface": "CardRenderer", + "type": "AlexaInterface" + }, + { + "interface": "Navigation", + "type": "AlexaInterface", + "version": "1.1" + }, + { + "type": "AlexaInterface", + "version": "2.0", + "interface": "Alexa.Comms.PhoneCallController" + }, + { + "type": "AlexaInterface", + "version": "1.1", + "interface": "ExternalMediaPlayer" + }, + { + "type": "AlexaInterface", + "interface": "Alerts", + "configurations": { + "maximumAlerts": { + "timers": 2, + "overall": 99, + "alarms": 2 + } + }, + "version": "1.3" + }, + { + "version": "1.0", + "interface": "Alexa.Display.Window", + "type": "AlexaInterface", + "configurations": { + "templates": [ + { + "type": "STANDARD", + "id": "app_window_template", + "configuration": { + "sizes": [ + { + "id": "fullscreen", + "type": "DISCRETE", + "value": { + "value": { + "height": 1440, + "width": 3200 + }, + "unit": "PIXEL" + } + } + ], + "interactionModes": [ + "mobile_mode", + "auto_mode" + ] + } + } + ] + } + }, + { + "type": "AlexaInterface", + "interface": "AccessoryKit", + "version": "0.1" + }, + { + "type": "AlexaInterface", + "interface": "Alexa.AudioSignal.ActiveNoiseControl", + "version": "1.0", + "configurations": { + "ambientSoundProcessingModes": [ + { + "name": "ACTIVE_NOISE_CONTROL" + }, + { + "name": "PASSTHROUGH" + } + ] + } + }, + { + "interface": "PlaybackController", + "type": "AlexaInterface", + "version": "1.0" + }, + { + "version": "1.0", + "interface": "Speaker", + "type": "AlexaInterface" + }, + { + "version": "1.0", + "interface": "SpeechSynthesizer", + "type": "AlexaInterface" + }, + { + "version": "1.0", + "interface": "AudioActivityTracker", + "type": "AlexaInterface" + }, + { + "type": "AlexaInterface", + "interface": "Alexa.Camera.LiveViewController", + "version": "1.0" + }, + { + "type": "AlexaInterface", + "version": "1.0", + "interface": "Alexa.Input.Text" + }, + { + "type": "AlexaInterface", + "interface": "Alexa.PlaybackStateReporter", + "version": "1.0" + }, + { + "version": "1.1", + "interface": "Geolocation", + "type": "AlexaInterface" + }, + { + "interface": "Alexa.Health.Fitness", + "version": "1.0", + "type": "AlexaInterface" + }, + { + "interface": "Settings", + "type": "AlexaInterface", + "version": "1.0" + }, + { + "configurations": { + "interactionModes": [ + { + "dialog": "SUPPORTED", + "interactionDistance": { + "value": 18, + "unit": "INCHES" + }, + "video": "SUPPORTED", + "keyboard": "SUPPORTED", + "id": "mobile_mode", + "uiMode": "MOBILE", + "touch": "SUPPORTED" + }, + { + "video": "UNSUPPORTED", + "dialog": "SUPPORTED", + "interactionDistance": { + "value": 36, + "unit": "INCHES" + }, + "uiMode": "AUTO", + "touch": "SUPPORTED", + "id": "auto_mode", + "keyboard": "UNSUPPORTED" + } + ] + }, + "type": "AlexaInterface", + "interface": "Alexa.InteractionMode", + "version": "1.0" + }, + { + "type": "AlexaInterface", + "configurations": { + "catalogs": [ + { + "type": "IOS_APP_STORE", + "identifierTypes": [ + "URI_HTTP_SCHEME", + "URI_CUSTOM_SCHEME" + ] + } + ] + }, + "version": "0.2", + "interface": "Alexa.Launcher" + }, + { + "interface": "System", + "version": "1.0", + "type": "AlexaInterface" + }, + { + "interface": "Alexa.IOComponents", + "type": "AlexaInterface", + "version": "1.4" + }, + { + "type": "AlexaInterface", + "interface": "Alexa.FavoritesController", + "version": "1.0" + }, + { + "version": "1.0", + "type": "AlexaInterface", + "interface": "Alexa.Mobile.Push" + }, + { + "type": "AlexaInterface", + "interface": "InteractionModel", + "version": "1.1" + }, + { + "interface": "Alexa.PlaylistController", + "type": "AlexaInterface", + "version": "1.0" + }, + { + "interface": "SpeechRecognizer", + "type": "AlexaInterface", + "version": "2.1" + }, + { + "interface": "AudioPlayer", + "type": "AlexaInterface", + "version": "1.3" + }, + { + "type": "AlexaInterface", + "version": "3.1", + "interface": "Alexa.RTCSessionController" + }, + { + "interface": "VisualActivityTracker", + "version": "1.1", + "type": "AlexaInterface" + }, + { + "interface": "Alexa.PlaybackController", + "version": "1.0", + "type": "AlexaInterface" + }, + { + "type": "AlexaInterface", + "interface": "Alexa.SeekController", + "version": "1.0" + }, + { + "interface": "Alexa.Comms.MessagingController", + "type": "AlexaInterface", + "version": "1.0" + } + ] +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/test/java/org/openhab/binding/amazonechocontrol/internal/GsonTypeAdapterFactoriesTest.java b/bundles/org.openhab.binding.amazonechocontrol/src/test/java/org/openhab/binding/amazonechocontrol/internal/GsonTypeAdapterFactoriesTest.java new file mode 100644 index 0000000000000..71882f15e2429 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/test/java/org/openhab/binding/amazonechocontrol/internal/GsonTypeAdapterFactoriesTest.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.openhab.binding.amazonechocontrol.internal.util.NonNullListTypeAdapterFactory; +import org.openhab.binding.amazonechocontrol.internal.util.SerializeNull; +import org.openhab.binding.amazonechocontrol.internal.util.SerializeNullTypeAdapterFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; + +/** + * The {@link GsonTypeAdapterFactoriesTest} contains tests for the various {@link TypeAdapterFactory} implementations + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@SuppressWarnings("unused") +public class GsonTypeAdapterFactoriesTest { + @Test + public void testSerializeNullFactoryReturnsNullForNonAnnotatedClass() { + TypeAdapterFactory factory = new SerializeNullTypeAdapterFactory(); + TypeToken typeToken = TypeToken.get(NonAnnotatedTestTO.class); + TypeAdapter typeAdapter = factory.create(new Gson(), typeToken); + + assertThat(typeAdapter, is(nullValue())); + } + + @Test + public void testSerializeNullFactoryReturnsTypeAdapterForAnnotatedClass() { + TypeAdapterFactory factory = new SerializeNullTypeAdapterFactory(); + TypeToken typeToken = TypeToken.get(AnnotatedTestTO.class); + TypeAdapter typeAdapter = factory.create(new Gson(), typeToken); + + assertThat(typeAdapter, is(notNullValue())); + } + + @Test + public void testNonNullListFactoryReturnsNullForNonListClass() { + TypeAdapterFactory factory = new NonNullListTypeAdapterFactory(); + TypeToken typeToken = TypeToken.get(String.class); + TypeAdapter typeAdapter = factory.create(new Gson(), typeToken); + + assertThat(typeAdapter, is(nullValue())); + } + + @Test + public void testNonNullListFactoryReturnsTypeAdapterForAnnotatedClass() { + TypeAdapterFactory factory = new NonNullListTypeAdapterFactory(); + TypeToken typeToken = TypeToken.getParameterized(List.class, String.class); + TypeAdapter typeAdapter = factory.create(new Gson(), typeToken); + + assertThat(typeAdapter, is(notNullValue())); + } + + @Test + public void testSerializeAnnotatedNull() { + Gson gson = new GsonBuilder().registerTypeAdapterFactory(new SerializeNullTypeAdapterFactory()).create(); + + String serialized = gson.toJson(new AnnotatedTestTO()); + String expected = "{\"annotatedNullValue\":null,\"annotatedNonNullValue\":\"bar\"," + + "\"nonNullValue\":\"foo\",\"serializedNameNonNullValue\":\"foo\"}"; + + assertThat(serialized, is(expected)); + } + + @Test + public void testNullListsAreNotDeserialized() { + Gson gson = new GsonBuilder().registerTypeAdapterFactory(new NonNullListTypeAdapterFactory()).create(); + + String in = "{\"list\" : [\"foo\"],\"nullList\" : null}"; + + ListTestTO listTest = Objects.requireNonNull(gson.fromJson(in, ListTestTO.class)); + assertThat(listTest.list, is(List.of("foo"))); + assertThat(listTest.nullList, is(List.of())); + assertThat(listTest.missingList, is(List.of())); + } + + @Test + public void combinedTest() { + String in = "{\"list\" : null}"; + + Gson gson = new GsonBuilder().registerTypeAdapterFactory(new SerializeNullTypeAdapterFactory()) + .registerTypeAdapterFactory(new NonNullListTypeAdapterFactory()).create(); + + CombinedTestTO combined = Objects.requireNonNull(gson.fromJson(in, CombinedTestTO.class)); + assertThat(combined.list, is(List.of())); + + String expected = "{\"serializeNullString\":null,\"list\":[]}"; + String out = gson.toJson(combined); + assertThat(out, is(expected)); + } + + private static class ListTestTO { + public List list = List.of(); + public List nullList = List.of(); + public List missingList = List.of(); + } + + private static class CombinedTestTO { + @SerializeNull + public @Nullable String serializeNullString = null; + public @Nullable String noSerializeNullString = null; + public List list = List.of(); + } + + private static class AnnotatedTestTO extends NonAnnotatedTestTO { + @SerializeNull + public @Nullable String annotatedNullValue = null; + + @SerializeNull + public @Nullable String annotatedNonNullValue = "bar"; + } + + private static class NonAnnotatedTestTO { + public @Nullable String nullValue = null; + + public String nonNullValue = "foo"; + + @SerializedName("serializedNameNullValue") + public @Nullable String nullValue2 = null; + + @SerializedName("serializedNameNonNullValue") + public String nonNullNullValue2 = "foo"; + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/test/java/org/openhab/binding/amazonechocontrol/internal/ServletUriTest.java b/bundles/org.openhab.binding.amazonechocontrol/src/test/java/org/openhab/binding/amazonechocontrol/internal/ServletUriTest.java new file mode 100644 index 0000000000000..5ccd801313314 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/test/java/org/openhab/binding/amazonechocontrol/internal/ServletUriTest.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlServlet.SERVLET_PATH; + +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * The {@link ServletUriTest} contains tests for the {@link ServletUri} record + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class ServletUriTest { + + private static Stream testGetStrippedUri() { + return Stream.of(Arguments.of(SERVLET_PATH, new ServletUri("", "")), // + Arguments.of(SERVLET_PATH + "/", new ServletUri("", "")), // + Arguments.of(SERVLET_PATH + "/accountUid", new ServletUri("accountUid", "")), // + Arguments.of(SERVLET_PATH + "/accountUid/", new ServletUri("accountUid", "")), // + Arguments.of(SERVLET_PATH + "/accountUid/foo/bar", new ServletUri("accountUid", "/foo/bar")), // + Arguments.of(SERVLET_PATH + "/accountUid/foo/bar/", new ServletUri("accountUid", "/foo/bar/")), // + Arguments.of("/foo/bar", null), // + Arguments.of(null, null)); + } + + @ParameterizedTest + @MethodSource + public void testGetStrippedUri(@Nullable String in, @Nullable ServletUri expected) { + assertThat(in, ServletUri.fromFullUri(in), is(expected)); + } +} diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/test/java/org/openhab/binding/amazonechocontrol/internal/smarthome/AlexaColorTest.java b/bundles/org.openhab.binding.amazonechocontrol/src/test/java/org/openhab/binding/amazonechocontrol/internal/smarthome/AlexaColorTest.java new file mode 100644 index 0000000000000..f10bf50000da8 --- /dev/null +++ b/bundles/org.openhab.binding.amazonechocontrol/src/test/java/org/openhab/binding/amazonechocontrol/internal/smarthome/AlexaColorTest.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal.smarthome; + +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants; +import org.openhab.core.test.java.JavaTest; + +/** + * The {@link AlexaColorTest} is a + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class AlexaColorTest extends JavaTest { + + @ParameterizedTest + @MethodSource("getColors") + public void distanceTest(AlexaColor color) { + Assertions.assertEquals(color.colorName, AlexaColor.getClosestColorName(color.value)); + } + + @SuppressWarnings("unused") + private static Stream getColors() { + return AmazonEchoControlBindingConstants.ALEXA_COLORS.stream(); + } +}