[amazonechocontrol] Import SmartHomeJ fork (#17935)

Both forks have diverged a reasonable amount. 

Signed-off-by: Cody Cutrer <cody@cutrer.us>
Co-authored-by: Jan N. Klug <github@klug.nrw>
Co-authored-by: Tom Blum <trinitus01@googlemail.com>
pull/18293/head
Cody Cutrer 2025-02-19 02:40:24 -07:00 committed by GitHub
parent 64eeeae32c
commit f2e4492c31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
244 changed files with 12732 additions and 9548 deletions

View File

@ -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

View File

@ -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 |
| `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`).
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
### Channels
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.
| 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.<FlahshbriefingDeviceID> (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 its 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. `<speak>I want to tell you a secret.<amazon:effect name="whispered">I am not a real human.</amazon:effect></speak>` |
| 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 |
#### `smartHomeDevice` and `smartHomeDeviceGroup` Thing Configuration
## Advanced Feature Technically Experienced Users
| Configuration name | Description |
|--------------------------|---------------------------------------------------------------------------|
| id | The id of the device or device group |
The url <YOUR_OPENHAB>/amazonechocontrol/<YOUR_ACCOUNT>/PROXY/<API_URL> provides a proxy server with an authenticated connection to the Amazon Alexa server.
This can be used to call Alexa API from rules.
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.
E.g. to read out the history call from an installation on openhab:8080 with an account named account1:
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
http://openhab:8080/amazonechocontrol/account1/PROXY/api/activities?startTime=&size=50&offset=1
## Channels
### Example
| 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.\<FlahshbriefingDeviceID> (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 its 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. `<speak>I want to tell you a secret.<amazon:effect name="whispered">I am not a real human.</amazon:effect></speak>` |
| 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.
## Full 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>
// Player control
Player Echo_Living_Room_Player "Player" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:player"}
Dimmer Echo_Living_Room_Volume "Volume [%.0f %%]" <soundvolume> (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 %%]" <soundvolume> (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" <bluetooth> (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothMAC"}
Switch Echo_Living_Room_Bluetooth "Bluetooth" <bluetooth> (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetooth"}
String Echo_Living_Room_BluetoothDeviceName "Bluetooth Device" <bluetooth> (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothDeviceName"}
String Echo_Living_Room_BluetoothMAC "Bluetooth MAC Address" <bluetooth> (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothMAC"}
Switch Echo_Living_Room_Bluetooth "Bluetooth" <bluetooth> (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetooth"}
String Echo_Living_Room_BluetoothDeviceName "Bluetooth Device" <bluetooth> (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.<YOUR FLASHBRIEFING THING ID>
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 <YOUR_DEVICE_MAC> Place holder with the MAC address shown, if Alexa is connected to the device
Selection item=Echo_Living_Room_BluetoothMAC mappings=[ ''='Disconnected', '<YOUR_DEVICE_MAC>'='Bluetooth Device 1', '<YOUR_DEVICE_MAC>'='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 <YOUR_OPENHAB>/amazonechocontrol/<YOUR_ACCOUNT>/PROXY/<API_URL> 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":"<Speak>", "title": "<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

View File

@ -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>

View File

@ -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>

View File

@ -25,4 +25,5 @@ public class AccountHandlerConfig {
public int discoverSmartHome = 0;
public int pollingIntervalSmartHomeAlexa = 60;
public int pollingIntervalSmartSkills = 120;
public int activityRequestDelay = 10;
}

View File

@ -1,719 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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("</title><head><body>");
html.append("<h1>" + HtmlEscape.escapeHtml4(BINDING_NAME + " - " + this.account.getThing().getLabel()));
if (!title.isEmpty()) {
html.append(" - ");
html.append(HtmlEscape.escapeHtml4(title));
}
html.append("</h1>");
return html;
}
private void createPageEndAndSent(HttpServletResponse resp, StringBuilder html) {
// account overview link
html.append("<br><a href='" + servletUrl + "/../' >");
html.append(HtmlEscape.escapeHtml4("Account overview"));
html.append("</a><br>");
html.append("</body></html>");
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("<h2>Capabilities</h2>");
html.append("<table><tr><th align='left'>Name</th></tr>");
device.getCapabilities().forEach(
capability -> html.append("<tr><td>").append(HtmlEscape.escapeHtml4(capability)).append("</td></tr>"));
html.append("</table>");
}
private void renderMusicProviderIdChannel(Connection connection, StringBuilder html) {
html.append("<h2>").append(HtmlEscape.escapeHtml4("Channel " + CHANNEL_MUSIC_PROVIDER_ID)).append("</h2>");
html.append("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
List<JsonMusicProvider> musicProviders = connection.getMusicProviders();
for (JsonMusicProvider musicProvider : musicProviders) {
List<String> 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("<tr><td>");
html.append(HtmlEscape.escapeHtml4(displayName));
html.append("</td><td>");
html.append(HtmlEscape.escapeHtml4(providerId));
html.append("</td></tr>");
}
}
html.append("</table>");
}
private void renderPlayAlarmSoundChannel(Connection connection, Device device, StringBuilder html) {
html.append("<h2>").append(HtmlEscape.escapeHtml4("Channel " + CHANNEL_PLAY_ALARM_SOUND)).append("</h2>");
List<JsonNotificationSound> 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("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
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("<tr><td>");
html.append(HtmlEscape.escapeHtml4(notificationSound.displayName));
html.append("</td><td>");
html.append(HtmlEscape.escapeHtml4(providerSoundId));
html.append("</td></tr>");
}
}
html.append("</table>");
} else {
html.append(HtmlEscape.escapeHtml4(errorMessage));
}
}
private void renderAmazonMusicPlaylistIdChannel(Connection connection, Device device, StringBuilder html) {
html.append("<h2>").append(HtmlEscape.escapeHtml4("Channel " + CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID))
.append("</h2>");
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<String, PlayList @Nullable []> playlistMap = playLists.playlists;
if (playlistMap != null && !playlistMap.isEmpty()) {
html.append("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
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("<tr><td>");
html.append(HtmlEscape.escapeHtml4(nullReplacement(playList.title)));
html.append("</td><td>");
html.append(HtmlEscape.escapeHtml4(nullReplacement(playList.playlistId)));
html.append("</td></tr>");
}
}
}
}
html.append("</table>");
} else {
html.append(HtmlEscape.escapeHtml4(errorMessage));
}
}
}
private void renderBluetoothMacChannel(Connection connection, Device device, StringBuilder html) {
html.append("<h2>").append(HtmlEscape.escapeHtml4("Channel " + CHANNEL_BLUETOOTH_MAC)).append("</h2>");
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<PairedDevice> pairedDeviceList = state.getPairedDeviceList();
if (!pairedDeviceList.isEmpty()) {
html.append("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
for (PairedDevice pairedDevice : pairedDeviceList) {
html.append("<tr><td>");
html.append(HtmlEscape.escapeHtml4(nullReplacement(pairedDevice.friendlyName)));
html.append("</td><td>");
html.append(HtmlEscape.escapeHtml4(nullReplacement(pairedDevice.address)));
html.append("</td></tr>");
}
html.append("</table>");
} 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<String, String> 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=\"&#x2F;", "action=\"" + servletUrl + "/")
.replace("https://www." + amazonSite + "/", servletUrl + "/")
.replace("https://www." + amazonSite + ":443" + "/", servletUrl + "/")
.replace("https:&#x2F;&#x2F;www." + amazonSite + "&#x2F;", servletUrl + "/")
.replace("https:&#x2F;&#x2F;www." + amazonSite + ":443" + "&#x2F;", servletUrl + "/")
.replace("http://www." + amazonSite + "/", servletUrl + "/")
.replace("http:&#x2F;&#x2F;www." + amazonSite + "&#x2F;", 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("<html>" + HtmlEscape.escapeHtml4(message) + "<br><a href='" + servletUrl
+ "'>Try again</a></html>");
} catch (IOException e) {
logger.info("Returning error message failed", e);
}
}
}

View File

@ -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<ThingTypeUID> 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<ThingTypeUID> 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<ThingTypeUID> 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<ThingTypeUID> 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<String, String> 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<AlexaColor> ALEXA_COLORS = ResourceUtil
.readProperties(AlexaColor.class, "color.properties").entrySet().stream()
.map(e -> new AlexaColor(e.getKey(), new HSBType(e.getValue()))).toList();
}

View File

@ -0,0 +1,116 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<FlashBriefingProfileHandler> flashBriefingProfileHandlers,
Collection<DeviceTO> targets) {
List<CommandOption> 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<EchoHandler> echoHandlers,
Collection<FlashBriefingProfileHandler> flashBriefingProfileHandlers) {
List<CommandOption> 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<NotificationSoundTO> alarmSounds) {
List<CommandOption> 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));
}
}

View File

@ -14,44 +14,35 @@ package org.openhab.binding.amazonechocontrol.internal;
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 @@ import com.google.gson.Gson;
AmazonEchoControlHandlerFactory.class }, configurationPid = "binding.amazonechocontrol")
@NonNullByDefault
public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(AmazonEchoControlHandlerFactory.class);
private final Map<ThingUID, List<ServiceRegistration<?>>> discoveryServiceRegistrations = new HashMap<>();
private final Set<AccountHandler> 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 class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory {
|| 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<String> 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<String> storage = storageService.getStorage(thing.getUID().toString(),
String.class.getClassLoader());
return new FlashBriefingProfileHandler(thing, storage);
Storage<? super Object> 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<ServiceRegistration<?>> 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<ServiceRegistration<?>> 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);
}
}
});
}
}
}

View File

@ -0,0 +1,516 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<String, String[]> 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<String, List<ChannelOption>> channels = new HashMap<>();
List<ChannelOption> 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<ChannelOption> 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<BluetoothStateTO> states = connection.getBluetoothConnectionStates();
List<ChannelOption> 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<String, String> 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=\"&#x2F;", "action=\"" + servletUrl)
.replace("https://www." + retailDomain + "/", servletUrl)
.replace("https://www." + retailDomain + ":443" + "/", servletUrl)
.replace("https:&#x2F;&#x2F;www." + retailDomain + "&#x2F;", servletUrl)
.replace("https:&#x2F;&#x2F;www." + retailDomain + ":443" + "&#x2F;", servletUrl)
.replace("http://www." + retailDomain + "/", servletUrl)
.replace("http:&#x2F;&#x2F;www." + retailDomain + "&#x2F;", 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("<html>" + escapeHtml4(message) + "<br><a href='" + tryAgainUri + "'>Try again</a></html>");
}
private 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 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;
}
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<ChannelUID, StateDescription> 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;
}
}
}

View File

@ -1,250 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<PairedDevice> pairedDeviceList = bluetoothState.getPairedDeviceList();
if (pairedDeviceList.isEmpty()) {
return null;
}
List<StateOption> 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<StateOption> options = new ArrayList<>();
options.add(new StateOption("", ""));
Map<String, PlayList @Nullable []> 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<JsonNotificationSound> notificationSounds = handler.findAlarmSounds();
if (notificationSounds.isEmpty()) {
return null;
}
List<StateOption> 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<Device> devices = accountHandler.getLastKnownDevices();
if (devices.isEmpty()) {
return null;
}
List<StateOption> 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<JsonMusicProvider> musicProviders = handler.findMusicProviders();
if (musicProviders.isEmpty()) {
return null;
}
List<StateOption> options = new ArrayList<>();
for (JsonMusicProvider musicProvider : musicProviders) {
List<String> 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<FlashBriefingProfileHandler> flashbriefings = account.getFlashBriefingProfileHandlers();
if (flashbriefings.isEmpty()) {
return null;
}
List<StateOption> 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;
}
}

View File

@ -1,130 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<Thing> 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("<html><head><title>" + HtmlEscape.escapeHtml4(BINDING_NAME) + "</title><head><body>");
html.append("<h1>" + HtmlEscape.escapeHtml4(BINDING_NAME) + "</h1>");
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("<a href='./" + url + " '>" + HtmlEscape.escapeHtml4(accountHandler.getLabel())
+ "</a><br>");
}
}
}
html.append("</body></html>");
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);
}
}
}

View File

@ -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 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
* @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);
}
}

View File

@ -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 @@ public class ConsoleCommandExtension extends AbstractConsoleCommandExtension {
.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.");
}

View File

@ -1,36 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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;
}
}

View File

@ -1,27 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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);
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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) : "");
}
}

View File

@ -1,608 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<HttpCookie> 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<HttpCookie> 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;
}
}
}

View File

@ -1,62 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 <T> @Nullable T tryParseJson(String json, Class<T> type) {
try {
return gson.fromJson(json, type);
} catch (JsonSyntaxException e) {
logger.debug("Json parse error", e);
return null;
}
}
protected <T> @Nullable T parseJson(String json, Class<T> type) throws JsonSyntaxException {
return gson.fromJson(json, type);
}
}

View File

@ -1,112 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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>")) {
speak = "<speak>" + XmlEscape.escapeXml10(speak) + "</speak>";
}
if (sound && speak.startsWith("<speak>")) {
body = "Error: The combination of sound and speak in SSML syntax is not allowed";
title = "Error";
speak = "<speak><lang xml:lang=\"en-UK\">Error: The combination of sound and speak in <prosody rate=\"x-slow\"><say-as interpret-as=\"characters\">SSML</say-as></prosody> syntax is not allowed</lang></speak>";
}
}
if ("<speak> </speak>".equals(speak)) {
volume = -1; // Do not change volume
}
}
} catch (JsonSyntaxException e) {
body = "Invalid Json." + e.getLocalizedMessage();
title = "Error";
speak = "<speak><lang xml:lang=\"en-US\">" + XmlEscape.escapeXml10(body) + "</lang></speak>";
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;
}
}

View File

@ -1,133 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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;
}
}
}

View File

@ -1,36 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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;
}

View File

@ -1,32 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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;
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<DeviceTO> 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<DeviceTO> 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>") && speak.endsWith("</speak>")) ? "ssml" : "text";
return announcement;
}
}

View File

@ -0,0 +1,337 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<Cookie> 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);
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<DeviceTO> 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<DeviceTO> 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 + "}";
}
}

View File

@ -15,25 +15,26 @@ package org.openhab.binding.amazonechocontrol.internal.discovery;
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 @@ import org.slf4j.LoggerFactory;
* 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<AccountHandler> {
private static final int BACKGROUND_INTERVAL = 10; // in seconds
private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class);
private final Set<String> discoveredFlashBriefings = new HashSet<>();
private final Set<List<EnabledFeedTO>> 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 @@ public class AmazonEchoDiscovery extends AbstractDiscoveryService {
if (activateTimeStamp != null) {
removeOlderResults(activateTimeStamp);
}
setDevices(accountHandler.updateDeviceList());
setDevices(thingHandler.updateDeviceList());
String currentFlashBriefingConfiguration = accountHandler.getNewCurrentFlashbriefingConfiguration();
List<EnabledFeedTO> 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 @@ public class AmazonEchoDiscovery extends AbstractDiscoveryService {
}
@Override
@Activate
public void activate(@Nullable Map<String, Object> 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<Device> deviceList) {
for (Device device : deviceList) {
private synchronized void setDevices(List<DeviceTO> 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, "<unknown>"))
.withRepresentationProperty(DEVICE_PROPERTY_SERIAL_NUMBER).withBridge(bridgeThingUID)
.build();
@ -170,27 +161,21 @@ public class AmazonEchoDiscovery extends AbstractDiscoveryService {
}
}
public synchronized void discoverFlashBriefingProfiles(String currentFlashBriefingJson) {
if (currentFlashBriefingJson.isEmpty()) {
private synchronized void discoverFlashBriefingProfiles(List<EnabledFeedTO> 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);
}
}
}

View File

@ -14,9 +14,7 @@ package org.openhab.binding.amazonechocontrol.internal.discovery;
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 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.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<AccountHandler> {
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);
}
@Override
protected void stopBackgroundDiscovery() {
stopScanJob();
}
void stopScanJob() {
ScheduledFuture<?> currentStartScanStateJob = startScanStateJob;
if (currentStartScanStateJob != null) {
currentStartScanStateJob.cancel(false);
startScanStateJob = null;
}
protected void stopScan() {
removeOlderResults(getTimestampOfLastScan());
super.stopScan();
}
@Override
@Activate
public void activate(@Nullable Map<String, Object> config) {
super.activate(config);
if (config != null) {
modified(config);
}
Long activateTimeStamp = this.activateTimeStamp;
if (activateTimeStamp == null) {
this.activateTimeStamp = new Date().getTime();
protected void startBackgroundDiscovery() {
ScheduledFuture<?> discoveryJob = this.discoveryJob;
if (discoveryJob == null || discoveryJob.isCancelled()) {
this.discoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 1, 5, TimeUnit.MINUTES);
}
}
synchronized void setSmartHomeDevices(List<SmartHomeBaseDevice> deviceList) {
int smartHomeDeviceDiscoveryMode = accountHandler.getSmartHomeDevicesDiscoveryMode();
@Override
protected void stopBackgroundDiscovery() {
ScheduledFuture<?> discoveryJob = this.discoveryJob;
if (discoveryJob != null) {
discoveryJob.cancel(true);
this.discoveryJob = null;
}
}
private synchronized void setSmartHomeDevices(List<SmartHomeBaseDevice> 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<String, Object> 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 @@ public class SmartHomeDevicesDiscovery extends AbstractDiscoveryService {
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 @@ public class SmartHomeDevicesDiscovery extends AbstractDiscoveryService {
thingUID = new ThingUID(THING_TYPE_SMART_HOME_DEVICE, bridgeThingUID, entityId.replace(".", "-"));
List<JsonSmartHomeDeviceAlias> 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<JsonSmartHomeDevice.DeviceIdentifier> 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 @@ public class SmartHomeDevicesDiscovery extends AbstractDiscoveryService {
// No id
continue;
}
Set<SmartHomeDevice> supportedChildren = SmartHomeDeviceHandler.getSupportedSmartHomeDevices(shg,
Set<JsonSmartHomeDevice> supportedChildren = SmartHomeDeviceHandler.getSupportedSmartHomeDevices(shg,
deviceList);
if (supportedChildren.isEmpty()) {
// No children with a supported interface

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -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<T> {
public T operationPayload;
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<String> profiles = List.of();
@Override
public @NonNull String toString() {
return "BluetoothPairedDeviceTO{address='" + address + "', connected=" + connected + ", deviceClass='"
+ deviceClass + "', friendlyName='" + friendlyName + "', profiles=" + profiles + "}";
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<String> 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 + "}";
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<EnabledFeedTO> enabledFeeds = List.of();
@Override
public @NonNull String toString() {
return "EnabledFeedsTO{enabledFeeds=" + enabledFeeds + "}";
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<BehaviorOperationValidationResultTO<PlaySearchPhraseTO>> VALIDATION_RESULT_TO_TYPE_TOKEN = (TypeToken<BehaviorOperationValidationResultTO<PlaySearchPhraseTO>>) 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
+ "'}";
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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
+ "}";
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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
+ "'}";
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,110 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<String, Object>> MAP_TYPE_TOKEN = (TypeToken<Map<String, Object>>) TypeToken
.getParameterized(Map.class, String.class, Object.class);
private TOMapper() {
// prevent instantiation
}
public static Map<String, Object> 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);
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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;
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<RenderingUpdateTO> 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 + "'}";
}
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<AnnouncementContentTO> 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 + "'}";
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<DeviceIdTO> devices = List.of();
@Override
public @NonNull String toString() {
return "AnnouncementTargetTO{customerId='" + customerId + "', devices=" + devices + "}";
}
}

View File

@ -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 + "'}";
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<CookieTO> webSiteCookies = List.of();
public String domain = ".amazon.com";
@Override
public @NonNull String toString() {
return "AuthRegisterCookiesTO{webSiteCookies=" + webSiteCookies + ", domain='" + domain + "'}";
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<String> 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<String, String> userContextMap = Map.of();
@SerializedName("requested_token_type")
public List<String> 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 + "}";
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<String, List<CookieTO>> cookies = new HashMap<>();
@Override
public @NonNull String toString() {
return "ExchangeTokenTokensTO{cookies=" + cookies + "}";
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<String, Object> 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 + "}";
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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
+ "'}";
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<AccountTO>> LIST_TYPE_TOKEN = (TypeToken<List<AccountTO>>) 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
+ "}";
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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<AscendingAlarmModelTO> ascendingAlarmModelList = List.of();
@Override
public @NonNull String toString() {
return "AscendingAlarmModelsTO{ascendingAlarmModelList=" + ascendingAlarmModelList + "}";
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "'}";
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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 + "}";
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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
+ "'}";
}
}

Some files were not shown because too many files have changed in this diff Show More