From 7d97f67450a05ab70ed410b412649ddbc2f52e0d Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sat, 6 Jan 2018 22:20:26 +0100 Subject: [PATCH 01/56] [amazonechocontrol] Initial commit for the Amazon Control Binding (#3083) Signed-off-by: Michael Geramb (github: mgeramb) --- .../.classpath | 7 + .../.project | 33 ++ .../ESH-INF/binding/binding.xml | 13 + .../i18n/amazonechocontrol_xx_XX.properties | 13 + .../ESH-INF/thing/thing-types.xml | 192 +++++++ .../META-INF/MANIFEST.MF | 30 ++ .../OSGI-INF/.gitignore | 1 + .../README.md | 145 ++++++ .../about.html | 32 ++ .../build.properties | 7 + .../pom.xml | 19 + .../AmazonEchoControlBindingConstants.java | 60 +++ .../discovery/AmazonEchoDiscovery.java | 186 +++++++ .../discovery/IAmazonEchoDiscovery.java | 5 + .../handler/AccountHandler.java | 370 ++++++++++++++ .../handler/EchoHandler.java | 462 +++++++++++++++++ .../internal/AccountConfiguration.java | 28 + .../AmazonEchoControlHandlerFactory.java | 66 +++ .../internal/Connection.java | 483 ++++++++++++++++++ .../internal/HttpException.java | 35 ++ .../internal/jsons/JsonBluetoothStates.java | 44 ++ .../internal/jsons/JsonDevices.java | 17 + .../internal/jsons/JsonMediaState.java | 63 +++ .../internal/jsons/JsonPlayerState.java | 46 ++ addons/binding/pom.xml | 1 + .../src/main/feature/feature.xml | 5 + 26 files changed, 2363 insertions(+) create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/.classpath create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/.project create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_xx_XX.properties create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/OSGI-INF/.gitignore create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/README.md create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/about.html create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/build.properties create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/pom.xml create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/AmazonEchoDiscovery.java create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/IAmazonEchoDiscovery.java create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/.classpath b/addons/binding/org.openhab.binding.amazonechocontrol/.classpath new file mode 100755 index 0000000000000..a9d178f069e87 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/.project b/addons/binding/org.openhab.binding.amazonechocontrol/.project new file mode 100755 index 0000000000000..3f4c8692d661a --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/.project @@ -0,0 +1,33 @@ + + + org.openhab.binding.amazonechocontrol + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml new file mode 100755 index 0000000000000..f2e4d0fcfb43b --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml @@ -0,0 +1,13 @@ + + + + Amazon Echo Control Binding + Binding for controlling Amazon Echo (Alexa). This binding enables openhab to control the volume, playing state, bluetooth connection of your amazon echo devices. + + Michael Geramb + + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_xx_XX.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_xx_XX.properties new file mode 100755 index 0000000000000..d8f73548cf33d --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_xx_XX.properties @@ -0,0 +1,13 @@ +# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE +# FIXME: please do not add the file to the repo if you add or change no content +# binding +binding.amazonechocontrol.name = +binding.amazonechocontrol.description = + +# thing types +thing-type.amazonechocontrol.sample.label = +thing-type.amazonechocontrol.sample.description = + +# channel types +channel-type.amazonechocontrol.sample-channel.label = +channel-type.amazonechocontrol.sample-channel.description = \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml new file mode 100755 index 0000000000000..5fd5a2746641e --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -0,0 +1,192 @@ + + + + + + + Amazon Account where your amazon echo is registered. + + + + + + + false + + + + + + + + + Select the site where your amazon account is created. + + + + + + Enter the email address of the amazon account which is used for the amazon echo devices. Note: 2 factor authentication is currently not supported. + + + + password + + Enter the password of the amazon account which is used for the amazon echo devices. + + + 60 + + Refresh state interval in seconds. Lower time causes more network traffic. + Seconds + + + + + + + + + + + + + + + + + Amazon Echo device (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) + + + + + + + + + + + + + + + + + + + + serialNumber + + + + + You will find the serial number of your device in the Alexa app + + + + + + + + String + + Connected bluetooth device + + + + + String + + Id of the radio station + + + + String + + Name of Alexa + + + + + String + + Name of music provider + + + + + + String + + Id of the bluethooth connected device + + + + String + + Url of the album image or radio station logo + + + + + String + + Title + + + + + String + + Subtitle 1 + + + + + String + + Subtitle 2 + + + + + Switch + + Alexa plays radio + + + + Switch + + Connect to last used device + + + + Switch + + Loop + + + + Switch + + Shuffle play + + + + Player + + Content Playing + + + + + Dimmer + + Volume of the sound + + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF new file mode 100755 index 0000000000000..63c48be192824 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF @@ -0,0 +1,30 @@ +Manifest-Version: 1.0 +Bundle-ActivationPolicy: lazy +Bundle-ClassPath: . +Bundle-ManifestVersion: 2 +Bundle-Name: AmazonEchoControl Binding +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-SymbolicName: org.openhab.binding.amazonechocontrol;singleton:=true +Bundle-Vendor: openHAB +Bundle-Version: 2.3.0.qualifier +Export-Package: + org.openhab.binding.amazonechocontrol, + org.openhab.binding.amazonechocontrol.handler +Import-Package: + org.apache.commons.lang.time;version="2.6.0", + org.eclipse.jdt.annotation;resolution:=optional, + org.eclipse.smarthome.config.core, + org.eclipse.smarthome.config.discovery, + org.eclipse.smarthome.core.library.types, + org.eclipse.smarthome.core.thing, + org.eclipse.smarthome.core.thing.binding, + org.eclipse.smarthome.core.thing.binding.builder, + org.eclipse.smarthome.core.thing.type, + org.eclipse.smarthome.core.types, + org.openhab.binding.amazonechocontrol, + org.openhab.binding.amazonechocontrol.handler, + org.openhab.core.library.types, + org.osgi.framework;version="1.8.0", + org.slf4j +Service-Component: OSGI-INF/*.xml +Require-Bundle: com.google.gson diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/OSGI-INF/.gitignore b/addons/binding/org.openhab.binding.amazonechocontrol/OSGI-INF/.gitignore new file mode 100755 index 0000000000000..b81c7954b78b3 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/OSGI-INF/.gitignore @@ -0,0 +1 @@ +*.xml \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md new file mode 100755 index 0000000000000..05d8cba3bec55 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -0,0 +1,145 @@ +# Amazon Echo Control Binding + +This binding let control openHAB Amazon Echo devices (Alexa). +It provide features to control and view the current state of: + +- volume +- pause/continue/next track/previous track +- connect/disconnect bluetooth devices +- start playing radio + +Some ideas what you can do in your home by using rules and other openHAB controlled devices: + +- Automatic turn on your amplifier and connect echo with bluetooth if the echo playes music +- If the amplifier was turned of, the echo stop playing and disconnect the bluetooth +- The echo starts playing radio if the light was turned on +- The echo starts playing radio at specified time + +## Note ## + +This binding uses the same API as the Web-Browser-Based Alexa site (alexa.amazon.de). In other words, it simulates a user which is using the web page. +Unfortunately, the binding can get broken if Amazon change the website. + +I have currently only tested the binding with amazon.de If you have an other account, please let me know if it works. Otherwise you will get instruction how you can help me to make it working. + +## Warning ## + +For the connection to the Amazon server, your password of the Amazon account is required, this will be stored in your openHAB thing device configuration. So you should be sure, that nobody other has access to your configuration! + +## What else you should know ## + +All the display options are updated by polling the amazon server. The polling time can be configured, but a minimum of 10 seconds is required. The default is 60 seconds, which means the it can take up to 60 seconds to see the correct state. I do not know, if there is a limit implemented in the amazon server if the polling is too fast and maybe amazon will lock your account. 60 seconds seems to be safe. + +## Supported Things + +| Thing type id | Name | +|--------------------------|-----------------------| +| account | Amazon Account | +| echo | Amazon Echo Device | + + +## Discovery + +The first 'Amazon Account' thing will be automatically discovered. After configuration of the thing with the account data, a 'Amazon Echo' thing will be discovered for each registered device. + +## Binding Configuration + +The binding does not have any configuration. The configuration of your amazon account habe to be done in the 'Amazon Account' device. + +## Thing Configuration + +The Amazon Account device need the following configurations: + +| Config name | Description | +|--------------------------|-----------------------| +| amazonSite | The amazon site where the echos are registered. e.g. amazon.de | +| email | Email of your amazon account | +| password | Password of your amazon account | +| pollingIntervalInSeconds | Polling interval for the device state in seconds. Default 60, minimum 10 | + +The Amazon Echo device need the following configurations: + +| Config name | Description | +|--------------------------|-----------------------| +| serialNumber | Serial number of the amazon echo in the Alexa app | + +You will find the serial number in the alexa app. + +## Channels + +| Channel Type ID | Item Type | Access Mode | Description +|---------------------|-----------|-------------|------------------------------------------------------------------------------------------------------- +| player | Player | R/W | Control the music player e.g. pause/continue/next track/previous track +| volume | Dimmer | R/W | Control the volume +| shuffle | Switch | R/W | Shuffle play if applicable, e.g. playing a playlist +| imageUrl | String | R | Url of the album image or radio station logo +| title | String | R | Title of the current media +| subtitle1 | String | R | Subtitle of the current media +| subtitle2 | String | R | Additional subtitle of the current media +| providerDisplayName | String | R | Name of the music provider +| bluetoothId | String | R/W | Bluetooth device id. Used to connect to a specific device or disconnect if a empty string was provided +| bluetooth | Switch | R/W | Connect/Disconnect to the last used bluetooth device (works after a bluetooth connection was established after the openhab start) +| bluetoothDeviceName | String | R | User friendly name of the connected bluetooth device +| radioStationId | String | R/W | Start playing of a radio station by specifying its id od stops playing if a empty string was provided +| radio | Switch | R/W | Start playing of the last used radio station works after the radio station started after the openhab start) + +## Full Example + +### amzonechocontrol.things + +```php +Bridge amazonechocontrol:account:account1 [amazonSite="amazon.de", email="myaccountemail@myprovider.com", password="secure", pollingIntervalInSeconds=60] +{ + Thing echo echo1 "Alexa" @ "Living Room" [serialNumber="SERIAL_NUMBER"] +} +``` +You will find the serial number in the Alexa app. + +### amzonechocontrol.items: + +``` +Group Alexa_Living_Room + +Player Echo_Living_Room_Player "Player" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:player"} +Dimmer Echo_Living_Room_Volume "Volume [%.0f %%]" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:volume"} +Switch Echo_Living_Room_Shuffle "Shuffle" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:shuffle"} +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_BluetoothId "Bluetooth Id" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothId"} +Switch Echo_Living_Room_Bluetooth "Bluetooth" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetooth"} +String Echo_Living_Room_BluetoothDeviceName "Bluetooth Device" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothDeviceName"} +String Echo_Living_Room_RadioStationId "Radio Station Id" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:radioStationId"} +Switch Echo_Living_Room_Radio "Radio" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:radio"} + +``` + +### amzonechocontrol.sitemap: + +``` +sitemap amzonechocontrol label="Echo Devices" +{ + Frame label="Alexa" { + Default item=Echo_Living_Room_Player + Slider item=Echo_Living_Room_Volume + Switch item=Echo_Living_Room_Shuffle + Text item=Echo_Living_Room_Title + Text item=Echo_Living_Room_Subtitle1 + Text item=Echo_Living_Room_Subtitle2 + Text item=Echo_Living_Room_ProviderDisplayName + Text item=Echo_Living_Room_BluetoothId + Switch item=Echo_Living_Room_Bluetooth + Text item=Echo_Living_Room_BluetoothDeviceName + Text item=Echo_Living_Room_RadioStationId + Switch item=Echo_Living_Room_Radio + } +} +``` + +## Trademark Disclaimer + +All Amazon Echo, Alexa and other products and Amazon and other companies are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/about.html b/addons/binding/org.openhab.binding.amazonechocontrol/about.html new file mode 100755 index 0000000000000..089907bd268e0 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/about.html @@ -0,0 +1,32 @@ + + + + + About + + +

About This Content

+ +

March 30, 2017

+

License

+ +

+ The openHAB community makes available all content in this plug-in ("Content"). Unless otherwise + indicated below, the Content is provided to you under the terms and conditions of the + Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available + at http://www.eclipse.org/legal/epl-v10.html. + For purposes of the EPL, "Program" will mean the Content. +

+ +

+ If you did not receive this Content directly from the openHAB community, the Content is + being redistributed by another party ("Redistributor") and different terms and conditions may + apply to your use of any object code in the Content. Check the Redistributor's license that was + provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise + indicated below, the terms and conditions of the EPL still apply to any source code in the Content + and such source code may be obtained at openhab.org. +

+ + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/build.properties b/addons/binding/org.openhab.binding.amazonechocontrol/build.properties new file mode 100755 index 0000000000000..c67911aff5e9e --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/build.properties @@ -0,0 +1,7 @@ +source..=src/main/java/ +output..=target/classes +bin.includes=META-INF/,\ + .,\ + OSGI-INF/,\ + ESH-INF/,\ + about.html diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/pom.xml b/addons/binding/org.openhab.binding.amazonechocontrol/pom.xml new file mode 100755 index 0000000000000..302ca0b64c80f --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/pom.xml @@ -0,0 +1,19 @@ + + + + 4.0.0 + + + org.openhab.binding + pom + 2.3.0-SNAPSHOT + + + org.openhab.binding.amazonechocontrol + 2.3.0-SNAPSHOT + + AmazonEchoControl Binding + eclipse-plugin + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java new file mode 100755 index 0000000000000..c3a1936f87c16 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2014,2017 by the respective copyright holders. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link AmazonEchoControlBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class AmazonEchoControlBindingConstants { + + private static final String BINDING_ID = "amazonechocontrol"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_ECHO = new ThingTypeUID(BINDING_ID, "echo"); + public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account"); + + public static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet( + Arrays.asList(THING_TYPE_ECHO, THING_TYPE_ACCOUNT)); + + // List of all Channel ids + public static final String CHANNEL_PLAYER = "player"; + public static final String CHANNEL_VOLUME = "volume"; + 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"; + public static final String CHANNEL_SUBTITLE2 = "subtitle2"; + public static final String CHANNEL_PROVIDER_DISPLAY_NAME = "providerDisplayName"; + public static final String CHANNEL_BLUETOOTH_ID = "bluetoothId"; + 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"; + + // List of all Properties + public static final String DEVICE_PROPERTY_SERIAL_NUMBER = "serialNumber"; + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/AmazonEchoDiscovery.java new file mode 100755 index 0000000000000..288a28e2fbac7 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/AmazonEchoDiscovery.java @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2010-2017 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.discovery; + +import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResult; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Modified; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AmazonEchoDiscovery} is responsible for discovering echo devices on + * the amazon account specified in the binding. + * + * @author Michael Geramb - Initial contribution + */ +@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.amazonechocontrol") +public class AmazonEchoDiscovery extends AbstractDiscoveryService { + + private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); + public static AmazonEchoDiscovery instance; + private static List discoveryServices = new ArrayList<>(); + + public static void addDiscoveryHandler(IAmazonEchoDiscovery discoveryService) { + synchronized (discoveryServices) { + discoveryServices.add(discoveryService); + } + + } + + public static void removeDiscoveryHandler(IAmazonEchoDiscovery discoveryService) { + synchronized (discoveryServices) { + discoveryServices.remove(discoveryService); + } + } + + public AmazonEchoDiscovery() { + super(SUPPORTED_THING_TYPES_UIDS, 10); + } + + @Override + public void deactivate() { + super.deactivate(); + } + + static boolean discoverAccount = true; + + public static void setHandlerExist() { + discoverAccount = false; + } + + @Override + protected void startScan() { + if (startScanStateJob != null) { + startScanStateJob.cancel(false); + startScanStateJob = null; + } + if (discoverAccount) { + + discoverAccount = false; + // No accounts created yet, create one + ThingUID thingUID = new ThingUID(THING_TYPE_ACCOUNT, "account1"); + + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("Amazon Account").build(); + + logger.debug("Device [Amazon Account] found."); + + thingDiscovered(result); + } + + IAmazonEchoDiscovery[] accounts; + + synchronized (discoveryServices) { + accounts = new IAmazonEchoDiscovery[discoveryServices.size()]; + accounts = discoveryServices.toArray(accounts); + } + + for (IAmazonEchoDiscovery discovery : accounts) { + discovery.updateDeviceList(); + } + + } + + ScheduledFuture startScanStateJob; + + @Override + protected void startBackgroundDiscovery() { + AmazonEchoDiscovery.instance = this; + if (startScanStateJob != null) { + startScanStateJob.cancel(false); + startScanStateJob = null; + } + + startScanStateJob = scheduler.schedule(() -> { + + startScan(); + }, 3000, TimeUnit.MILLISECONDS); + + } + + @Override + protected void stopBackgroundDiscovery() { + AmazonEchoDiscovery.instance = null; + if (startScanStateJob != null) { + startScanStateJob.cancel(false); + startScanStateJob = null; + } + + } + + @Override + @Activate + public void activate(Map config) { + super.activate(config); + modified(config); + }; + + @Override + @Modified + protected void modified(Map config) { + super.modified(config); + + } + + Map lastDeviceInformations = new HashMap<>(); + + public synchronized void setDevices(ThingUID brigdeThingUID, Device[] deviceInformations) { + + Set toRemove = new HashSet(lastDeviceInformations.keySet()); + for (Device deviceInformation : deviceInformations) { + String serialNumber = deviceInformation.serialNumber; + + boolean alreadyfound = toRemove.remove(serialNumber); + // new + if (!alreadyfound && deviceInformation.deviceFamily != null + && deviceInformation.deviceFamily.equals("ECHO")) { + + ThingUID thingUID = new ThingUID(THING_TYPE_ECHO, brigdeThingUID, serialNumber); + + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) + .withLabel(deviceInformation.accountName) + .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) + .withRepresentationProperty(DEVICE_PROPERTY_SERIAL_NUMBER).withBridge(brigdeThingUID).build(); + + logger.debug("Device [{}, {}] found.", serialNumber, deviceInformation.accountName); + + thingDiscovered(result); + lastDeviceInformations.put(serialNumber, thingUID); + } + + } + } + + public synchronized void removeExisting(@NonNull ThingUID uid) { + for (String id : lastDeviceInformations.keySet()) { + if (lastDeviceInformations.get(id).equals(uid)) { + lastDeviceInformations.remove(id); + } + } + } + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/IAmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/IAmazonEchoDiscovery.java new file mode 100755 index 0000000000000..f040999f68bb8 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/IAmazonEchoDiscovery.java @@ -0,0 +1,5 @@ +package org.openhab.binding.amazonechocontrol.discovery; + +public interface IAmazonEchoDiscovery { + void updateDeviceList(); +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java new file mode 100755 index 0000000000000..07b4740e36d75 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -0,0 +1,370 @@ +/** + * Copyright (c) 2010-2017 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.handler; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.openhab.binding.amazonechocontrol.discovery.AmazonEchoDiscovery; +import org.openhab.binding.amazonechocontrol.discovery.IAmazonEchoDiscovery; +import org.openhab.binding.amazonechocontrol.internal.AccountConfiguration; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles the connection to the amazon server. + * + * @author Michael Geramb - Initial Contribution + */ +public class AccountHandler extends BaseBridgeHandler implements IAmazonEchoDiscovery { + + private final Logger logger = LoggerFactory.getLogger(AccountHandler.class); + private AccountConfiguration config; + private Connection connection; + private List childs = new ArrayList<>(); + private Object synchronizeConnection = new Object(); + private Map jsonSerialNumberDeviceMapping = new HashMap<>(); + private ScheduledFuture refreshJob; + private ScheduledFuture refreshLogin; + + public AccountHandler(@NonNull Bridge bridge) { + super(bridge); + AmazonEchoDiscovery.setHandlerExist(); + + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.trace("Command '{}' received for channel '{}'", command, channelUID); + + if (command instanceof RefreshType) { + refreshData(); + } + } + + @Override + public void initialize() { + start(); + } + + @Override + public void childHandlerInitialized(@NonNull ThingHandler childHandler, @NonNull Thing childThing) { + super.childHandlerInitialized(childHandler, childThing); + if (childHandler instanceof EchoHandler) { + EchoHandler echoHandler = (EchoHandler) childHandler; + synchronized (childs) { + childs.add(echoHandler); + + Connection temp = connection; + if (temp != null) { + initializeChild(echoHandler, temp); + } + } + } + } + + private void initializeChild(EchoHandler echoHandler, Connection temp) { + intializeChildDevice(temp, echoHandler); + + Device device = findDeviceJson(echoHandler); + BluetoothState state = null; + + JsonBluetoothStates states = null; + try { + states = temp.getBluetoothConnectionStates(); + } catch (Exception e) { + + logger.info(e.getMessage()); + } + if (states != null) { + state = states.findStateByDevice(device); + } + echoHandler.updateState(device, state); + } + + @Override + public void childHandlerDisposed(@NonNull ThingHandler childHandler, @NonNull Thing childThing) { + if (childHandler instanceof EchoHandler) { + synchronized (childs) { + childs.remove(childHandler); + } + + AmazonEchoDiscovery instance = AmazonEchoDiscovery.instance; + if (instance != null) { + instance.removeExisting(childThing.getUID()); + } + } + super.childHandlerDisposed(childHandler, childThing); + } + + @Override + public void handleConfigurationUpdate(@NonNull Map<@NonNull String, @NonNull Object> configurationParameters) { + super.handleConfigurationUpdate(configurationParameters); + start(); + } + + @Override + public void handleRemoval() { + + cleanup(); + super.handleRemoval(); + } + + @Override + public void dispose() { + AmazonEchoDiscovery.removeDiscoveryHandler(this); + cleanup(); + super.dispose(); + } + + private void cleanup() { + if (refreshJob != null) { + refreshJob.cancel(true); + refreshJob = null; + } + if (refreshLogin != null) { + refreshLogin.cancel(true); + refreshLogin = null; + } + if (connection != null) { + connection.logout(); + connection = null; + } + } + + private void start() { + logger.debug("amazon account bridge starting handler ..."); + + config = getConfigAs(AccountConfiguration.class); + if (config.amazonSite == null || config.amazonSite.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Amazon site not configured"); + cleanup(); + return; + } + if (config.email == null || config.email.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Account email not configured"); + cleanup(); + return; + } + if (config.password == null || config.password.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Account password not configured"); + cleanup(); + return; + } + if (config.pollingIntervalInSeconds == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Polling interval not configured"); + cleanup(); + return; + } + if (config.pollingIntervalInSeconds < 10) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Polling interval less than 10 seconds not allowed"); + cleanup(); + return; + } + synchronized (synchronizeConnection) { + if (connection == null || !connection.getEmail().equals(config.email) + || !connection.getPassword().equals(config.password) + || !connection.getAmazonSite().equals(config.amazonSite)) { + connection = new Connection(config.email, config.password, config.amazonSite); + } + } + refreshLogin = scheduler.scheduleWithFixedDelay(() -> { + checkLogin(); + }, 0, 60, TimeUnit.SECONDS); + + refreshJob = scheduler.scheduleWithFixedDelay(() -> { + refreshData(); + }, 4, config.pollingIntervalInSeconds, TimeUnit.SECONDS); + + logger.debug("amazon account bridge handler started."); + } + + private void checkLogin() { + + synchronized (synchronizeConnection) { + Connection temp = connection; + if (temp == null) { + return; + } + Date loginTime = temp.tryGetLoginTime(); + Date currentDate = new Date(); + long currentTime = currentDate.getTime(); + if (loginTime != null && currentTime - loginTime.getTime() > 3600000) // One hour + { + try { + if (!temp.verifyLogin()) { + temp.logout(); + } + } catch (Exception e) { + logger.info(e.getMessage()); + temp.logout(); + } + } + loginTime = temp.tryGetLoginTime(); + if (loginTime != null && currentTime - loginTime.getTime() > 86400000 * 5) // 5 days + { + // Recreate session + this.updateProperty("sessionStorage", ""); + temp = new Connection(temp.getEmail(), temp.getPassword(), temp.getAmazonSite()); + } + if (!temp.getIsLoggedIn()) { + try { + + // read session data from property + String sessionStore = this.thing.getProperties().get("sessionStorage"); + + // try use the session data + if (!temp.tryRestoreLogin(sessionStore)) { + // session data not valid -> login + int retry = 0; + while (true) { + try { + temp.makeLogin(); + break; + } catch (Exception e) { + // Up to 3 retries for login + retry++; + if (retry > 3) { + temp.logout(); + throw e; + } + // give amazon some time + Thread.sleep(2000); + } + } + // store session data in property + this.updateProperty("sessionStorage", temp.serializeLoginData()); + } + connection = temp; + updateStatus(ThingStatus.ONLINE); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); + } + // update the device list + updateDeviceList(); + AmazonEchoDiscovery.addDiscoveryHandler(this); + + } + } + } + + private void refreshData() { + synchronized (synchronizeConnection) { + logger.debug("amazon account bridge refreshing data ..."); + Connection temp = connection; + if (temp != null) { + if (!temp.getIsLoggedIn()) { + try { + temp.makeLogin(); + updateStatus(ThingStatus.ONLINE); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + e.getLocalizedMessage()); + } + } + } + synchronized (childs) { + updateDeviceList(); + JsonBluetoothStates states = null; + if (temp != null && temp.getIsLoggedIn()) { + try { + states = temp.getBluetoothConnectionStates(); + } catch (Exception e) { + logger.info(e.getMessage()); + } + if (states != null) { + + } + } + + for (EchoHandler child : childs) { + Device device = findDeviceJson(child); + BluetoothState state = null; + if (states != null) { + state = states.findStateByDevice(device); + } + child.updateState(device, state); + } + } + } + } + + public Device findDeviceJson(EchoHandler echoHandler) { + String serialNumber = echoHandler.findSerialNumber(); + Device result = null; + if (!serialNumber.isEmpty()) { + Map temp = jsonSerialNumberDeviceMapping; + if (temp != null) { + result = temp.get(serialNumber); + } + return result; + } + return result; + } + + @Override + synchronized public void updateDeviceList() { + Connection temp = connection; + if (temp == null) { + return; + } + + Device[] devices = null; + try { + devices = temp.getDeviceList(); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); + } + if (devices != null) { + Map newJsonSerialDeviceMapping = new HashMap<>(); + for (Device device : devices) { + newJsonSerialDeviceMapping.put(device.serialNumber, device); + } + jsonSerialNumberDeviceMapping = newJsonSerialDeviceMapping; + + AmazonEchoDiscovery discoveryService = AmazonEchoDiscovery.instance; + if (discoveryService != null) { + discoveryService.setDevices(getThing().getUID(), devices); + } + } + synchronized (childs) { + for (EchoHandler child : childs) { + initializeChild(child, temp); + } + } + } + + private void intializeChildDevice(Connection connection, EchoHandler child) { + Device deviceJson = this.findDeviceJson(child); + if (deviceJson != null) { + child.intialize(connection, deviceJson); + } + } + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java new file mode 100755 index 0000000000000..e9498f75d3026 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -0,0 +1,462 @@ +/** + * Copyright (c) 2014,2017 by the respective copyright holders. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.handler; + +import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType; +import org.eclipse.smarthome.core.library.types.NextPreviousType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.PlayPauseType; +import org.eclipse.smarthome.core.library.types.RewindFastforwardType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.HttpException; +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.JsonMediaState; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState.QueueEntry; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.InfoText; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.MainArt; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Provider; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.Volume; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EchoHandler} is responsible for the handling of the echo device + * + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class EchoHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(EchoHandler.class); + + private @Nullable Device device; + private @Nullable Connection connection; + private @Nullable ScheduledFuture updateStateJob; + private @Nullable String lastKnownRadioStationId; + private @Nullable String lastKnownBluetoothId; + private int lastKnownVolume = 25; + private @Nullable BluetoothState bluetoothState; + private boolean disableUpdate = false; + + public EchoHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + + logger.info("Amazon Echo Control Binding initialized"); + updateStatus(ThingStatus.ONLINE); + } + + public void intialize(Connection connection, @Nullable Device deviceJson) { + this.connection = connection; + this.device = deviceJson; + } + + @Override + public void dispose() { + ScheduledFuture updateStateJob = this.updateStateJob; + this.updateStateJob = null; + if (updateStateJob != null) { + updateStateJob.cancel(false); + } + super.dispose(); + } + + public String findSerialNumber() { + String id = (String) getConfig().get(DEVICE_PROPERTY_SERIAL_NUMBER); + if (id == null) { + return ""; + } + return id; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + try { + + int waitForUpdate = 1000; + boolean needBluetoothRefresh = false; + ScheduledFuture updateStateJob = this.updateStateJob; + this.updateStateJob = null; + if (updateStateJob != null) { + updateStateJob.cancel(false); + } + + Connection temp = connection; + if (temp == null) { + return; + } + Device device = this.device; + if (device == null) { + return; + } + + // Player commands + String channelId = channelUID.getId(); + if (channelId.equals(CHANNEL_PLAYER)) { + if (command == PlayPauseType.PAUSE || command == OnOffType.OFF) { + temp.command(device, "{\"type\":\"PauseCommand\"}"); + } else if (command == PlayPauseType.PLAY || command == OnOffType.ON) { + temp.command(device, "{\"type\":\"PlayCommand\"}"); + } else if (command == NextPreviousType.NEXT) { + temp.command(device, "{\"type\":\"NextCommand\"}"); + } else if (command == NextPreviousType.PREVIOUS) { + temp.command(device, "{\"type\":\"PreviousCommand\"}"); + } else if (command == RewindFastforwardType.FASTFORWARD) { + temp.command(device, "{\"type\":\"ForwardCommand\"}"); + } else if (command == RewindFastforwardType.REWIND) { + temp.command(device, "{\"type\":\"RewindCommand\"}"); + } + } + // Volume commands + if (channelId.equals(CHANNEL_VOLUME)) { + if (command instanceof PercentType) { + PercentType value = (PercentType) command; + int volume = value.intValue(); + temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume + "}"); + } else if (command == OnOffType.OFF) { + temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + 0 + "}"); + } else if (command == OnOffType.ON) { + temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + "}"); + } else if (command == IncreaseDecreaseType.INCREASE) { + if (lastKnownVolume < 100) { + lastKnownVolume++; + updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume)); + temp.command(device, + "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + "}"); + } + } else if (command == IncreaseDecreaseType.DECREASE) { + if (lastKnownVolume > 0) { + lastKnownVolume--; + updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume)); + temp.command(device, + "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + "}"); + } + } + } + // shuffle command + if (channelId.equals(CHANNEL_SHUFFLE)) { + if (command instanceof OnOffType) { + OnOffType value = (OnOffType) command; + + temp.command(device, "{\"type\":\"ShuffleCommand\",\"shuffle\":\"" + + (value == OnOffType.ON ? "true" : "false") + "\"}"); + } + } + + // bluetooth commands + if (channelId.equals(CHANNEL_BLUETOOTH_ID)) { + needBluetoothRefresh = true; + if (command instanceof StringType) { + String address = ((StringType) command).toFullString(); + if (!address.isEmpty()) { + waitForUpdate = 4000; + } + temp.bluetooth(device, address); + } + } + if (channelId.equals(CHANNEL_BLUETOOTH)) { + needBluetoothRefresh = true; + if (command == OnOffType.ON) { + waitForUpdate = 4000; + String bluetoothId = lastKnownBluetoothId; + BluetoothState state = bluetoothState; + if (state != null && (bluetoothId == null || bluetoothId.isEmpty())) { + if (state.pairedDeviceList != null) { + for (PairedDevice paired : state.pairedDeviceList) { + if (paired.address != null && !paired.address.isEmpty()) { + lastKnownBluetoothId = paired.address; + break; + } + } + } + } + if (lastKnownBluetoothId != null && !lastKnownBluetoothId.isEmpty()) { + temp.bluetooth(device, lastKnownBluetoothId); + } + } else if (command == OnOffType.OFF) { + temp.bluetooth(device, null); + } + } + if (channelId.equals(CHANNEL_BLUETOOTH_DEVICE_NAME)) { + needBluetoothRefresh = true; + } + // radio commands + if (channelId.equals(CHANNEL_RADIO_STATION_ID)) { + if (command instanceof StringType) { + + String stationId = ((StringType) command).toFullString(); + if (stationId != null && !stationId.isEmpty()) { + waitForUpdate = 3000; + } + temp.playRadio(device, stationId); + + } + } + if (channelId.equals(CHANNEL_RADIO)) { + + if (command == OnOffType.ON) { + if (lastKnownRadioStationId != null && !lastKnownRadioStationId.isEmpty()) { + waitForUpdate = 3000; + } + temp.playRadio(device, lastKnownRadioStationId); + } else if (command == OnOffType.OFF) { + temp.playRadio(device, ""); + } + + } + + // force update of the state + this.disableUpdate = true; + final boolean bluetoothRefresh = needBluetoothRefresh; + Runnable doRefresh = () -> { + BluetoothState state = null; + if (bluetoothRefresh) { + JsonBluetoothStates states; + try { + states = temp.getBluetoothConnectionStates(); + state = states.findStateByDevice(device); + } catch (Exception e) { + logger.info(e.getMessage()); + } + + } + this.disableUpdate = false; + updateState(device, state); + }; + if (command instanceof RefreshType) { + waitForUpdate = 0; + } + if (waitForUpdate == 0) { + doRefresh.run(); + } else { + this.updateStateJob = scheduler.schedule(doRefresh, waitForUpdate, TimeUnit.MILLISECONDS); + } + + } catch (Exception e) { + logger.info(e.getMessage()); + } + } + + public void updateState(@Nullable Device device, @Nullable BluetoothState bluetoothState) { + if (this.disableUpdate) { + return; + } + if (device == null) { + updateStatus(ThingStatus.UNKNOWN); + return; + } + this.device = device; + if (!this.device.online) { + updateStatus(ThingStatus.OFFLINE); + return; + } + updateStatus(ThingStatus.ONLINE); + Connection connection = this.connection; + if (connection == null) { + return; + } + + PlayerInfo playerInfo = null; + Provider provider = null; + InfoText infoText = null; + MainArt mainArt = null; + try { + JsonPlayerState playerState = connection.getPlayer(device); + playerInfo = playerState.playerInfo; + if (playerInfo != null) { + infoText = playerInfo.miniInfoText; + if (infoText == null) { + infoText = playerInfo.infoText; + } + mainArt = playerInfo.mainArt; + provider = playerInfo.provider; + } + } catch (HttpException e) { + if (e.getCode() == 400) { + // Ignore + } else { + logger.info(e.getMessage()); + } + } catch (Exception e) { + logger.info(e.getMessage()); + } + JsonMediaState mediaState = null; + try { + mediaState = connection.getMediaState(device); + + } catch (HttpException e) { + if (e.getCode() == 400) { + + updateState(CHANNEL_RADIO_STATION_ID, new StringType("")); + + } else { + logger.info(e.getMessage()); + } + } catch (Exception e) { + logger.info(e.getMessage()); + } + + // check playing + boolean playing = playerInfo != null && playerInfo.state != null && playerInfo.state.equals("PLAYING"); + + // handle bluetooth + String bluetoothId = ""; + String bluetoothDeviceName = ""; + boolean bluetoothIsConnected = false; + if (bluetoothState != null) { + this.bluetoothState = bluetoothState; + if (bluetoothState.pairedDeviceList != null) { + for (PairedDevice paired : bluetoothState.pairedDeviceList) { + if (paired.connected && paired.address != null) { + bluetoothIsConnected = true; + bluetoothId = paired.address; + bluetoothDeviceName = paired.friendlyName; + if (bluetoothDeviceName == null || bluetoothDeviceName.isEmpty()) { + bluetoothDeviceName = paired.address; + } + break; + } + } + } + + } + if (bluetoothId != null && !bluetoothId.isEmpty()) { + lastKnownBluetoothId = bluetoothId; + } + // handle radio + boolean isRadio = false; + if (mediaState != null && mediaState.radioStationId != null && !mediaState.radioStationId.isEmpty()) { + lastKnownRadioStationId = mediaState.radioStationId; + if (provider != null && provider.providerName.equalsIgnoreCase("TuneIn Live-Radio")) { + isRadio = true; + } + } + String radioStationId = ""; + + if (isRadio && mediaState != null && mediaState.radioStationId != null) { + radioStationId = mediaState.radioStationId; + } + // handle title, subtitle, imageUrl + String title = ""; + String subTitle1 = ""; + String subTitle2 = ""; + String imageUrl = ""; + if (infoText != null) { + if (infoText.title != null) { + title = infoText.title; + } + if (infoText.subText1 != null) { + subTitle1 = infoText.subText1; + } + + if (infoText.subText2 != null) { + subTitle2 = infoText.subText2; + } + } + if (mainArt != null) { + if (mainArt.url != null) { + imageUrl = mainArt.url; + } + } + if (mediaState != null) { + QueueEntry[] queueEntries = mediaState.queue; + if (queueEntries != null && queueEntries.length > 0) { + QueueEntry entry = queueEntries[0]; + if (entry != null) { + + if (isRadio) { + if (imageUrl.isEmpty() && entry.imageURL != null) { + imageUrl = entry.imageURL; + } + if (subTitle1.isEmpty() && entry.radioStationSlogan != null) { + subTitle1 = entry.radioStationSlogan; + } + if (subTitle2.isEmpty() && entry.radioStationLocation != null) { + subTitle2 = entry.radioStationLocation; + } + } + } + } + } + // handle provider + String providerDisplayName = ""; + if (provider != null) { + if (provider.providerDisplayName != null) { + providerDisplayName = provider.providerDisplayName; + } + if (provider.providerName != null) { + if (providerDisplayName.isEmpty()) { + providerDisplayName = provider.providerName; + } + } + } + // handle volume + Integer volume = null; + if (mediaState != null) { + volume = mediaState.volume; + } else if (playerInfo != null) { + + Volume volumnInfo = playerInfo.volume; + if (volumnInfo != null) { + volume = volumnInfo.volume; + } + } + + if (volume != null && volume > 0) { + lastKnownVolume = volume; + } + + // Update states + updateState(CHANNEL_RADIO_STATION_ID, new StringType(radioStationId)); + updateState(CHANNEL_RADIO, playing && isRadio ? OnOffType.ON : OnOffType.OFF); + updateState(CHANNEL_VOLUME, volume != null ? new PercentType(volume) : UnDefType.UNDEF); + updateState(CHANNEL_PROVIDER_DISPLAY_NAME, new StringType(providerDisplayName)); + updateState(CHANNEL_PLAYER, playing ? PlayPauseType.PLAY : PlayPauseType.PAUSE); + updateState(CHANNEL_IMAGE_URL, new StringType(imageUrl)); + updateState(CHANNEL_TITLE, new StringType(title)); + updateState(CHANNEL_SUBTITLE1, new StringType(subTitle1)); + updateState(CHANNEL_SUBTITLE2, new StringType(subTitle2)); + + if (bluetoothState != null) { + updateState(CHANNEL_BLUETOOTH, bluetoothIsConnected ? OnOffType.ON : OnOffType.OFF); + updateState(CHANNEL_BLUETOOTH_ID, new StringType(bluetoothId)); + updateState(CHANNEL_BLUETOOTH_DEVICE_NAME, new StringType(bluetoothDeviceName)); + } + + } + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java new file mode 100755 index 0000000000000..23017f08292ff --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2014,2017 by the respective copyright holders. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.amazonechocontrol.internal; + +/** + * Account Thing configuration + * + * @author Michael Geramb - Initial Contribution + */ +public class AccountConfiguration { + + public String email; + public String password; + public String amazonSite; + public Integer pollingIntervalInSeconds; + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java new file mode 100755 index 0000000000000..322bfc939a3f0 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2014,2017 by the respective copyright holders. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 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.AmazonEchoControlBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.openhab.binding.amazonechocontrol.handler.AccountHandler; +import org.openhab.binding.amazonechocontrol.handler.EchoHandler; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link AmazonEchoControlHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Michael Geramb - Initial contribution + */ +@Component(service = ThingHandlerFactory.class, immediate = true, configurationPid = "binding.amazonechocontrol") +@NonNullByDefault +public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { + AccountHandler bridgeHandler = new AccountHandler((Bridge) thing); + + return bridgeHandler; + } + if (thingTypeUID.equals(THING_TYPE_ECHO)) { + return new EchoHandler(thing); + } + + return null; + } + + @Override + protected void removeHandler(ThingHandler thingHandler) { + + super.removeHandler(thingHandler); + } + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java new file mode 100755 index 0000000000000..58548d3f21eef --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -0,0 +1,483 @@ +/** + * Copyright (c) 2014,2017 by the respective copyright holders. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 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.InputStream; +import java.io.OutputStream; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.ssl.HttpsURLConnection; + +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState; + +import com.google.gson.Gson; + +/** + * The {@link Connection} is responsible for the connection to the amazon server and + * handling of the commands + * + * @author Michael Geramb - Initial contribution + */ +public class Connection { + private java.net.CookieManager m_cookieManager = new java.net.CookieManager(); + private String m_email; + private String m_password; + private String m_amazonSite; + private String m_sessionId; + private Date m_loginTime; + private String m_apiServer = "layla.amazon.de"; + + public Connection(String email, String password, String amazonSite) { + m_email = email; + m_password = password; + m_amazonSite = amazonSite; + if (m_amazonSite.equalsIgnoreCase("amazon.com")) { + m_apiServer = "pitangui.amazon.com"; + } + } + + public String getEmail() { + return m_email; + } + + public String getPassword() { + return m_password; + } + + public String getAmazonSite() { + return m_amazonSite; + } + + public String serializeLoginData() { + if (m_sessionId == null || m_loginTime == null) { + return ""; + } + StringBuilder builder = new StringBuilder(); + builder.append("4\n"); // version + builder.append(m_email); + builder.append("\n"); + builder.append(m_password.hashCode()); + builder.append("\n"); + builder.append(m_sessionId); + builder.append("\n"); + builder.append(m_loginTime.getTime()); + builder.append("\n"); + List cookies = m_cookieManager.getCookieStore().getCookies(); + builder.append(cookies.size()); + builder.append("\n"); + for (HttpCookie cookie : cookies) { + + writeValue(builder, cookie.getName()); + writeValue(builder, cookie.getValue()); + writeValue(builder, cookie.getComment()); + writeValue(builder, cookie.getCommentURL()); + writeValue(builder, cookie.getDomain()); + writeValue(builder, cookie.getMaxAge()); + writeValue(builder, cookie.getPath()); + writeValue(builder, cookie.getPortlist()); + writeValue(builder, cookie.getVersion()); + writeValue(builder, cookie.getSecure()); + writeValue(builder, cookie.getDiscard()); + + } + return builder.toString(); + } + + private void writeValue(StringBuilder builder, Object value) { + if (value == null) { + builder.append('0'); + } else { + builder.append('1'); + builder.append("\n"); + builder.append(value.toString()); + } + builder.append("\n"); + } + + private String readValue(Scanner scanner) { + if (scanner.nextLine().equals("1")) { + return scanner.nextLine(); + } + return null; + } + + public Boolean tryRestoreLogin(String data) { + if (data == null || data.isEmpty()) { + return false; + } + + Scanner scanner = new Scanner(data); + String version = scanner.nextLine(); + if (!version.equals("4")) { + scanner.close(); + return false; + } + + String email = scanner.nextLine(); + if (!email.equals(this.m_email)) { + scanner.close(); + return false; + } + + int passwordHash = Integer.parseInt(scanner.nextLine()); + if (passwordHash != this.m_password.hashCode()) { + scanner.close(); + return false; + } + m_sessionId = scanner.nextLine(); + m_loginTime = new Date(Long.parseLong(scanner.nextLine())); + + CookieStore cookieStore = m_cookieManager.getCookieStore(); + + cookieStore.removeAll(); + + Integer numberOfCookies = Integer.parseInt(scanner.nextLine()); + for (Integer i = 0; i < numberOfCookies; i++) { + String name = readValue(scanner); + + String value = readValue(scanner); + + HttpCookie clientCookie = new HttpCookie(name, value); + + clientCookie.setComment(readValue(scanner)); + + clientCookie.setCommentURL(readValue(scanner)); + + clientCookie.setDomain(readValue(scanner)); + + clientCookie.setMaxAge(Long.parseLong(readValue(scanner))); + + clientCookie.setPath(readValue(scanner)); + + clientCookie.setPortlist(readValue(scanner)); + + clientCookie.setVersion(Integer.parseInt(readValue(scanner))); + + clientCookie.setSecure(Boolean.parseBoolean(readValue(scanner))); + + clientCookie.setDiscard(Boolean.parseBoolean(readValue(scanner))); + cookieStore.add(null, clientCookie); + } + + scanner.close(); + try { + if (verifyLogin()) { + return true; + + } + } catch (Exception e) { + } + cookieStore.removeAll(); + m_sessionId = null; + m_loginTime = null; + return false; + + } + + public Date tryGetLoginTime() { + return m_loginTime; + } + + private HttpsURLConnection makeRequest(String url, String referer, String postData, Boolean json) throws Exception { + String currentUrl = url; + for (int i = 0; i < 30; i++) { + HttpsURLConnection connection = (HttpsURLConnection) new URL(currentUrl).openConnection(); + connection.setInstanceFollowRedirects(true); + connection.setRequestProperty("Accept-Language", "en-US"); + connection.setRequestProperty("User-Agent", "Mozilla/5.0"); + connection.setRequestProperty("DNT", "1"); + connection.setRequestProperty("Upgrade-Insecure-Requests", "1"); + connection.setInstanceFollowRedirects(false); + if (referer != null) { + connection.setRequestProperty("Referer", referer); + } + + // add cookies + URI uri = connection.getURL().toURI(); + + StringBuilder cookieHeaderBuilder = new StringBuilder(); + for (HttpCookie cookie : m_cookieManager.getCookieStore().get(uri)) { + if (cookieHeaderBuilder.length() > 0) { + cookieHeaderBuilder.insert(0, "; "); + } + cookieHeaderBuilder.insert(0, cookie); + if (cookie.getName().equals("csrf")) { + connection.setRequestProperty("csrf", cookie.getValue()); + } + + } + if (cookieHeaderBuilder.length() > 0) { + + connection.setRequestProperty("Cookie", cookieHeaderBuilder.toString()); + } + + // make the request + if (postData != null) { + byte[] postDataBytes = postData.getBytes(StandardCharsets.UTF_8); + int postDataLength = postDataBytes.length; + connection.setFixedLengthStreamingMode(postDataLength); + if (json) { + connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + } else { + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + } + + connection.setRequestProperty("Content-Length", Integer.toString(postDataLength)); + + connection.setRequestMethod("POST"); + connection.setRequestProperty("Expect", "100-continue"); + + connection.setDoOutput(true); + OutputStream outputStream = connection.getOutputStream(); + outputStream.write(postDataBytes); + outputStream.close(); + } + + int code = connection.getResponseCode(); + String location = null; + Map> headerFields = connection.getHeaderFields(); + for (Map.Entry> header : headerFields.entrySet()) { + String key = header.getKey(); + if (key != null) { + if (key.equalsIgnoreCase("Set-Cookie")) { + + for (String cookieHeader : header.getValue()) { + + List cookies = HttpCookie.parse(cookieHeader); + for (HttpCookie cookie : cookies) { + m_cookieManager.getCookieStore().add(uri, cookie); + + } + } + } + if (key.equalsIgnoreCase("Location")) { + location = header.getValue().get(0); + } + } + } + if (code == 200) { + return connection; + } + if (code == 302 && location != null) { + currentUrl = location; + continue; + } + if (code != 200) { + throw new HttpException(code, connection.getResponseMessage()); + } + } + throw new Exception("To many redirects"); + + } + + public boolean getIsLoggedIn() { + return m_sessionId != null; + } + + public void makeLogin() throws Exception { + try { + // clear session data + m_cookieManager.getCookieStore().removeAll(); + m_sessionId = null; + m_loginTime = null; + + // get login form + String loginFormHtml = makeRequestAndReturnString("https://alexa." + m_amazonSite); + + // get session id from cookies + for (HttpCookie cookie : m_cookieManager.getCookieStore().getCookies()) { + if (cookie.getName().equalsIgnoreCase("session-id")) { + m_sessionId = cookie.getValue(); + break; + } + } + if (m_sessionId == null) { + throw new Exception("No session id received"); + } + + // read hidden form inputs, the will be used later in the url and for posting + Pattern inputPattern = Pattern + .compile("[^\"]+)\"\\s+value=\"(?[^\"]*)\""); + Matcher matcher = inputPattern.matcher(loginFormHtml); + + StringBuilder postDataBuilder = new StringBuilder(); + while (matcher.find()) { + + postDataBuilder.append(URLEncoder.encode(matcher.group("name"), "UTF-8")); + postDataBuilder.append('='); + postDataBuilder.append(URLEncoder.encode(matcher.group("value"), "UTF-8")); + postDataBuilder.append('&'); + } + + String queryParameters = postDataBuilder.toString() + "session-id=" + + URLEncoder.encode(m_sessionId, "UTF-8"); + + postDataBuilder.append("email"); + postDataBuilder.append('='); + postDataBuilder.append(URLEncoder.encode(m_email, "UTF-8")); + postDataBuilder.append('&'); + postDataBuilder.append("password"); + postDataBuilder.append('='); + postDataBuilder.append(URLEncoder.encode(m_password, "UTF-8")); + + String postData = postDataBuilder.toString(); + + // post login data + + String referer = "https://www." + m_amazonSite + "/ap/signin?" + queryParameters; + m_cookieManager.getCookieStore().add(new URL("https://www." + m_amazonSite).toURI(), + HttpCookie.parse("session-id=" + m_sessionId).get(0)); + String response = makeRequestAndReturnString("https://www." + m_amazonSite + "/ap/signin", referer, + postData, false); + if (response.contains("Amazon Alexa")) { + // login seems to be ok + } + + // get CSRF + makeRequest("https://" + m_apiServer + "/api/language", "https://alexa." + m_amazonSite + "/spa/index.html", + null, false); + + // verify login + if (!verifyLogin()) { + throw new Exception("Login fails."); + } + m_loginTime = new Date(); + } catch (Exception e) { + // clear session data + m_cookieManager.getCookieStore().removeAll(); + m_sessionId = null; + m_loginTime = null; + throw e; + } + + } + + private String makeRequestAndReturnString(String url) throws Exception { + return makeRequestAndReturnString(url, null, null, false); + } + + private String makeRequestAndReturnString(String url, String referer, String postData, Boolean json) + throws Exception { + HttpsURLConnection connection = makeRequest(url, referer, postData, json); + return getResponse(connection); + } + + public boolean verifyLogin() throws Exception { + String response = makeRequestAndReturnString("https://" + m_apiServer + "/api/bootstrap?version=0"); + Boolean result = response.contains("\"authenticated\":true"); + return result; + } + + private String getResponse(HttpsURLConnection request) throws Exception { + InputStream input = request.getInputStream(); + Scanner inputScanner = new Scanner(input); + Scanner scannerWithoutDelimiter = inputScanner.useDelimiter("\\A"); + String result = scannerWithoutDelimiter.hasNext() ? scannerWithoutDelimiter.next() : ""; + inputScanner.close(); + scannerWithoutDelimiter.close(); + input.close(); + return result; + } + + public void logout() { + m_cookieManager.getCookieStore().removeAll(); + m_sessionId = null; + m_loginTime = null; + } + + public Device[] getDeviceList() throws Exception { + String json = makeRequestAndReturnString("https://" + m_apiServer + "/api/devices-v2/device?cached=false"); + Gson gson = new Gson(); + JsonDevices devices = gson.fromJson(json, JsonDevices.class); + return devices.devices; + } + + public JsonPlayerState getPlayer(Device device) throws Exception { + String json = makeRequestAndReturnString("https://" + m_apiServer + "/api/np/player?deviceSerialNumber=" + + device.serialNumber + "&deviceType=" + device.deviceType + "&screenWidth=1440"); + Gson gson = new Gson(); + JsonPlayerState playerState = gson.fromJson(json, JsonPlayerState.class); + return playerState; + } + + public JsonMediaState getMediaState(Device device) throws Exception { + String json = makeRequestAndReturnString("https://" + m_apiServer + "/api/media/state?deviceSerialNumber=" + + device.serialNumber + "&deviceType=" + device.deviceType); + Gson gson = new Gson(); + JsonMediaState mediaState = gson.fromJson(json, JsonMediaState.class); + return mediaState; + } + + public JsonBluetoothStates getBluetoothConnectionStates() throws Exception { + String json = makeRequestAndReturnString("https://alexa." + m_amazonSite + "/api/bluetooth?cached=true"); + Gson gson = new Gson(); + JsonBluetoothStates bluetoothStates = gson.fromJson(json, JsonBluetoothStates.class); + return bluetoothStates; + } + + public void command(Device device, String command) throws Exception { + String url = "https://alexa." + m_amazonSite + "/api/np/command?deviceSerialNumber=" + device.serialNumber + + "&deviceType=" + device.deviceType; + makeRequest(url, null, command, true); + + } + + public void bluetooth(Device device, String address) throws Exception { + if (address == null || address.isEmpty()) { + // disconnect + makeRequest("https://" + m_apiServer + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" + + device.serialNumber, null, "", true); + } else { + makeRequest("https://" + m_apiServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + + device.serialNumber, null, "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true); + + } + } + + public void playRadio(Device device, String stationId) throws Exception { + if (stationId == null || stationId.isEmpty()) { + command(device, "{\"type\":\"PauseCommand\"}"); + } else { + makeRequest( + "https://" + m_apiServer + "/api/tunein/queue-and-play?deviceSerialNumber=" + device.serialNumber + + "&deviceType=" + device.deviceType + "&guideId=" + stationId + + "&contentType=station&callSign=&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId, + null, "", true); + } + } + + public void playPrimeSong(Device device, String trackId) throws Exception { + String command = "{\"trackId\":\"" + trackId + "\",\"playQueuePrime\":true}"; + makeRequest("https://alexa." + m_amazonSite + "/api/cloudplayer?deviceSerialNumber=" + device.serialNumber + + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId + + "&shuffle=false", null, command, true); + + } + +} \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java new file mode 100755 index 0000000000000..5f3d466803e13 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2014,2017 by the respective copyright holders. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.amazonechocontrol.internal; + +/** + * The {@link HttpException} is used for http error codes + * + * @author Michael Geramb - Initial contribution + */ +public class HttpException extends RuntimeException { + + private static final long serialVersionUID = 1L; + int code; + + public int getCode() { + return code; + } + + public HttpException(int code, String message) { + super(message); + this.code = code; + + } +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java new file mode 100644 index 0000000000000..ca10a104ab80f --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java @@ -0,0 +1,44 @@ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; + +public class JsonBluetoothStates { + + public BluetoothState findStateByDevice(Device device) { + if (device == null) { + return null; + } + if (bluetoothStates == null) { + return null; + } + for (BluetoothState state : bluetoothStates) { + if (state.deviceSerialNumber != null && state.deviceSerialNumber.equals(device.serialNumber)) { + return state; + } + } + return null; + } + + public BluetoothState[] bluetoothStates; + + public class PairedDevice { + public String address; + public boolean connected; + public String deviceClass; + public String friendlyName; + // "profiles":[ + // "AVRCP", + // "A2DP-SINK" + // ] + } + + public class BluetoothState { + public String deviceSerialNumber; + public String deviceType; + public String friendlyName; + public boolean gadgetPaired; + public boolean online; + public PairedDevice[] pairedDeviceList; + + } +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java new file mode 100755 index 0000000000000..183448af43d9d --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java @@ -0,0 +1,17 @@ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +public class JsonDevices { + + public class Device { + public String accountName; + public String serialNumber; + public String deviceOwnerCustomerId; + public String deviceAccountId; + public String deviceFamily; + public String deviceType; + public boolean online; + + } + + public Device[] devices; +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java new file mode 100755 index 0000000000000..47e2bf3f17371 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java @@ -0,0 +1,63 @@ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +public class JsonMediaState { + + public String clientId; + public String contentId; + public String contentType; + public String currentState; + public String imageURL; + public boolean isDisliked; + public boolean isLiked; + public boolean looping; + public String mediaOwnerCustomerId; + public boolean muted; + public String programId; + public int progressSeconds; + public String providerId; + public QueueEntry[] queue; + public String queueId; + public Integer queueSize; + public String radioStationId; + public int radioVariety; + public String referenceId; + public String service; + public boolean shuffling; + public int timeLastShuffled; + public int volume; + + public class QueueEntry { + + public String album; + public String albumAsin; + public String artist; + public String asin; + public String cardImageURL; + public String contentId; + public String contentType; + public int durationSeconds; + public boolean feedbackDisabled; + public String historicalId; + public String imageURL; + public int index; + public boolean isAd; + public boolean isDisliked; + public boolean isFreeWithPrime; + public boolean isLiked; + public String programId; + public String programName; + public String providerId; + public String queueId; + public String radioStationCallSign; + public String radioStationId; + public String radioStationLocation; + public String radioStationSlogan; + public String referenceId; + public String service; + public String startTime; + public String title; + public String trackId; + public String trackStatus; + + } +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java new file mode 100755 index 0000000000000..3c0d22b90ac3c --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java @@ -0,0 +1,46 @@ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.Nullable; + +public class JsonPlayerState { + public PlayerInfo playerInfo; + + public class PlayerInfo { + public String state; + public @Nullable InfoText infoText; + public @Nullable InfoText miniInfoText; + public @Nullable Provider provider; + public @Nullable Volume volume; + public @Nullable MainArt mainArt; + + public String queueId; + public String mediaId; + + public class InfoText { + public boolean multiLineMode; + public String subText1; + public String subText2; + public String title; + + } + + public class Provider { + public String providerDisplayName; + public String providerName; + } + + public class Volume { + public boolean muted; + public int volume; + } + + public class MainArt { + public String altText; + public String artType; + public String contentType; + public String url; + } + + } + +} diff --git a/addons/binding/pom.xml b/addons/binding/pom.xml index 93a46b50cc1f5..375b6eb1d7ba6 100644 --- a/addons/binding/pom.xml +++ b/addons/binding/pom.xml @@ -18,6 +18,7 @@ org.openhab.binding.airquality org.openhab.binding.allplay org.openhab.binding.amazondashbutton + org.openhab.binding.amazonechocontrol org.openhab.binding.atlona org.openhab.binding.autelis org.openhab.binding.avmfritz diff --git a/features/openhab-addons/src/main/feature/feature.xml b/features/openhab-addons/src/main/feature/feature.xml index 51e981caf0c43..0108fed6c64c8 100644 --- a/features/openhab-addons/src/main/feature/feature.xml +++ b/features/openhab-addons/src/main/feature/feature.xml @@ -28,6 +28,11 @@ mvn:org.openhab.binding/org.openhab.binding.amazondashbutton/${project.version} + + openhab-runtime-base + mvn:org.openhab.binding/org.openhab.binding.amazonechocontrol/${project.version} + + openhab-runtime-base mvn:org.openhab.binding/org.openhab.binding.atlona/${project.version} From c73afb921d4972320231e3043b586f6d993e5cd9 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Tue, 9 Jan 2018 18:46:50 +0100 Subject: [PATCH 02/56] [amazonechocontrol] bugfixes, support of amzon.co.uk accounts Signed-off-by: Michael Geramb (github: mgeramb) --- .../README.md | 2 +- .../discovery/AmazonEchoDiscovery.java | 4 +- .../handler/AccountHandler.java | 35 ++++----- .../internal/Connection.java | 71 ++++++++++++------- 4 files changed, 70 insertions(+), 42 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 05d8cba3bec55..cff1f2e43d643 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -20,7 +20,7 @@ Some ideas what you can do in your home by using rules and other openHAB control This binding uses the same API as the Web-Browser-Based Alexa site (alexa.amazon.de). In other words, it simulates a user which is using the web page. Unfortunately, the binding can get broken if Amazon change the website. -I have currently only tested the binding with amazon.de If you have an other account, please let me know if it works. Otherwise you will get instruction how you can help me to make it working. +The binding is tested with amazon.de and amazon.co.uk accounts, but should also work with all others. ## Warning ## diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/AmazonEchoDiscovery.java index 288a28e2fbac7..7b080878f5c3a 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/AmazonEchoDiscovery.java @@ -47,7 +47,9 @@ public class AmazonEchoDiscovery extends AbstractDiscoveryService { public static void addDiscoveryHandler(IAmazonEchoDiscovery discoveryService) { synchronized (discoveryServices) { - discoveryServices.add(discoveryService); + if (!discoveryServices.contains(discoveryService)) { + discoveryServices.add(discoveryService); + } } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index 07b4740e36d75..ad82d2284ec32 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -96,7 +96,9 @@ private void initializeChild(EchoHandler echoHandler, Connection temp) { JsonBluetoothStates states = null; try { - states = temp.getBluetoothConnectionStates(); + if (temp.getIsLoggedIn()) { + states = temp.getBluetoothConnectionStates(); + } } catch (Exception e) { logger.info(e.getMessage()); @@ -194,6 +196,7 @@ private void start() { connection = new Connection(config.email, config.password, config.amazonSite); } } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login"); refreshLogin = scheduler.scheduleWithFixedDelay(() -> { checkLogin(); }, 0, 60, TimeUnit.SECONDS); @@ -233,6 +236,7 @@ private void checkLogin() { this.updateProperty("sessionStorage", ""); temp = new Connection(temp.getEmail(), temp.getPassword(), temp.getAmazonSite()); } + boolean loginIsValid = true; if (!temp.getIsLoggedIn()) { try { @@ -262,16 +266,20 @@ private void checkLogin() { this.updateProperty("sessionStorage", temp.serializeLoginData()); } connection = temp; - updateStatus(ThingStatus.ONLINE); } catch (Exception e) { + loginIsValid = false; updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } - // update the device list - updateDeviceList(); - AmazonEchoDiscovery.addDiscoveryHandler(this); + if (loginIsValid) { + // update the device list + updateDeviceList(); + updateStatus(ThingStatus.ONLINE); + AmazonEchoDiscovery.addDiscoveryHandler(this); + } } } + } private void refreshData() { @@ -280,17 +288,13 @@ private void refreshData() { Connection temp = connection; if (temp != null) { if (!temp.getIsLoggedIn()) { - try { - temp.makeLogin(); - updateStatus(ThingStatus.ONLINE); - } catch (Exception e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - e.getLocalizedMessage()); - } + return; } } synchronized (childs) { updateDeviceList(); + updateStatus(ThingStatus.ONLINE); + JsonBluetoothStates states = null; if (temp != null && temp.getIsLoggedIn()) { try { @@ -298,9 +302,6 @@ private void refreshData() { } catch (Exception e) { logger.info(e.getMessage()); } - if (states != null) { - - } } for (EchoHandler child : childs) { @@ -337,7 +338,9 @@ synchronized public void updateDeviceList() { Device[] devices = null; try { - devices = temp.getDeviceList(); + if (temp.getIsLoggedIn()) { + devices = temp.getDeviceList(); + } } catch (Exception e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 58548d3f21eef..e40db29655cb9 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -35,6 +35,8 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.gson.Gson; @@ -45,21 +47,35 @@ * @author Michael Geramb - Initial contribution */ public class Connection { + private final Logger logger = LoggerFactory.getLogger(Connection.class); + private java.net.CookieManager m_cookieManager = new java.net.CookieManager(); private String m_email; private String m_password; private String m_amazonSite; private String m_sessionId; private Date m_loginTime; - private String m_apiServer = "layla.amazon.de"; + private String m_alexaServer; public Connection(String email, String password, String amazonSite) { m_email = email; m_password = password; + m_amazonSite = amazonSite; - if (m_amazonSite.equalsIgnoreCase("amazon.com")) { - m_apiServer = "pitangui.amazon.com"; + if (m_amazonSite.toLowerCase().startsWith("http://")) { + m_amazonSite = m_amazonSite.substring(7); + } + if (m_amazonSite.toLowerCase().startsWith("https://")) { + m_amazonSite = m_amazonSite.substring(8); + } + if (m_amazonSite.toLowerCase().startsWith("www.")) { + m_amazonSite = m_amazonSite.substring(4); + } + if (m_amazonSite.toLowerCase().startsWith("alexa.")) { + m_amazonSite = m_amazonSite.substring(6); } + m_alexaServer = "https://alexa." + amazonSite; + } public String getEmail() { @@ -151,7 +167,7 @@ public Boolean tryRestoreLogin(String data) { return false; } m_sessionId = scanner.nextLine(); - m_loginTime = new Date(Long.parseLong(scanner.nextLine())); + Date loginTime = new Date(Long.parseLong(scanner.nextLine())); CookieStore cookieStore = m_cookieManager.getCookieStore(); @@ -188,6 +204,7 @@ public Boolean tryRestoreLogin(String data) { scanner.close(); try { if (verifyLogin()) { + m_loginTime = loginTime; return true; } @@ -297,7 +314,7 @@ private HttpsURLConnection makeRequest(String url, String referer, String postDa } public boolean getIsLoggedIn() { - return m_sessionId != null; + return m_loginTime != null; } public void makeLogin() throws Exception { @@ -308,7 +325,7 @@ public void makeLogin() throws Exception { m_loginTime = null; // get login form - String loginFormHtml = makeRequestAndReturnString("https://alexa." + m_amazonSite); + String loginFormHtml = makeRequestAndReturnString(m_alexaServer); // get session id from cookies for (HttpCookie cookie : m_cookieManager.getCookieStore().getCookies()) { @@ -338,6 +355,9 @@ public void makeLogin() throws Exception { String queryParameters = postDataBuilder.toString() + "session-id=" + URLEncoder.encode(m_sessionId, "UTF-8"); + logger.debug("Login query String:"); + logger.debug(queryParameters); + postDataBuilder.append("email"); postDataBuilder.append('='); postDataBuilder.append(URLEncoder.encode(m_email, "UTF-8")); @@ -356,12 +376,13 @@ public void makeLogin() throws Exception { String response = makeRequestAndReturnString("https://www." + m_amazonSite + "/ap/signin", referer, postData, false); if (response.contains("Amazon Alexa")) { - // login seems to be ok + logger.debug("Response seems to be alexa app"); + } else { + logger.debug("Response maybe not valid"); } // get CSRF - makeRequest("https://" + m_apiServer + "/api/language", "https://alexa." + m_amazonSite + "/spa/index.html", - null, false); + makeRequest(m_alexaServer + "/api/language", m_alexaServer + "/spa/index.html", null, false); // verify login if (!verifyLogin()) { @@ -373,6 +394,7 @@ public void makeLogin() throws Exception { m_cookieManager.getCookieStore().removeAll(); m_sessionId = null; m_loginTime = null; + logger.debug("Login failed: " + e.getMessage()); throw e; } @@ -389,7 +411,7 @@ private String makeRequestAndReturnString(String url, String referer, String pos } public boolean verifyLogin() throws Exception { - String response = makeRequestAndReturnString("https://" + m_apiServer + "/api/bootstrap?version=0"); + String response = makeRequestAndReturnString(m_alexaServer + "/api/bootstrap?version=0"); Boolean result = response.contains("\"authenticated\":true"); return result; } @@ -412,14 +434,14 @@ public void logout() { } public Device[] getDeviceList() throws Exception { - String json = makeRequestAndReturnString("https://" + m_apiServer + "/api/devices-v2/device?cached=false"); + String json = makeRequestAndReturnString(m_alexaServer + "/api/devices-v2/device?cached=false"); Gson gson = new Gson(); JsonDevices devices = gson.fromJson(json, JsonDevices.class); return devices.devices; } public JsonPlayerState getPlayer(Device device) throws Exception { - String json = makeRequestAndReturnString("https://" + m_apiServer + "/api/np/player?deviceSerialNumber=" + String json = makeRequestAndReturnString(m_alexaServer + "/api/np/player?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&screenWidth=1440"); Gson gson = new Gson(); JsonPlayerState playerState = gson.fromJson(json, JsonPlayerState.class); @@ -427,7 +449,7 @@ public JsonPlayerState getPlayer(Device device) throws Exception { } public JsonMediaState getMediaState(Device device) throws Exception { - String json = makeRequestAndReturnString("https://" + m_apiServer + "/api/media/state?deviceSerialNumber=" + String json = makeRequestAndReturnString(m_alexaServer + "/api/media/state?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType); Gson gson = new Gson(); JsonMediaState mediaState = gson.fromJson(json, JsonMediaState.class); @@ -435,15 +457,15 @@ public JsonMediaState getMediaState(Device device) throws Exception { } public JsonBluetoothStates getBluetoothConnectionStates() throws Exception { - String json = makeRequestAndReturnString("https://alexa." + m_amazonSite + "/api/bluetooth?cached=true"); + String json = makeRequestAndReturnString(m_alexaServer + "/api/bluetooth?cached=true"); Gson gson = new Gson(); JsonBluetoothStates bluetoothStates = gson.fromJson(json, JsonBluetoothStates.class); return bluetoothStates; } public void command(Device device, String command) throws Exception { - String url = "https://alexa." + m_amazonSite + "/api/np/command?deviceSerialNumber=" + device.serialNumber - + "&deviceType=" + device.deviceType; + String url = m_alexaServer + "/api/np/command?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + + device.deviceType; makeRequest(url, null, command, true); } @@ -451,11 +473,12 @@ public void command(Device device, String command) throws Exception { public void bluetooth(Device device, String address) throws Exception { if (address == null || address.isEmpty()) { // disconnect - makeRequest("https://" + m_apiServer + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" - + device.serialNumber, null, "", true); + makeRequest( + m_alexaServer + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" + device.serialNumber, + null, "", true); } else { - makeRequest("https://" + m_apiServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" - + device.serialNumber, null, "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true); + makeRequest(m_alexaServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber, + null, "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true); } } @@ -465,7 +488,7 @@ public void playRadio(Device device, String stationId) throws Exception { command(device, "{\"type\":\"PauseCommand\"}"); } else { makeRequest( - "https://" + m_apiServer + "/api/tunein/queue-and-play?deviceSerialNumber=" + device.serialNumber + m_alexaServer + "/api/tunein/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&guideId=" + stationId + "&contentType=station&callSign=&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId, null, "", true); @@ -474,9 +497,9 @@ public void playRadio(Device device, String stationId) throws Exception { public void playPrimeSong(Device device, String trackId) throws Exception { String command = "{\"trackId\":\"" + trackId + "\",\"playQueuePrime\":true}"; - makeRequest("https://alexa." + m_amazonSite + "/api/cloudplayer?deviceSerialNumber=" + device.serialNumber - + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId - + "&shuffle=false", null, command, true); + makeRequest(m_alexaServer + "/api/cloudplayer?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId + "&shuffle=false", null, + command, true); } From 3812b093ba9c6fae3e4f1a8eec9c7ec33dca041b Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 10 Jan 2018 18:36:38 +0100 Subject: [PATCH 03/56] [amazonechocontrol] package dependencies fixed, readme changed Signed-off-by: Michael Geramb (github: mgeramb) --- .../META-INF/MANIFEST.MF | 20 ++++++++++--------- .../README.md | 6 +++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF index 63c48be192824..e8ab2a7cbc756 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF +++ b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF @@ -3,18 +3,17 @@ Bundle-ActivationPolicy: lazy Bundle-ClassPath: . Bundle-ManifestVersion: 2 Bundle-Name: AmazonEchoControl Binding -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-SymbolicName: org.openhab.binding.amazonechocontrol;singleton:=true +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-Vendor: openHAB Bundle-Version: 2.3.0.qualifier -Export-Package: - org.openhab.binding.amazonechocontrol, - org.openhab.binding.amazonechocontrol.handler -Import-Package: - org.apache.commons.lang.time;version="2.6.0", +Import-Package: com.google.gson;resolution:=optional, + com.google.gson.annotations, org.eclipse.jdt.annotation;resolution:=optional, org.eclipse.smarthome.config.core, org.eclipse.smarthome.config.discovery, + org.eclipse.smarthome.core.cache, + org.eclipse.smarthome.core.i18n, org.eclipse.smarthome.core.library.types, org.eclipse.smarthome.core.thing, org.eclipse.smarthome.core.thing.binding, @@ -23,8 +22,11 @@ Import-Package: org.eclipse.smarthome.core.types, org.openhab.binding.amazonechocontrol, org.openhab.binding.amazonechocontrol.handler, - org.openhab.core.library.types, - org.osgi.framework;version="1.8.0", + org.osgi.framework, + org.osgi.service.component.annotations;resolution:=optional, org.slf4j Service-Component: OSGI-INF/*.xml -Require-Bundle: com.google.gson +Export-Package: + org.openhab.binding.amazonechocontrol, + org.openhab.binding.amazonechocontrol.handler +Bundle-ActivationPolicy: lazy diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index cff1f2e43d643..62c862dbdb120 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -1,12 +1,16 @@ # Amazon Echo Control Binding This binding let control openHAB Amazon Echo devices (Alexa). -It provide features to control and view the current state of: + +The idea for writing this binding come from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German) Thank you Alex! + +The binding provide features to control and view the current state of echo dot devices: - volume - pause/continue/next track/previous track - connect/disconnect bluetooth devices - start playing radio +- show album art image in sitemap Some ideas what you can do in your home by using rules and other openHAB controlled devices: From 92a04d73ca7b134b54a752a7048acaa22d2edd0c Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 10 Jan 2018 18:56:30 +0100 Subject: [PATCH 04/56] [amazonechocontrol] fix duplicate line in manifest Signed-off-by: Michael Geramb (github: mgeramb) --- .../org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF index e8ab2a7cbc756..bd7ac2446c8da 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF +++ b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF @@ -29,4 +29,4 @@ Service-Component: OSGI-INF/*.xml Export-Package: org.openhab.binding.amazonechocontrol, org.openhab.binding.amazonechocontrol.handler -Bundle-ActivationPolicy: lazy + From 7cc41baaa1a2fad01bea0a6f503976ab4e991b61 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 10 Jan 2018 19:44:27 +0100 Subject: [PATCH 05/56] [amazonechocontrol] german translations and thing-types cleanup Signed-off-by: Michael Geramb (github: mgeramb) --- .../i18n/amazonechocontrol_de.properties | 75 +++++++++++++++++++ .../i18n/amazonechocontrol_xx_XX.properties | 13 ---- .../ESH-INF/thing/thing-types.xml | 16 ++-- 3 files changed, 80 insertions(+), 24 deletions(-) create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties delete mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_xx_XX.properties diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties new file mode 100755 index 0000000000000..884aeb2145c7e --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties @@ -0,0 +1,75 @@ +# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE +# FIXME: please do not add the file to the repo if you add or change no content +# binding +binding.amazonechocontrol.name = Amazon Echo Steuerung Binding +binding.amazonechocontrol.description = Binding zum Steuern von Amazon Echo (Alexa). Diese Binding ermöglicht openHAB die Lautstärke, den Wiedergabe Status und die Bluetooth-Verbinding deines Amazon Echo Gerätes zu steuern. + +# thing types +thing-type.amazonechocontrol.account.label = Amazon Konto +thing-type.amazonechocontrol.account.description = Amazon Konto bei dem dein Amazon Echo registriert ist. + +thing-type.config.amazonechocontrol.account.amazonSite.label = Amazon Seite +thing-type.config.amazonechocontrol.account.amazonSite.description = Wähle die Seite bei der dein Amazon Konto erstellt wurde. + +thing-type.config.amazonechocontrol.account.email.label = Amazon Konto E-Mail +thing-type.config.amazonechocontrol.account.email.description = E-Mail des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde eingeben. + +thing-type.config.amazonechocontrol.account.password.label = Amazon Konto Kennwort +thing-type.config.amazonechocontrol.account.password.description = Kennwort des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde eingeben. + +thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.label = Status-Aktualisierungs-Intervall +thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.description = Aktualtisierungs-Intervall für den Status in Sekunden. Kleinere Zeiten verursachen höheren Netzwerkverkehr. + + +thing-type.amazonechocontrol.echo.label = Amazon Echo +thing-type.amazonechocontrol.echo.description = Amazon Echo Gerät (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) + +thing-type.config.amazonechocontrol.echo.serialNumber.label = Seriennummer +thing-type.config.amazonechocontrol.echo.serialNumber.description = Die Seriennummer findest du in der Alexa App. + + +# channel types +channel-type.amazonechocontrol.bluetoothDeviceName.label = Bluetooth Gerät +channel-type.amazonechocontrol.bluetoothDeviceName.description = Verbundenes Bluetoothgerät + +channel-type.amazonechocontrol.radioStationId.label = Radio Stations ID +channel-type.amazonechocontrol.radioStationId.description = ID der Radio Station + +channel-type.amazonechocontrol.providerDisplayName.label = Anbieter Name +channel-type.amazonechocontrol.providerDisplayName.description = Name des Musikanbieters + +channel-type.amazonechocontrol.bluetoothId.label = Bluetooth Verbinding +channel-type.amazonechocontrol.bluetoothId.description = MAC-Adresse des verbundenen Bluetoothgerätes + +channel-type.amazonechocontrol.imageUrl.label = Bild URL +channel-type.amazonechocontrol.imageUrl.description = URL des Album Covers oder des Radiostations Logos + +channel-type.amazonechocontrol.title.label = Titel +channel-type.amazonechocontrol.title.description = Titel + +channel-type.amazonechocontrol.subtitle1.label = Untertitel 1 +channel-type.amazonechocontrol.subtitle1.description = Untertitel 1 + +channel-type.amazonechocontrol.subtitle2.label = Untertitel 2 +channel-type.amazonechocontrol.subtitle2.description = Untertitel 2 + +channel-type.amazonechocontrol.radio.label = Radio +channel-type.amazonechocontrol.radio.description = Radio eingestalten + +channel-type.amazonechocontrol.bluetooth.label = Bluetooth Verbinding +channel-type.amazonechocontrol.bluetooth.description = Verbindet zum letzten benutzten Bluetooth Gerätes + +channel-type.amazonechocontrol.loop.label = Wiederholung +channel-type.amazonechocontrol.loop.description = Wiederholung + +channel-type.amazonechocontrol.shuffle.label = Zufall +channel-type.amazonechocontrol.shuffle.description = Zufallswiedergabe + +channel-type.amazonechocontrol.player.label = Musikwiedergabe +channel-type.amazonechocontrol.player.description = Musikwiedergabe + +channel-type.amazonechocontrol.volume.label = Lautstärke +channel-type.amazonechocontrol.volume.description = Wiedergabelautstärke + + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_xx_XX.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_xx_XX.properties deleted file mode 100755 index d8f73548cf33d..0000000000000 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_xx_XX.properties +++ /dev/null @@ -1,13 +0,0 @@ -# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE -# FIXME: please do not add the file to the repo if you add or change no content -# binding -binding.amazonechocontrol.name = -binding.amazonechocontrol.description = - -# thing types -thing-type.amazonechocontrol.sample.label = -thing-type.amazonechocontrol.sample.description = - -# channel types -channel-type.amazonechocontrol.sample-channel.label = -channel-type.amazonechocontrol.sample-channel.description = \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 5fd5a2746641e..849516849ad00 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -29,7 +29,7 @@ - Enter the email address of the amazon account which is used for the amazon echo devices. Note: 2 factor authentication is currently not supported. + Enter the email address of the amazon account which is used for the amazon echo devices. @@ -103,13 +103,7 @@ Id of the radio station - - String - - Name of Alexa - - - + String @@ -121,7 +115,7 @@ String - Id of the bluethooth connected device + MAC-Address of the bluethooth connected device @@ -155,7 +149,7 @@ Switch - Alexa plays radio + Radio turned on @@ -179,7 +173,7 @@ Player - Content Playing + Music Player From a14859ccbfdcfa6f9f07e769beef43f7c860abc7 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 10 Jan 2018 21:27:46 +0100 Subject: [PATCH 06/56] [amazonechocontrol] improve logging, correct copyrights, create specialized exception for connection, correct comments Signed-off-by: Michael Geramb (github: mgeramb) --- .../ESH-INF/thing/thing-types.xml | 1 - .../README.md | 4 +-- .../AmazonEchoControlBindingConstants.java | 2 +- .../discovery/IAmazonEchoDiscovery.java | 5 ---- .../handler/AccountHandler.java | 12 ++++----- .../handler/EchoHandler.java | 12 ++++----- .../internal/AccountConfiguration.java | 2 +- .../AmazonEchoControlHandlerFactory.java | 2 +- .../internal/Connection.java | 13 +++++---- .../internal/ConnectionException.java | 27 +++++++++++++++++++ .../internal/HttpException.java | 3 +-- .../discovery/AmazonEchoDiscovery.java | 4 +-- .../discovery/IAmazonEchoDiscovery.java | 18 +++++++++++++ .../internal/jsons/JsonBluetoothStates.java | 18 +++++++++++++ .../internal/jsons/JsonDevices.java | 18 +++++++++++++ .../internal/jsons/JsonMediaState.java | 18 +++++++++++++ .../internal/jsons/JsonPlayerState.java | 18 +++++++++++++ 17 files changed, 143 insertions(+), 34 deletions(-) delete mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/IAmazonEchoDiscovery.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java rename addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/{ => internal}/discovery/AmazonEchoDiscovery.java (97%) create mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 849516849ad00..d3d948246c55f 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -13,7 +13,6 @@ - false diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 62c862dbdb120..cc6575751c2e9 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -91,12 +91,13 @@ You will find the serial number in the alexa app. ### amzonechocontrol.things -```php +``` Bridge amazonechocontrol:account:account1 [amazonSite="amazon.de", email="myaccountemail@myprovider.com", password="secure", pollingIntervalInSeconds=60] { Thing echo echo1 "Alexa" @ "Living Room" [serialNumber="SERIAL_NUMBER"] } ``` + You will find the serial number in the Alexa app. ### amzonechocontrol.items: @@ -117,7 +118,6 @@ Switch Echo_Living_Room_Bluetooth "Bluetooth" String Echo_Living_Room_BluetoothDeviceName "Bluetooth Device" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothDeviceName"} String Echo_Living_Room_RadioStationId "Radio Station Id" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:radioStationId"} Switch Echo_Living_Room_Radio "Radio" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:radio"} - ``` ### amzonechocontrol.sitemap: diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java index c3a1936f87c16..8f31989e78a20 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2014,2017 by the respective copyright holders. + * Copyright (c) 2014-2018 by the respective copyright holders. * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/IAmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/IAmazonEchoDiscovery.java deleted file mode 100755 index f040999f68bb8..0000000000000 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/IAmazonEchoDiscovery.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.openhab.binding.amazonechocontrol.discovery; - -public interface IAmazonEchoDiscovery { - void updateDeviceList(); -} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index ad82d2284ec32..ba5a72098d94a 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2017 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -26,10 +26,10 @@ import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; -import org.openhab.binding.amazonechocontrol.discovery.AmazonEchoDiscovery; -import org.openhab.binding.amazonechocontrol.discovery.IAmazonEchoDiscovery; import org.openhab.binding.amazonechocontrol.internal.AccountConfiguration; import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; +import org.openhab.binding.amazonechocontrol.internal.discovery.IAmazonEchoDiscovery; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; @@ -101,7 +101,7 @@ private void initializeChild(EchoHandler echoHandler, Connection temp) { } } catch (Exception e) { - logger.info(e.getMessage()); + logger.info("getBluetoothConnectionStates failed: {}", e); } if (states != null) { state = states.findStateByDevice(device); @@ -225,7 +225,7 @@ private void checkLogin() { temp.logout(); } } catch (Exception e) { - logger.info(e.getMessage()); + logger.info("logout failed: {}", e.getMessage()); temp.logout(); } } @@ -300,7 +300,7 @@ private void refreshData() { try { states = temp.getBluetoothConnectionStates(); } catch (Exception e) { - logger.info(e.getMessage()); + logger.info("getBluetoothConnectionStates failed: {}", e); } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index e9498f75d3026..159dd6bc81893 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2014,2017 by the respective copyright holders. + * Copyright (c) 2014-2018 by the respective copyright holders. * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -250,7 +250,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { states = temp.getBluetoothConnectionStates(); state = states.findStateByDevice(device); } catch (Exception e) { - logger.info(e.getMessage()); + logger.info("getBluetoothConnectionStates failes: {}", e); } } @@ -267,7 +267,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } catch (Exception e) { - logger.info(e.getMessage()); + logger.info("handleCommand failes: {}", e); } } @@ -309,7 +309,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto if (e.getCode() == 400) { // Ignore } else { - logger.info(e.getMessage()); + logger.info("getPlayer failes: {}", e); } } catch (Exception e) { logger.info(e.getMessage()); @@ -324,10 +324,10 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto updateState(CHANNEL_RADIO_STATION_ID, new StringType("")); } else { - logger.info(e.getMessage()); + logger.info("getMediaState failes: {}", e); } } catch (Exception e) { - logger.info(e.getMessage()); + logger.info("getMediaState failes: {}", e); } // check playing diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java index 23017f08292ff..94e99cb79223e 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2014,2017 by the respective copyright holders. + * Copyright (c) 2014-2018 by the respective copyright holders. * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 322bfc939a3f0..9e1ff966c1511 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2014,2017 by the respective copyright holders. + * Copyright (c) 2014-2018 by the respective copyright holders. * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index e40db29655cb9..019a0af82e1b4 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2014,2017 by the respective copyright holders. + * Copyright (c) 2014-2018 by the respective copyright holders. * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -309,7 +309,7 @@ private HttpsURLConnection makeRequest(String url, String referer, String postDa throw new HttpException(code, connection.getResponseMessage()); } } - throw new Exception("To many redirects"); + throw new ConnectionException("To many redirects"); } @@ -335,7 +335,7 @@ public void makeLogin() throws Exception { } } if (m_sessionId == null) { - throw new Exception("No session id received"); + throw new ConnectionException("No session id received"); } // read hidden form inputs, the will be used later in the url and for posting @@ -355,8 +355,7 @@ public void makeLogin() throws Exception { String queryParameters = postDataBuilder.toString() + "session-id=" + URLEncoder.encode(m_sessionId, "UTF-8"); - logger.debug("Login query String:"); - logger.debug(queryParameters); + logger.debug("Login query String: {}", queryParameters); postDataBuilder.append("email"); postDataBuilder.append('='); @@ -386,7 +385,7 @@ public void makeLogin() throws Exception { // verify login if (!verifyLogin()) { - throw new Exception("Login fails."); + throw new ConnectionException("Login fails."); } m_loginTime = new Date(); } catch (Exception e) { @@ -394,7 +393,7 @@ public void makeLogin() throws Exception { m_cookieManager.getCookieStore().removeAll(); m_sessionId = null; m_loginTime = null; - logger.debug("Login failed: " + e.getMessage()); + logger.debug("Login failed:{} ", e); throw e; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java new file mode 100644 index 0000000000000..7e3a6cbcdafd6 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2014-2018 by the respective copyright holders. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.amazonechocontrol.internal; + +/** + * The {@link ConnectionException} is used for errors in the connection to the amazon server + * + * @author Michael Geramb - Initial contribution + */ +public class ConnectionException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ConnectionException(String message) { + super(message); + } +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java index 5f3d466803e13..21b1d77a7feb6 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2014,2017 by the respective copyright holders. + * Copyright (c) 2014-2018 by the respective copyright holders. * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,6 @@ * * SPDX-License-Identifier: EPL-2.0 */ - package org.openhab.binding.amazonechocontrol.internal; /** diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java similarity index 97% rename from addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/AmazonEchoDiscovery.java rename to addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 7b080878f5c3a..cae93aa4f09a7 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -1,12 +1,12 @@ /** - * Copyright (c) 2010-2017 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.amazonechocontrol.discovery; +package org.openhab.binding.amazonechocontrol.internal.discovery; import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java new file mode 100755 index 0000000000000..2f36b6d9fea51 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.discovery; + +/** + * The {@link AmazonEcIAmazonEchoDiscoveryhoDiscovery} is responsible connection between account and discovery service + * + * @author Michael Geramb - Initial contribution + */ +public interface IAmazonEchoDiscovery { + void updateDeviceList(); +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java index ca10a104ab80f..07cfa1aea5868 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java @@ -1,7 +1,25 @@ +/** + * Copyright (c) 2014-2018 by the respective copyright holders. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + package org.openhab.binding.amazonechocontrol.internal.jsons; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; +/** + * The {@link JsonBluetoothStates} encapsulate the GSON data of bluetooth state + * + * @author Michael Geramb - Initial contribution + */ public class JsonBluetoothStates { public BluetoothState findStateByDevice(Device device) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java index 183448af43d9d..e19331874d028 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java @@ -1,5 +1,23 @@ +/** + * Copyright (c) 2014-2018 by the respective copyright holders. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + package org.openhab.binding.amazonechocontrol.internal.jsons; +/** + * The {@link JsonDevices} encapsulate the GSON data of device list + * + * @author Michael Geramb - Initial contribution + */ public class JsonDevices { public class Device { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java index 47e2bf3f17371..530f1e1a58d3c 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java @@ -1,5 +1,23 @@ +/** + * Copyright (c) 2014-2018 by the respective copyright holders. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + package org.openhab.binding.amazonechocontrol.internal.jsons; +/** + * The {@link JsonMediaState} encapsulate the GSON data of the current media state + * + * @author Michael Geramb - Initial contribution + */ public class JsonMediaState { public String clientId; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java index 3c0d22b90ac3c..29e1c51365593 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java @@ -1,7 +1,25 @@ +/** + * Copyright (c) 2014-2018 by the respective copyright holders. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + package org.openhab.binding.amazonechocontrol.internal.jsons; import org.eclipse.jdt.annotation.Nullable; +/** + * The {@link JsonPlayerState} encapsulate the GSON data of the player state + * + * @author Michael Geramb - Initial contribution + */ public class JsonPlayerState { public PlayerInfo playerInfo; From 6e1941e09f8f287380d40e1483eec2af8d2264c9 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 10 Jan 2018 21:48:21 +0100 Subject: [PATCH 07/56] [amazonechocontrol] fix spelling, fix wrong copyright header Signed-off-by: Michael Geramb (github: mgeramb) --- .../amazonechocontrol/handler/AccountHandler.java | 14 +++++++++----- .../amazonechocontrol/handler/EchoHandler.java | 12 ++++++------ .../amazonechocontrol/internal/Connection.java | 1 - .../internal/discovery/AmazonEchoDiscovery.java | 14 +++++++++----- .../internal/discovery/IAmazonEchoDiscovery.java | 14 +++++++++----- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index ba5a72098d94a..07258f3a9409c 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -1,10 +1,14 @@ /** - * Copyright (c) 2010-2018 by the respective copyright holders. + * Copyright (c) 2014-2018 by the respective copyright holders. * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 */ package org.openhab.binding.amazonechocontrol.handler; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 159dd6bc81893..684ca01b5cb7c 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -250,7 +250,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { states = temp.getBluetoothConnectionStates(); state = states.findStateByDevice(device); } catch (Exception e) { - logger.info("getBluetoothConnectionStates failes: {}", e); + logger.info("getBluetoothConnectionStates fails: {}", e); } } @@ -267,7 +267,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } catch (Exception e) { - logger.info("handleCommand failes: {}", e); + logger.info("handleCommand fails: {}", e); } } @@ -309,10 +309,10 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto if (e.getCode() == 400) { // Ignore } else { - logger.info("getPlayer failes: {}", e); + logger.info("getPlayer fails: {}", e); } } catch (Exception e) { - logger.info(e.getMessage()); + logger.info("getPlayer fails: {}", e); } JsonMediaState mediaState = null; try { @@ -324,10 +324,10 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto updateState(CHANNEL_RADIO_STATION_ID, new StringType("")); } else { - logger.info("getMediaState failes: {}", e); + logger.info("getMediaState fails: {}", e); } } catch (Exception e) { - logger.info("getMediaState failes: {}", e); + logger.info("getMediaState fails: {}", e); } // check playing diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 019a0af82e1b4..1d750dc6953c1 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -10,7 +10,6 @@ * * SPDX-License-Identifier: EPL-2.0 */ - package org.openhab.binding.amazonechocontrol.internal; import java.io.InputStream; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index cae93aa4f09a7..151f0c7f9fee9 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -1,10 +1,14 @@ /** - * Copyright (c) 2010-2018 by the respective copyright holders. + * Copyright (c) 2014-2018 by the respective copyright holders. * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 */ package org.openhab.binding.amazonechocontrol.internal.discovery; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java index 2f36b6d9fea51..7c9987a647bc7 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java @@ -1,10 +1,14 @@ /** - * Copyright (c) 2010-2018 by the respective copyright holders. + * Copyright (c) 2014-2018 by the respective copyright holders. * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 */ package org.openhab.binding.amazonechocontrol.internal.discovery; From dffe04c08cf6ab6142c8863bd0997e62edb1f4a7 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 10 Jan 2018 22:09:46 +0100 Subject: [PATCH 08/56] [amazonechocontrol] fix null annotation, fix wrong licence in copyright Signed-off-by: Michael Geramb (github: mgeramb) --- .../AmazonEchoControlBindingConstants.java | 14 ++++------ .../handler/AccountHandler.java | 28 ++++++++++--------- .../handler/EchoHandler.java | 16 ++++------- .../internal/AccountConfiguration.java | 14 ++++------ .../AmazonEchoControlHandlerFactory.java | 14 ++++------ .../internal/Connection.java | 15 ++++------ .../internal/ConnectionException.java | 14 ++++------ .../internal/HttpException.java | 14 ++++------ .../discovery/AmazonEchoDiscovery.java | 14 ++++------ .../discovery/IAmazonEchoDiscovery.java | 14 ++++------ .../internal/jsons/JsonBluetoothStates.java | 15 ++++------ .../internal/jsons/JsonDevices.java | 15 ++++------ .../internal/jsons/JsonMediaState.java | 14 ++++------ .../internal/jsons/JsonPlayerState.java | 14 ++++------ 14 files changed, 82 insertions(+), 133 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java index 8f31989e78a20..c125289809d79 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java @@ -1,14 +1,10 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.amazonechocontrol; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index 07258f3a9409c..94f4d16a7ea09 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -1,14 +1,10 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.amazonechocontrol.handler; @@ -92,7 +88,7 @@ public void childHandlerInitialized(@NonNull ThingHandler childHandler, @NonNull } } - private void initializeChild(EchoHandler echoHandler, Connection temp) { + private void initializeChild(@NonNull EchoHandler echoHandler, @NonNull Connection temp) { intializeChildDevice(temp, echoHandler); Device device = findDeviceJson(echoHandler); @@ -267,7 +263,11 @@ private void checkLogin() { } } // store session data in property - this.updateProperty("sessionStorage", temp.serializeLoginData()); + String serializedStorage = temp.serializeLoginData(); + if (serializedStorage == null) { + serializedStorage = ""; + } + this.updateProperty("sessionStorage", serializedStorage); } connection = temp; } catch (Exception e) { @@ -362,12 +362,14 @@ synchronized public void updateDeviceList() { } synchronized (childs) { for (EchoHandler child : childs) { - initializeChild(child, temp); + if (child != null) { + initializeChild(child, temp); + } } } } - private void intializeChildDevice(Connection connection, EchoHandler child) { + private void intializeChildDevice(@NonNull Connection connection, @NonNull EchoHandler child) { Device deviceJson = this.findDeviceJson(child); if (deviceJson != null) { child.intialize(connection, deviceJson); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 684ca01b5cb7c..9d600b063b2b1 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -1,14 +1,10 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.amazonechocontrol.handler; @@ -280,7 +276,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto return; } this.device = device; - if (!this.device.online) { + if (!device.online) { updateStatus(ThingStatus.OFFLINE); return; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java index 94e99cb79223e..a7e2869c7f6ef 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java @@ -1,14 +1,10 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.amazonechocontrol.internal; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 9e1ff966c1511..4499a43fb8481 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -1,14 +1,10 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.amazonechocontrol.internal; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 1d750dc6953c1..74f70cf2585de 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -1,15 +1,12 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ + package org.openhab.binding.amazonechocontrol.internal; import java.io.InputStream; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java index 7e3a6cbcdafd6..2d609d4d74fcf 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/ConnectionException.java @@ -1,14 +1,10 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.amazonechocontrol.internal; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java index 21b1d77a7feb6..6063fcf893681 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java @@ -1,14 +1,10 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.amazonechocontrol.internal; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 151f0c7f9fee9..cae93aa4f09a7 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -1,14 +1,10 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.amazonechocontrol.internal.discovery; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java index 7c9987a647bc7..2f36b6d9fea51 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java @@ -1,14 +1,10 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.amazonechocontrol.internal.discovery; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java index 07cfa1aea5868..120ac1dfb656a 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java @@ -1,16 +1,11 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ - package org.openhab.binding.amazonechocontrol.internal.jsons; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java index e19331874d028..c7144e80ec214 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java @@ -1,16 +1,11 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ - package org.openhab.binding.amazonechocontrol.internal.jsons; /** diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java index 530f1e1a58d3c..e856f528a208f 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java @@ -1,14 +1,10 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.amazonechocontrol.internal.jsons; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java index 29e1c51365593..3f4a605053da7 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java @@ -1,14 +1,10 @@ /** - * Copyright (c) 2014-2018 by the respective copyright holders. + * Copyright (c) 2010-2018 by the respective copyright holders. * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.amazonechocontrol.internal.jsons; From adec353cda7df710220371636e5743509ed11b32 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 14 Jan 2018 13:54:36 +0100 Subject: [PATCH 09/56] [amazonechocontrol] fix problems with redirects to insecure sites and code cleanup Signed-off-by: Michael Geramb (github: mgeramb) --- .../README.md | 2 +- .../handler/EchoHandler.java | 1 - .../internal/Connection.java | 174 ++++++++++-------- 3 files changed, 103 insertions(+), 74 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index cc6575751c2e9..dbdd7d5df4b66 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -2,7 +2,7 @@ This binding let control openHAB Amazon Echo devices (Alexa). -The idea for writing this binding come from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German) Thank you Alex! +The idea for writing this binding came from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German) Thank you Alex! The binding provide features to control and view the current state of echo dot devices: diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 9d600b063b2b1..f10be25613827 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -71,7 +71,6 @@ public EchoHandler(Thing thing) { @Override public void initialize() { - logger.info("Amazon Echo Control Binding initialized"); updateStatus(ThingStatus.ONLINE); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 74f70cf2585de..413ae4ca420a6 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -219,94 +219,116 @@ public Date tryGetLoginTime() { private HttpsURLConnection makeRequest(String url, String referer, String postData, Boolean json) throws Exception { String currentUrl = url; - for (int i = 0; i < 30; i++) { - HttpsURLConnection connection = (HttpsURLConnection) new URL(currentUrl).openConnection(); - connection.setInstanceFollowRedirects(true); - connection.setRequestProperty("Accept-Language", "en-US"); - connection.setRequestProperty("User-Agent", "Mozilla/5.0"); - connection.setRequestProperty("DNT", "1"); - connection.setRequestProperty("Upgrade-Insecure-Requests", "1"); - connection.setInstanceFollowRedirects(false); - if (referer != null) { - connection.setRequestProperty("Referer", referer); - } - - // add cookies - URI uri = connection.getURL().toURI(); - - StringBuilder cookieHeaderBuilder = new StringBuilder(); - for (HttpCookie cookie : m_cookieManager.getCookieStore().get(uri)) { - if (cookieHeaderBuilder.length() > 0) { - cookieHeaderBuilder.insert(0, "; "); - } - cookieHeaderBuilder.insert(0, cookie); - if (cookie.getName().equals("csrf")) { - connection.setRequestProperty("csrf", cookie.getValue()); + for (int i = 0; i < 30; i++) // loop for handling redirect, using automatic redirect is not possible, because + // all response headers must be catched + { + int code; + HttpsURLConnection connection; + try { + + logger.debug("Make request to {}", url); + connection = (HttpsURLConnection) new URL(currentUrl).openConnection(); + connection.setRequestProperty("Accept-Language", "en-US"); + connection.setRequestProperty("User-Agent", "Mozilla/5.0"); + connection.setRequestProperty("DNT", "1"); + connection.setRequestProperty("Upgrade-Insecure-Requests", "1"); + connection.setInstanceFollowRedirects(false); + if (referer != null) { + connection.setRequestProperty("Referer", referer); } - } - if (cookieHeaderBuilder.length() > 0) { + // add cookies + URI uri = connection.getURL().toURI(); - connection.setRequestProperty("Cookie", cookieHeaderBuilder.toString()); - } + StringBuilder cookieHeaderBuilder = new StringBuilder(); + for (HttpCookie cookie : m_cookieManager.getCookieStore().get(uri)) { + if (cookieHeaderBuilder.length() > 0) { + cookieHeaderBuilder.insert(0, "; "); + } + cookieHeaderBuilder.insert(0, cookie); + if (cookie.getName().equals("csrf")) { + connection.setRequestProperty("csrf", cookie.getValue()); + } - // make the request - if (postData != null) { - byte[] postDataBytes = postData.getBytes(StandardCharsets.UTF_8); - int postDataLength = postDataBytes.length; - connection.setFixedLengthStreamingMode(postDataLength); - if (json) { - connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); - } else { - connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + } + if (cookieHeaderBuilder.length() > 0) { + connection.setRequestProperty("Cookie", cookieHeaderBuilder.toString()); } - connection.setRequestProperty("Content-Length", Integer.toString(postDataLength)); - - connection.setRequestMethod("POST"); - connection.setRequestProperty("Expect", "100-continue"); - - connection.setDoOutput(true); - OutputStream outputStream = connection.getOutputStream(); - outputStream.write(postDataBytes); - outputStream.close(); - } - - int code = connection.getResponseCode(); - String location = null; - Map> headerFields = connection.getHeaderFields(); - for (Map.Entry> header : headerFields.entrySet()) { - String key = header.getKey(); - if (key != null) { - if (key.equalsIgnoreCase("Set-Cookie")) { + if (postData != null) { - for (String cookieHeader : header.getValue()) { + // post data + byte[] postDataBytes = postData.getBytes(StandardCharsets.UTF_8); + int postDataLength = postDataBytes.length; - List cookies = HttpCookie.parse(cookieHeader); - for (HttpCookie cookie : cookies) { - m_cookieManager.getCookieStore().add(uri, cookie); + connection.setFixedLengthStreamingMode(postDataLength); + if (json) { + connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + } else { + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + } + connection.setRequestProperty("Content-Length", Integer.toString(postDataLength)); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Expect", "100-continue"); + + connection.setDoOutput(true); + OutputStream outputStream = connection.getOutputStream(); + outputStream.write(postDataBytes); + outputStream.close(); + } + // handle result + code = connection.getResponseCode(); + String location = null; + + // handle response headers + Map> headerFields = connection.getHeaderFields(); + for (Map.Entry> header : headerFields.entrySet()) { + String key = header.getKey(); + if (key != null) { + if (key.equalsIgnoreCase("Set-Cookie")) { + // store cookie + for (String cookieHeader : header.getValue()) { + List cookies = HttpCookie.parse(cookieHeader); + for (HttpCookie cookie : cookies) { + m_cookieManager.getCookieStore().add(uri, cookie); + } + } + } + if (key.equalsIgnoreCase("Location")) { + // get redirect location + location = header.getValue().get(0); + if (location != null) { + if (code == 302) { + logger.debug("Redirected to {}", location); + } + // check for https + if (location.toLowerCase().startsWith("http://")) { + // always use https + location = "https://" + location.substring(7); + logger.debug("Redirect corrected to {}", location); + } } } - } - if (key.equalsIgnoreCase("Location")) { - location = header.getValue().get(0); } } - } - if (code == 200) { - return connection; - } - if (code == 302 && location != null) { - currentUrl = location; - continue; + if (code == 200) { + logger.debug("Call to {} succeeded", url); + return connection; + } + if (code == 302 && location != null) { + currentUrl = location; + continue; + } + } catch (Exception e) { + logger.warn("Request to url '" + currentUrl + "' fails with unkown error: " + e.getMessage()); + throw e; } if (code != 200) { throw new HttpException(code, connection.getResponseMessage()); } } throw new ConnectionException("To many redirects"); - } public boolean getIsLoggedIn() { @@ -320,9 +342,12 @@ public void makeLogin() throws Exception { m_sessionId = null; m_loginTime = null; + logger.debug("Start Login to {}", m_alexaServer); // get login form String loginFormHtml = makeRequestAndReturnString(m_alexaServer); + logger.debug("Received login form {}", loginFormHtml); + // get session id from cookies for (HttpCookie cookie : m_cookieManager.getCookieStore().getCookies()) { if (cookie.getName().equalsIgnoreCase("session-id")) { @@ -373,23 +398,26 @@ public void makeLogin() throws Exception { if (response.contains("Amazon Alexa")) { logger.debug("Response seems to be alexa app"); } else { - logger.debug("Response maybe not valid"); + logger.info("Response maybe not valid"); } + logger.debug("Received content after login {}", response); + // get CSRF - makeRequest(m_alexaServer + "/api/language", m_alexaServer + "/spa/index.html", null, false); + // makeRequest(m_alexaServer + "/api/language", m_alexaServer + "/spa/index.html", null, false); // verify login if (!verifyLogin()) { throw new ConnectionException("Login fails."); } m_loginTime = new Date(); + logger.debug("Login succeeded"); } catch (Exception e) { // clear session data m_cookieManager.getCookieStore().removeAll(); m_sessionId = null; m_loginTime = null; - logger.debug("Login failed:{} ", e); + logger.info("Login failed:{} ", e); throw e; } @@ -428,6 +456,8 @@ public void logout() { m_loginTime = null; } + // commands and states + public Device[] getDeviceList() throws Exception { String json = makeRequestAndReturnString(m_alexaServer + "/api/devices-v2/device?cached=false"); Gson gson = new Gson(); From 7f5cfa102da4a8ef4335a2d747f4c9814907d77c Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 14 Jan 2018 14:51:43 +0100 Subject: [PATCH 10/56] [amazonechocontrol] code cleanup Signed-off-by: Michael Geramb (github: mgeramb) --- .../handler/EchoHandler.java | 2 + .../internal/Connection.java | 2 +- .../discovery/AmazonEchoDiscovery.java | 49 +++++++++---------- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index f10be25613827..08eb12d023a11 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -105,6 +105,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { int waitForUpdate = 1000; boolean needBluetoothRefresh = false; ScheduledFuture updateStateJob = this.updateStateJob; + String lastKnownBluetoothId = this.lastKnownBluetoothId; + this.updateStateJob = null; if (updateStateJob != null) { updateStateJob.cancel(false); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 413ae4ca420a6..903330cdbaa33 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -321,7 +321,7 @@ private HttpsURLConnection makeRequest(String url, String referer, String postDa continue; } } catch (Exception e) { - logger.warn("Request to url '" + currentUrl + "' fails with unkown error: " + e.getMessage()); + logger.warn("Request to url '{}' fails with unkown error: {}", url, e); throw e; } if (code != 200) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index cae93aa4f09a7..46f4bfa0802ef 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -28,7 +28,6 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Modified; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,9 +40,11 @@ @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.amazonechocontrol") public class AmazonEchoDiscovery extends AbstractDiscoveryService { - private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); public static AmazonEchoDiscovery instance; - private static List discoveryServices = new ArrayList<>(); + private final @NonNull static List discoveryServices = new ArrayList<>(); + + private final @NonNull Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); + private final @NonNull Map lastDeviceInformations = new HashMap<>(); public static void addDiscoveryHandler(IAmazonEchoDiscovery discoveryService) { synchronized (discoveryServices) { @@ -138,42 +139,36 @@ protected void stopBackgroundDiscovery() { @Activate public void activate(Map config) { super.activate(config); - modified(config); + if (config != null) { + modified(config); + } }; - @Override - @Modified - protected void modified(Map config) { - super.modified(config); - - } - - Map lastDeviceInformations = new HashMap<>(); - public synchronized void setDevices(ThingUID brigdeThingUID, Device[] deviceInformations) { Set toRemove = new HashSet(lastDeviceInformations.keySet()); for (Device deviceInformation : deviceInformations) { String serialNumber = deviceInformation.serialNumber; + if (serialNumber != null) { + boolean alreadyfound = toRemove.remove(serialNumber); + // new + if (!alreadyfound && deviceInformation.deviceFamily != null + && deviceInformation.deviceFamily.equals("ECHO")) { - boolean alreadyfound = toRemove.remove(serialNumber); - // new - if (!alreadyfound && deviceInformation.deviceFamily != null - && deviceInformation.deviceFamily.equals("ECHO")) { + ThingUID thingUID = new ThingUID(THING_TYPE_ECHO, brigdeThingUID, serialNumber); - ThingUID thingUID = new ThingUID(THING_TYPE_ECHO, brigdeThingUID, serialNumber); + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) + .withLabel(deviceInformation.accountName) + .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) + .withRepresentationProperty(DEVICE_PROPERTY_SERIAL_NUMBER).withBridge(brigdeThingUID) + .build(); - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) - .withLabel(deviceInformation.accountName) - .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) - .withRepresentationProperty(DEVICE_PROPERTY_SERIAL_NUMBER).withBridge(brigdeThingUID).build(); + logger.debug("Device [{}, {}] found.", serialNumber, deviceInformation.accountName); - logger.debug("Device [{}, {}] found.", serialNumber, deviceInformation.accountName); - - thingDiscovered(result); - lastDeviceInformations.put(serialNumber, thingUID); + thingDiscovered(result); + lastDeviceInformations.put(serialNumber, thingUID); + } } - } } From 5e5b9136d5c0b52975746f6a920201c3ab21a008 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Thu, 18 Jan 2018 20:48:01 +0100 Subject: [PATCH 11/56] [amazonechocontrol] bugfix configuration update, better error handling for lost internet connection Signed-off-by: Michael Geramb (github: mgeramb) --- .../handler/AccountHandler.java | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index 94f4d16a7ea09..0a16892b3b134 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -8,6 +8,7 @@ */ package org.openhab.binding.amazonechocontrol.handler; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -28,6 +29,7 @@ import org.eclipse.smarthome.core.types.RefreshType; import org.openhab.binding.amazonechocontrol.internal.AccountConfiguration; import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.ConnectionException; import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; import org.openhab.binding.amazonechocontrol.internal.discovery.IAmazonEchoDiscovery; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; @@ -124,12 +126,6 @@ public void childHandlerDisposed(@NonNull ThingHandler childHandler, @NonNull Th super.childHandlerDisposed(childHandler, childThing); } - @Override - public void handleConfigurationUpdate(@NonNull Map<@NonNull String, @NonNull Object> configurationParameters) { - super.handleConfigurationUpdate(configurationParameters); - start(); - } - @Override public void handleRemoval() { @@ -251,10 +247,10 @@ private void checkLogin() { try { temp.makeLogin(); break; - } catch (Exception e) { + } catch (ConnectionException e) { // Up to 3 retries for login retry++; - if (retry > 3) { + if (retry >= 3) { temp.logout(); throw e; } @@ -270,6 +266,10 @@ private void checkLogin() { this.updateProperty("sessionStorage", serializedStorage); } connection = temp; + } catch (UnknownHostException e) { + loginIsValid = false; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown host name '" + e.getMessage() + "'. Maybe your internet connection is offline"); } catch (Exception e) { loginIsValid = false; updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); @@ -283,24 +283,29 @@ private void checkLogin() { } } - } private void refreshData() { - synchronized (synchronizeConnection) { - logger.debug("amazon account bridge refreshing data ..."); - Connection temp = connection; - if (temp != null) { - if (!temp.getIsLoggedIn()) { - return; + logger.debug("amazon account bridge refreshing data ..."); + try { + Connection temp = null; + synchronized (synchronizeConnection) { + temp = connection; + if (temp != null) { + if (!temp.getIsLoggedIn()) { + return; + } } } + if (temp == null) { + return; + } synchronized (childs) { + updateDeviceList(); - updateStatus(ThingStatus.ONLINE); JsonBluetoothStates states = null; - if (temp != null && temp.getIsLoggedIn()) { + if (temp.getIsLoggedIn()) { try { states = temp.getBluetoothConnectionStates(); } catch (Exception e) { @@ -317,6 +322,11 @@ private void refreshData() { child.updateState(device, state); } } + updateStatus(ThingStatus.ONLINE); + + } catch (Exception e) { + logger.warn("Update states of amazon account failed: {}", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } } From 3d2dfe22a26b0fc2ecac88594acd0897f1027cfe Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sat, 27 Jan 2018 13:08:14 +0100 Subject: [PATCH 12/56] [amazonechocontrol] new channels, new thing types, dynamic state provider Signed-off-by: Michael Geramb (github: mgeramb) --- .../i18n/amazonechocontrol_de.properties | 50 +++- .../ESH-INF/thing/thing-types.xml | 231 ++++++++++++++++-- .../README.md | 146 +++++++---- .../AmazonEchoControlBindingConstants.java | 28 ++- .../handler/EchoHandler.java | 152 ++++++++++-- .../AmazonEchoControlHandlerFactory.java | 11 +- .../internal/Connection.java | 138 +++++++++-- .../discovery/AmazonEchoDiscovery.java | 49 ++-- .../internal/jsons/JsonDevices.java | 1 + .../internal/jsons/JsonMediaState.java | 3 +- .../jsons/JsonNotificationRequest.java | 34 +++ .../internal/jsons/JsonNotificationSound.java | 22 ++ .../jsons/JsonNotificationSounds.java | 18 ++ .../internal/jsons/JsonPlaylists.java | 28 +++ ...onEchoDynamicStateDescriptionProvider.java | 168 +++++++++++++ 15 files changed, 938 insertions(+), 141 deletions(-) create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSounds.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaylists.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties index 884aeb2145c7e..86fa4e638fa9c 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties @@ -6,10 +6,10 @@ binding.amazonechocontrol.description = Binding zum Steuern von Amazon Echo (Ale # thing types thing-type.amazonechocontrol.account.label = Amazon Konto -thing-type.amazonechocontrol.account.description = Amazon Konto bei dem dein Amazon Echo registriert ist. +thing-type.amazonechocontrol.account.description = Amazon Konto bei dem dein Amazon Echo registriert ist. Hinweis: 2 Faktor Authentifizierung ist nicht unterstützt. thing-type.config.amazonechocontrol.account.amazonSite.label = Amazon Seite -thing-type.config.amazonechocontrol.account.amazonSite.description = Wähle die Seite bei der dein Amazon Konto erstellt wurde. +thing-type.config.amazonechocontrol.account.amazonSite.description = Wähle die Seite bei der dein Amazon Konto erstellt wurde. Hinweis: 2 Faktor Authentifizierung ist nicht unterstützt. thing-type.config.amazonechocontrol.account.email.label = Amazon Konto E-Mail thing-type.config.amazonechocontrol.account.email.description = E-Mail des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde eingeben. @@ -27,20 +27,62 @@ thing-type.amazonechocontrol.echo.description = Amazon Echo Ger thing-type.config.amazonechocontrol.echo.serialNumber.label = Seriennummer thing-type.config.amazonechocontrol.echo.serialNumber.description = Die Seriennummer findest du in der Alexa App. +thing-type.amazonechocontrol.echospot.label = Amazon Echo Spot +thing-type.amazonechocontrol.echospot.description = Amazon Echo Spot Gerät + +thing-type.amazonechocontrol.echoshow.label = Amazon Echo Show +thing-type.amazonechocontrol.echoshow.description = Amazon Echo Show Gerät + +thing-type.config.amazonechocontrol.echospot.serialNumber.label = Seriennummer +thing-type.config.amazonechocontrol.echospot.serialNumber.description = Die Seriennummer findest du in der Alexa App. + +thing-type.amazonechocontrol.wha.label = Amazon Multi-Raum Musik +thing-type.amazonechocontrol.wha.description = Multi-Raum Musik Steuerung + +thing-type.config.amazonechocontrol.wha.serialNumber.label = Seriennummer +thing-type.config.amazonechocontrol.wha.serialNumber.description = Die Seriennummer findest du in der Alexa App. + +thing-type.amazonechocontrol.unknown.label = Unbekanntes Echo Gerät oder unbekannte App +thing-type.amazonechocontrol.unknown.description = Unbekanntes Echo Gerät. Warnung: Möglicherweise werden nicht alle Kanäle vom Gerät unterstützt + +thing-type.config.amazonechocontrol.unknown.serialNumber.label = Seriennummer +thing-type.config.amazonechocontrol.unknown.serialNumber.description = Die Seriennummer findest du in der Alexa App. + # channel types channel-type.amazonechocontrol.bluetoothDeviceName.label = Bluetooth Gerät channel-type.amazonechocontrol.bluetoothDeviceName.description = Verbundenes Bluetoothgerät -channel-type.amazonechocontrol.radioStationId.label = Radio Stations ID +channel-type.amazonechocontrol.radioStationId.label = TuneIn Radio Stations ID channel-type.amazonechocontrol.radioStationId.description = ID der Radio Station +channel-type.amazonechocontrol.amazonMusicTrackId.label = Amazon Music Lied ID +channel-type.amazonechocontrol.amazonMusicTrackId.description = ID des Liedes auf Amazon Music + +channel-type.amazonechocontrol.amazonMusic.label = Amazon Music +channel-type.amazonechocontrol.amazonMusic.description = Amazon Music eingeschalten + +channel-type.amazonechocontrol.amazonMusicPlayListId.label = Amazon Music Playlist ID (Nur schreiben) +channel-type.amazonechocontrol.amazonMusicPlayListId.description = ID der Playlist auf Amazon Music (Nur schreiben, kein aktueller Status). Auswahl funktioniert derzeit nur in PaperUI. + +channel-type.amazonechocontrol.amazonMusicPlayListIdLastUsed.label = Amazon Music letzte gestartete Playlist ID +channel-type.amazonechocontrol.amazonMusicPlayListIdLastUsed.description = Zuletzt über openHAB gestartete Amazon Music Playlist + channel-type.amazonechocontrol.providerDisplayName.label = Anbieter Name channel-type.amazonechocontrol.providerDisplayName.description = Name des Musikanbieters channel-type.amazonechocontrol.bluetoothId.label = Bluetooth Verbinding channel-type.amazonechocontrol.bluetoothId.description = MAC-Adresse des verbundenen Bluetoothgerätes +channel-type.amazonechocontrol.bluetoothIdSelection.label = Bluetooth Verbindingungsauswahl +channel-type.amazonechocontrol.bluetoothIdSelection.description = Bluetooth Verbindingungsauswahl (Derzeit nur in PaperUI) + +channel-type.amazonechocontrol.remind.label = Erinnere +channel-type.amazonechocontrol.remind.description = Spricht die Erinnerung und schickt eine Benachricht an die Alexa-APP (Nur schreiben) + +channel-type.amazonechocontrol.playAlarmSound.label = Spielt Alarm Sound +channel-type.amazonechocontrol.playAlarmSound.description = Spielt Alarm Sound ab (Nur schreiben) + channel-type.amazonechocontrol.imageUrl.label = Bild URL channel-type.amazonechocontrol.imageUrl.description = URL des Album Covers oder des Radiostations Logos @@ -53,7 +95,7 @@ channel-type.amazonechocontrol.subtitle1.description = Untertitel 1 channel-type.amazonechocontrol.subtitle2.label = Untertitel 2 channel-type.amazonechocontrol.subtitle2.description = Untertitel 2 -channel-type.amazonechocontrol.radio.label = Radio +channel-type.amazonechocontrol.radio.label = TuneIn Radio channel-type.amazonechocontrol.radio.description = Radio eingestalten channel-type.amazonechocontrol.bluetooth.label = Bluetooth Verbinding diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index d3d948246c55f..23337c4296377 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -28,13 +28,13 @@ - Enter the email address of the amazon account which is used for the amazon echo devices. + Enter the email address of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! password - Enter the password of the amazon account which is used for the amazon echo devices. + Enter the password of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! 60 @@ -48,18 +48,14 @@ - - - - + Amazon Echo device (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) - @@ -70,12 +66,18 @@ + - - + + + + + + + serialNumber @@ -88,6 +90,166 @@ + + + + + + + Amazon Echo Spot device + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + + + + Amazon Echo Spot device + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + + + + Amazon Multiroom Music + + + + + + + + + + + + + + + + + + serialNumber + + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + + + + Unknown Echo Device. Warning: Maybe not all channels will be supported from the device + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + + You will find the serial number of your device in the Alexa app + + + + String @@ -98,23 +260,63 @@ String - + Id of the radio station + + String + + Speak the reminder and send a notification to the Alexa app (Write Only) + - + + String + + Plays an alarm sound (Write Only) + + + + String + + Id of the amazon music track + + + + Switch + + Amazon Music turned on + + + + String + + Amazon Music play list id (Write only, no current state) + + + + String + + Is of the playlist which was started with openHAB + + + String Name of music provider - String - MAC-Address of the bluethooth connected device + MAC-Address of the bluetooth connected device + + + + String + + Bluetooth connection selection (Currently only in PaperUI) @@ -147,7 +349,7 @@ Switch - + Radio turned on @@ -175,7 +377,6 @@ Music Player - Dimmer diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index dbdd7d5df4b66..a3154c67054e1 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -2,27 +2,32 @@ This binding let control openHAB Amazon Echo devices (Alexa). -The idea for writing this binding came from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German) Thank you Alex! +The idea for writing this binding came from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German). Thank you Alex! The binding provide features to control and view the current state of echo dot devices: - volume - pause/continue/next track/previous track - connect/disconnect bluetooth devices -- start playing radio +- start playing tuneIn radio +- start playing Amazon Music +- control of multi room music - show album art image in sitemap +- speak a reminder message +- plays an alarm sound Some ideas what you can do in your home by using rules and other openHAB controlled devices: -- Automatic turn on your amplifier and connect echo with bluetooth if the echo playes music +- Automatic turn on your amplifier and connect echo with bluetooth if the echo plays music - If the amplifier was turned of, the echo stop playing and disconnect the bluetooth - The echo starts playing radio if the light was turned on - The echo starts playing radio at specified time +- Remind you we a voice message, that a window is open for a long time and it is winter ## Note ## This binding uses the same API as the Web-Browser-Based Alexa site (alexa.amazon.de). In other words, it simulates a user which is using the web page. -Unfortunately, the binding can get broken if Amazon change the website. +Unfortunately, the binding can get broken if Amazon change the web site. The binding is tested with amazon.de and amazon.co.uk accounts, but should also work with all others. @@ -36,56 +41,70 @@ All the display options are updated by polling the amazon server. The polling ti ## Supported Things -| Thing type id | Name | -|--------------------------|-----------------------| -| account | Amazon Account | -| echo | Amazon Echo Device | +| Thing type id | Name | +|---------------------|---------------------------------------| +| account | Amazon Account | +| echo | Amazon Echo Device | +| echospot | Amazon Echo Spot Device | +| echoshow | Amazon Echo Show Device | +| wha | Amazon Echo Whole House Audio Control | +| unknown | Unknown Echo Device or App | +The unknown device will provide all channels, but maybe not all of them supported by your device. ## Discovery -The first 'Amazon Account' thing will be automatically discovered. After configuration of the thing with the account data, a 'Amazon Echo' thing will be discovered for each registered device. +The first 'Amazon Account' thing will be automatically discovered. After configuration of the thing with the account data, a 'Amazon ' thing will be discovered for each registered device. If the device type is not known by the binding, an 'Unknown' device will be created. ## Binding Configuration -The binding does not have any configuration. The configuration of your amazon account habe to be done in the 'Amazon Account' device. +The binding does not have any configuration. The configuration of your amazon account must be done in the 'Amazon Account' device. ## Thing Configuration The Amazon Account device need the following configurations: -| Config name | Description | -|--------------------------|-----------------------| -| amazonSite | The amazon site where the echos are registered. e.g. amazon.de | -| email | Email of your amazon account | -| password | Password of your amazon account | -| pollingIntervalInSeconds | Polling interval for the device state in seconds. Default 60, minimum 10 | +| Configuration name | Description | +|--------------------------|---------------------------------------------------------------------------| +| amazonSite | The amazon site where the echos are registered. e.g. amazon.de | +| email | Email of your amazon account | +| password | Password of your amazon account | +| pollingIntervalInSeconds | Polling interval for the device state in seconds. Default 60, minimum 10 | -The Amazon Echo device need the following configurations: +2 factor authentication is not supported! -| Config name | Description | -|--------------------------|-----------------------| +All Amazon devices needs the following configurations: + +| Configuration name | Description | +|--------------------------|----------------------------------------------------| | serialNumber | Serial number of the amazon echo in the Alexa app | You will find the serial number in the alexa app. ## Channels -| Channel Type ID | Item Type | Access Mode | Description -|---------------------|-----------|-------------|------------------------------------------------------------------------------------------------------- -| player | Player | R/W | Control the music player e.g. pause/continue/next track/previous track -| volume | Dimmer | R/W | Control the volume -| shuffle | Switch | R/W | Shuffle play if applicable, e.g. playing a playlist -| imageUrl | String | R | Url of the album image or radio station logo -| title | String | R | Title of the current media -| subtitle1 | String | R | Subtitle of the current media -| subtitle2 | String | R | Additional subtitle of the current media -| providerDisplayName | String | R | Name of the music provider -| bluetoothId | String | R/W | Bluetooth device id. Used to connect to a specific device or disconnect if a empty string was provided -| bluetooth | Switch | R/W | Connect/Disconnect to the last used bluetooth device (works after a bluetooth connection was established after the openhab start) -| bluetoothDeviceName | String | R | User friendly name of the connected bluetooth device -| radioStationId | String | R/W | Start playing of a radio station by specifying its id od stops playing if a empty string was provided -| radio | Switch | R/W | Start playing of the last used radio station works after the radio station started after the openhab start) +| Channel Type ID | Item Type | Access Mode | Thing Type | Description +|---------------------|-----------|-------------|------------|------------------------------------------------------------------------------------------ +| player | Player | R/W | echo, echoshow, echospot, wha, unknown | Control the music player e.g. pause/continue/next track/previous track +| volume | Dimmer | R/W | echo, echoshow, echospot, unknown | Control the volume +| shuffle | Switch | R/W | echo, echoshow, echospot, wha, unknown | Shuffle play if applicable, e.g. playing a playlist +| imageUrl | String | R | echo, echoshow, echospot, wha, unknown | Url of the album image or radio station logo +| title | String | R | echo, echoshow, echospot, wha, unknown | Title of the current media +| subtitle1 | String | R | echo, echoshow, echospot, wha, unknown | Subtitle of the current media +| subtitle2 | String | R | echo, echoshow, echospot, wha, unknown | Additional subtitle of the current media +| providerDisplayName | String | R | echo, echoshow, echospot, wha, unknown | Name of the music provider +| bluetoothId | String | R/W | echo, echoshow, echospot, unknown | Bluetooth device id. Used to connect to a specific device or disconnect if a empty string was provided +| bluetoothIdSelection| String | R/W | echo, echoshow, echospot, unknown | Bluetooth device selection. The selection currently only works in PaperUI +| bluetooth | Switch | R/W | echo, echoshow, echospot, unknown | 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, unknown | User friendly name of the connected bluetooth device +| radioStationId | String | R/W | echo, echoshow, echospot, wha, unknown | Start playing of a TuneIn radio station by specifying it's id od stops playing if a empty string was provided +| radio | Switch | R/W | echo, echoshow, echospot, wha, unknown | 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, unknown | Start playing of a Amazon Music track by it's id od stops playing if a empty string was provided +| amazonMusicPlayListId | String | W | echo, echoshow, echospot, wha, unknown | Write Only! Start playing of a Amazon Music playlist by specifying it's id od stops playing if a empty string was provided. Selection will only work in PaperUI +| amazonMusicPlayListIdLastUsed | String | R | echo, echoshow, echospot, wha, unknown | The last play list id started from openHAB +| amazonMusic | Switch | R/W | echo, echoshow, echospot, wha, unknown | Start playing of the last used Amazon Music song (works after at least one song was started after the openhab start) +| remind | String | W | echo, echoshow, echospot, unknown | Write Only! Speak the reminder and sends a notification to the Alexa app (Currently the reminder is played and notified two times, this seems to be a bug in the amazon software) +| playAlarmSound | String | W | echo, echoshow, echospot, unknown | Write Only! Plays an alarm sound. In PaperUI will be a selection box available. For rules use the value shown in the square brackets ## Full Example @@ -94,7 +113,11 @@ You will find the serial number in the alexa app. ``` Bridge amazonechocontrol:account:account1 [amazonSite="amazon.de", email="myaccountemail@myprovider.com", password="secure", pollingIntervalInSeconds=60] { - Thing echo echo1 "Alexa" @ "Living Room" [serialNumber="SERIAL_NUMBER"] + Thing echo echo1 "Alexa" @ "Living Room" [serialNumber="SERIAL_NUMBER"] + Thing echoshow echo2 "Alexa" @ "Kitchen" [serialNumber="SERIAL_NUMBER"] + Thing echospot echo3 "Alexa" @ "Sleeping Room" [serialNumber="SERIAL_NUMBER"] + Thing wha echo4 "Alexa" @ "Ground Floor Music Group" [serialNumber="SERIAL_NUMBER"] + Thing unknown echo4 "Alexa" @ "Very new echo device" [serialNumber="SERIAL_NUMBER"] } ``` @@ -102,22 +125,31 @@ You will find the serial number in the Alexa app. ### amzonechocontrol.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. + ``` Group Alexa_Living_Room -Player Echo_Living_Room_Player "Player" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:player"} -Dimmer Echo_Living_Room_Volume "Volume [%.0f %%]" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:volume"} -Switch Echo_Living_Room_Shuffle "Shuffle" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:shuffle"} -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_BluetoothId "Bluetooth Id" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothId"} -Switch Echo_Living_Room_Bluetooth "Bluetooth" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetooth"} -String Echo_Living_Room_BluetoothDeviceName "Bluetooth Device" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothDeviceName"} -String Echo_Living_Room_RadioStationId "Radio Station Id" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:radioStationId"} -Switch Echo_Living_Room_Radio "Radio" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:radio"} +Player Echo_Living_Room_Player "Player" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:player"} +Dimmer Echo_Living_Room_Volume "Volume [%.0f %%]" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:volume"} +Switch Echo_Living_Room_Shuffle "Shuffle" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:shuffle"} +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_BluetoothId "Bluetooth Mac Address" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothId"} +String Echo_Living_Room_BluetoothId_Selection "Bluetooth Device" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothId"} +Switch Echo_Living_Room_Bluetooth "Bluetooth" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetooth"} +String Echo_Living_Room_BluetoothDeviceName "Bluetooth Device" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothDeviceName"} +String Echo_Living_Room_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"} +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 (Write Only)" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusicPlayListId"} +String Echo_Living_Room_AmazonMusicPlayListIdLastUsed "Amazon Music Playlist Id last used" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusicPlayListIdLastUsed"} +Switch Echo_Living_Room_AmazonMusic "Amazon Music" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusic"} +String Echo_Living_Room_Remind "Remind" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusic"} +String Echo_Living_Room_PlayAlarmSound "Play Alarm Sound" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playAlarmSound"} ``` ### amzonechocontrol.sitemap: @@ -133,17 +165,29 @@ sitemap amzonechocontrol label="Echo Devices" Text item=Echo_Living_Room_Subtitle1 Text item=Echo_Living_Room_Subtitle2 Text item=Echo_Living_Room_ProviderDisplayName + Text item=Echo_Living_Room_BluetoothId_Selection Text item=Echo_Living_Room_BluetoothId Switch item=Echo_Living_Room_Bluetooth Text item=Echo_Living_Room_BluetoothDeviceName Text item=Echo_Living_Room_RadioStationId - Switch item=Echo_Living_Room_Radio + Switch item=Echo_Living_Room_Radio + Text item=Echo_Living_Room_AmazonMusicTrackId + Text item=Echo_Living_Room_AmazonMusicPlayListId + Text item=Echo_Living_Room_AmazonMusicPlayListIdLastUsed + Switch item=Echo_Living_Room_AmazonMusic + Text item=Echo_Living_Room_Remind + Text item=Echo_Living_Room_PlayAlarmSound } } ``` -## Trademark Disclaimer +To get instead of the id fields an selection box, use the Selection element and provide mappings for your favorite id's: + +``` + Selection item=Echo_Living_Room_RadioStationId mappings=[ ''='Off', 's1139'='Antenne Steiermark', 's8007'='Hitradio Ă–3', 's16793'='Radio 10', 's8235'='FM4' ] +``` -All Amazon Echo, Alexa and other products and Amazon and other companies are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. +## Trademark Disclaimer +TuneIn, Amazon Echo, Amazon Echo Spot, Amazon Echo Show, Amazon Music, Amazon Prime, Alexa and all other products and Amazon, TuneIn and other companies are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java index c125289809d79..8cbf1f71ed0b7 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java @@ -14,6 +14,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; /** * The {@link AmazonEchoControlBindingConstants} class defines common constants, which are @@ -24,14 +25,18 @@ @NonNullByDefault public class AmazonEchoControlBindingConstants { - private static final String BINDING_ID = "amazonechocontrol"; + public static final String BINDING_ID = "amazonechocontrol"; // List of all Thing Type UIDs - public static final ThingTypeUID THING_TYPE_ECHO = new ThingTypeUID(BINDING_ID, "echo"); public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account"); + public static final ThingTypeUID THING_TYPE_ECHO = new ThingTypeUID(BINDING_ID, "echo"); + 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_UNKNOWN = new ThingTypeUID(BINDING_ID, "unknown"); - public static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet( - Arrays.asList(THING_TYPE_ECHO, THING_TYPE_ACCOUNT)); + public static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet(Arrays.asList( + THING_TYPE_ACCOUNT, THING_TYPE_ECHO, THING_TYPE_ECHO_SPOT, THING_TYPE_ECHO_WHA, THING_TYPE_UNKNOWN)); // List of all Channel ids public static final String CHANNEL_PLAYER = "player"; @@ -45,12 +50,27 @@ public class AmazonEchoControlBindingConstants { public static final String CHANNEL_SUBTITLE2 = "subtitle2"; public static final String CHANNEL_PROVIDER_DISPLAY_NAME = "providerDisplayName"; public static final String CHANNEL_BLUETOOTH_ID = "bluetoothId"; + public static final String CHANNEL_BLUETOOTH_ID_SELECTION = "bluetoothIdSelection"; 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_AMAZON_MUSIC_PLAY_LIST_ID_LAST_USED = "amazonMusicPlayListIdLastUsed"; + public static final String CHANNEL_REMIND = "remind"; + public static final String CHANNEL_PLAY_ALARM_SOUND = "playAlarmSound"; + + // List of channel Type UIDs + public static final ChannelTypeUID CHANNEL_TYPE_BLUETHOOTH_ID_SELECTION = new ChannelTypeUID(BINDING_ID, + "bluetoothIdSelection"); + 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"); // List of all Properties public static final String DEVICE_PROPERTY_SERIAL_NUMBER = "serialNumber"; + public static final String DEVICE_PROPERTY_FAMILY = "deviceFamily"; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 08eb12d023a11..e6073658c11c3 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -10,6 +10,7 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; +import java.util.HashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -25,6 +26,7 @@ import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; @@ -37,6 +39,7 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState.QueueEntry; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo.InfoText; @@ -56,14 +59,18 @@ public class EchoHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(EchoHandler.class); + private static HashMap instances = new HashMap(); private @Nullable Device device; private @Nullable Connection connection; private @Nullable ScheduledFuture updateStateJob; private @Nullable String lastKnownRadioStationId; private @Nullable String lastKnownBluetoothId; + private @Nullable String lastKnownAmazonMusicId; private int lastKnownVolume = 25; private @Nullable BluetoothState bluetoothState; private boolean disableUpdate = false; + private boolean updateRemind = true; + private boolean updateAlarm = true; public EchoHandler(Thing thing) { super(thing); @@ -72,6 +79,9 @@ public EchoHandler(Thing thing) { @Override public void initialize() { logger.info("Amazon Echo Control Binding initialized"); + synchronized (instances) { + instances.put(this.getThing().getUID(), this); + } updateStatus(ThingStatus.ONLINE); } @@ -82,6 +92,9 @@ public void intialize(Connection connection, @Nullable Device deviceJson) { @Override public void dispose() { + synchronized (instances) { + instances.remove(this.getThing().getUID()); + } ScheduledFuture updateStateJob = this.updateStateJob; this.updateStateJob = null; if (updateStateJob != null) { @@ -90,6 +103,24 @@ public void dispose() { super.dispose(); } + public static @Nullable EchoHandler find(ThingUID uid) { + synchronized (instances) { + return instances.get(uid); + } + } + + public @Nullable BluetoothState findBluetoothState() { + return this.bluetoothState; + } + + public @Nullable Connection findConnection() { + return this.connection; + } + + public @Nullable Device findDevice() { + return this.device; + } + public String findSerialNumber() { String id = (String) getConfig().get(DEVICE_PROPERTY_SERIAL_NUMBER); if (id == null) { @@ -143,24 +174,27 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof PercentType) { PercentType value = (PercentType) command; int volume = value.intValue(); - temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume + "}"); + temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume + + ",\"contentFocusClientId\":\"Default\"}"); } else if (command == OnOffType.OFF) { - temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + 0 + "}"); + temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + 0 + + ",\"contentFocusClientId\":\"Default\"}"); } else if (command == OnOffType.ON) { - temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + "}"); + temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + + ",\"contentFocusClientId\":\"Default\"}"); } else if (command == IncreaseDecreaseType.INCREASE) { if (lastKnownVolume < 100) { lastKnownVolume++; updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume)); - temp.command(device, - "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + "}"); + temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + + ",\"contentFocusClientId\":\"Default\"}"); } } else if (command == IncreaseDecreaseType.DECREASE) { if (lastKnownVolume > 0) { lastKnownVolume--; updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume)); - temp.command(device, - "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + "}"); + temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + + ",\"contentFocusClientId\":\"Default\"}"); } } } @@ -175,7 +209,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } // bluetooth commands - if (channelId.equals(CHANNEL_BLUETOOTH_ID)) { + if (channelId.equals(CHANNEL_BLUETOOTH_ID) || channelId.equals(CHANNEL_BLUETOOTH_ID_SELECTION)) { needBluetoothRefresh = true; if (command instanceof StringType) { String address = ((StringType) command).toFullString(); @@ -211,6 +245,43 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (channelId.equals(CHANNEL_BLUETOOTH_DEVICE_NAME)) { needBluetoothRefresh = true; } + // amazon music commands + if (channelId.equals(CHANNEL_AMAZON_MUSIC_TRACK_ID)) { + if (command instanceof StringType) { + + String trackId = ((StringType) command).toFullString(); + if (trackId != null && !trackId.isEmpty()) { + waitForUpdate = 3000; + } + temp.playAmazonMusicTrack(device, trackId); + + } + } + if (channelId.equals(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID)) { + if (command instanceof StringType) { + + String playListId = ((StringType) command).toFullString(); + if (playListId != null && !playListId.isEmpty()) { + waitForUpdate = 3000; + updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID_LAST_USED, new StringType(playListId)); + } + temp.playAmazonMusicPlayList(device, playListId); + + } + } + if (channelId.equals(CHANNEL_AMAZON_MUSIC)) { + + if (command == OnOffType.ON) { + if (lastKnownAmazonMusicId != null && !lastKnownAmazonMusicId.isEmpty()) { + waitForUpdate = 3000; + } + temp.playAmazonMusicTrack(device, lastKnownAmazonMusicId); + } else if (command == OnOffType.OFF) { + temp.playAmazonMusicTrack(device, ""); + } + + } + // radio commands if (channelId.equals(CHANNEL_RADIO_STATION_ID)) { if (command instanceof StringType) { @@ -220,7 +291,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { waitForUpdate = 3000; } temp.playRadio(device, stationId); - } } if (channelId.equals(CHANNEL_RADIO)) { @@ -235,7 +305,38 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } + // notification + if (channelId.equals(CHANNEL_REMIND)) { + if (command instanceof StringType) { + + String reminder = ((StringType) command).toFullString(); + if (reminder != null && !reminder.isEmpty()) { + waitForUpdate = 3000; + updateRemind = true; + temp.notification(device, "Reminder", reminder, null); + } + } + } + if (channelId.equals(CHANNEL_PLAY_ALARM_SOUND)) { + if (command instanceof StringType) { + String alarmSound = ((StringType) command).toFullString(); + if (alarmSound != null && !alarmSound.isEmpty()) { + waitForUpdate = 3000; + updateAlarm = true; + String[] parts = alarmSound.split(":", 2); + JsonNotificationSound sound = new JsonNotificationSound(); + if (parts.length == 2) { + sound.providerId = parts[0]; + sound.id = parts[1]; + } else { + sound.providerId = "ECHO"; + sound.id = alarmSound; + } + temp.notification(device, "Alarm", null, sound); + } + } + } // force update of the state this.disableUpdate = true; final boolean bluetoothRefresh = needBluetoothRefresh; @@ -295,9 +396,9 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto JsonPlayerState playerState = connection.getPlayer(device); playerInfo = playerState.playerInfo; if (playerInfo != null) { - infoText = playerInfo.miniInfoText; + infoText = playerInfo.infoText; if (infoText == null) { - infoText = playerInfo.infoText; + infoText = playerInfo.miniInfoText; } mainArt = playerInfo.mainArt; provider = playerInfo.provider; @@ -330,6 +431,20 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto // check playing boolean playing = playerInfo != null && playerInfo.state != null && playerInfo.state.equals("PLAYING"); + // handle amazon music + String amazonMusicTrackId = ""; + String amazonMusicPlayListId = ""; + boolean amazonMusic = false; + if (mediaState != null && mediaState.currentState != null && mediaState.currentState.equals("PLAYING") + && mediaState.providerId != null && mediaState.providerId.equals("CLOUD_PLAYER") + && mediaState.contentId != null && !mediaState.contentId.isEmpty()) { + + amazonMusicTrackId = mediaState.contentId; + lastKnownAmazonMusicId = amazonMusicTrackId; + amazonMusic = true; + + } + // handle bluetooth String bluetoothId = ""; String bluetoothDeviceName = ""; @@ -349,7 +464,6 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } } } - } if (bluetoothId != null && !bluetoothId.isEmpty()) { lastKnownBluetoothId = bluetoothId; @@ -363,8 +477,8 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } } String radioStationId = ""; - - if (isRadio && mediaState != null && mediaState.radioStationId != null) { + if (isRadio && mediaState != null && mediaState.currentState != null + && mediaState.currentState.equals("PLAYING") && mediaState.radioStationId != null) { radioStationId = mediaState.radioStationId; } // handle title, subtitle, imageUrl @@ -438,6 +552,15 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } // Update states + if (updateRemind) { + updateState(CHANNEL_REMIND, new StringType("")); + } + if (updateAlarm) { + updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType("")); + } + updateState(CHANNEL_AMAZON_MUSIC_TRACK_ID, new StringType(amazonMusicTrackId)); + updateState(CHANNEL_AMAZON_MUSIC, playing && amazonMusic ? OnOffType.ON : OnOffType.OFF); + updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID, new StringType(amazonMusicPlayListId)); updateState(CHANNEL_RADIO_STATION_ID, new StringType(radioStationId)); updateState(CHANNEL_RADIO, playing && isRadio ? OnOffType.ON : OnOffType.OFF); updateState(CHANNEL_VOLUME, volume != null ? new PercentType(volume) : UnDefType.UNDEF); @@ -451,6 +574,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto if (bluetoothState != null) { updateState(CHANNEL_BLUETOOTH, bluetoothIsConnected ? OnOffType.ON : OnOffType.OFF); updateState(CHANNEL_BLUETOOTH_ID, new StringType(bluetoothId)); + updateState(CHANNEL_BLUETOOTH_ID_SELECTION, new StringType(bluetoothId)); updateState(CHANNEL_BLUETOOTH_DEVICE_NAME, new StringType(bluetoothDeviceName)); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 4499a43fb8481..e498439cca3b5 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -43,20 +43,11 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { AccountHandler bridgeHandler = new AccountHandler((Bridge) thing); - return bridgeHandler; } - if (thingTypeUID.equals(THING_TYPE_ECHO)) { + if (SUPPORTED_THING_TYPES_UIDS.contains(THING_TYPE_ECHO)) { return new EchoHandler(thing); } - return null; } - - @Override - protected void removeHandler(ThingHandler thingHandler) { - - super.removeHandler(thingHandler); - } - } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 903330cdbaa33..f77da24f28dc3 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -17,6 +17,7 @@ import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Map; @@ -30,11 +31,17 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationRequest; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSounds; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; /** * The {@link Connection} is responsible for the connection to the amazon server and @@ -217,7 +224,8 @@ public Date tryGetLoginTime() { return m_loginTime; } - private HttpsURLConnection makeRequest(String url, String referer, String postData, Boolean json) throws Exception { + private HttpsURLConnection makeRequest(String verb, String url, String referer, String postData, Boolean json) + throws Exception { String currentUrl = url; for (int i = 0; i < 30; i++) // loop for handling redirect, using automatic redirect is not possible, because // all response headers must be catched @@ -228,6 +236,7 @@ private HttpsURLConnection makeRequest(String url, String referer, String postDa logger.debug("Make request to {}", url); connection = (HttpsURLConnection) new URL(currentUrl).openConnection(); + connection.setRequestMethod(verb); connection.setRequestProperty("Accept-Language", "en-US"); connection.setRequestProperty("User-Agent", "Mozilla/5.0"); connection.setRequestProperty("DNT", "1"); @@ -254,7 +263,6 @@ private HttpsURLConnection makeRequest(String url, String referer, String postDa if (cookieHeaderBuilder.length() > 0) { connection.setRequestProperty("Cookie", cookieHeaderBuilder.toString()); } - if (postData != null) { // post data @@ -269,8 +277,9 @@ private HttpsURLConnection makeRequest(String url, String referer, String postDa connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); } connection.setRequestProperty("Content-Length", Integer.toString(postDataLength)); - connection.setRequestMethod("POST"); - connection.setRequestProperty("Expect", "100-continue"); + if (verb == "POST") { + connection.setRequestProperty("Expect", "100-continue"); + } connection.setDoOutput(true); OutputStream outputStream = connection.getOutputStream(); @@ -393,7 +402,7 @@ public void makeLogin() throws Exception { String referer = "https://www." + m_amazonSite + "/ap/signin?" + queryParameters; m_cookieManager.getCookieStore().add(new URL("https://www." + m_amazonSite).toURI(), HttpCookie.parse("session-id=" + m_sessionId).get(0)); - String response = makeRequestAndReturnString("https://www." + m_amazonSite + "/ap/signin", referer, + String response = makeRequestAndReturnString("POST", "https://www." + m_amazonSite + "/ap/signin", referer, postData, false); if (response.contains("Amazon Alexa")) { logger.debug("Response seems to be alexa app"); @@ -424,12 +433,12 @@ public void makeLogin() throws Exception { } private String makeRequestAndReturnString(String url) throws Exception { - return makeRequestAndReturnString(url, null, null, false); + return makeRequestAndReturnString("GET", url, null, null, false); } - private String makeRequestAndReturnString(String url, String referer, String postData, Boolean json) + private String makeRequestAndReturnString(String verb, String url, String referer, String postData, Boolean json) throws Exception { - HttpsURLConnection connection = makeRequest(url, referer, postData, json); + HttpsURLConnection connection = makeRequest(verb, url, referer, postData, json); return getResponse(connection); } @@ -456,54 +465,71 @@ public void logout() { m_loginTime = null; } + // parser + private T parseJson(String json, Class type) { + try { + Gson gson = new Gson(); + return gson.fromJson(json, type); + } catch (JsonSyntaxException e) { + logger.warn("Parsing json failed {}", e); + logger.warn("{}", json); + throw e; + } + } + // commands and states public Device[] getDeviceList() throws Exception { String json = makeRequestAndReturnString(m_alexaServer + "/api/devices-v2/device?cached=false"); - Gson gson = new Gson(); - JsonDevices devices = gson.fromJson(json, JsonDevices.class); + JsonDevices devices = parseJson(json, JsonDevices.class); return devices.devices; } public JsonPlayerState getPlayer(Device device) throws Exception { String json = makeRequestAndReturnString(m_alexaServer + "/api/np/player?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&screenWidth=1440"); - Gson gson = new Gson(); - JsonPlayerState playerState = gson.fromJson(json, JsonPlayerState.class); + JsonPlayerState playerState = parseJson(json, JsonPlayerState.class); return playerState; } public JsonMediaState getMediaState(Device device) throws Exception { String json = makeRequestAndReturnString(m_alexaServer + "/api/media/state?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType); - Gson gson = new Gson(); - JsonMediaState mediaState = gson.fromJson(json, JsonMediaState.class); + JsonMediaState mediaState = parseJson(json, JsonMediaState.class); return mediaState; } public JsonBluetoothStates getBluetoothConnectionStates() throws Exception { String json = makeRequestAndReturnString(m_alexaServer + "/api/bluetooth?cached=true"); - Gson gson = new Gson(); - JsonBluetoothStates bluetoothStates = gson.fromJson(json, JsonBluetoothStates.class); + JsonBluetoothStates bluetoothStates = parseJson(json, JsonBluetoothStates.class); return bluetoothStates; } + public JsonPlaylists getPlaylists(Device device) throws Exception { + String json = makeRequestAndReturnString( + m_alexaServer + "/api/cloudplayer/playlists?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId); + JsonPlaylists playlists = parseJson(json, JsonPlaylists.class); + return playlists; + } + public void command(Device device, String command) throws Exception { String url = m_alexaServer + "/api/np/command?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType; - makeRequest(url, null, command, true); + makeRequest("POST", url, null, command, true); } public void bluetooth(Device device, String address) throws Exception { if (address == null || address.isEmpty()) { // disconnect - makeRequest( + makeRequest("POST", m_alexaServer + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" + device.serialNumber, null, "", true); } else { - makeRequest(m_alexaServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber, - null, "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true); + makeRequest("POST", + m_alexaServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber, null, + "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true); } } @@ -512,7 +538,7 @@ public void playRadio(Device device, String stationId) throws Exception { if (stationId == null || stationId.isEmpty()) { command(device, "{\"type\":\"PauseCommand\"}"); } else { - makeRequest( + makeRequest("POST", m_alexaServer + "/api/tunein/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&guideId=" + stationId + "&contentType=station&callSign=&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId, @@ -520,11 +546,71 @@ public void playRadio(Device device, String stationId) throws Exception { } } - public void playPrimeSong(Device device, String trackId) throws Exception { - String command = "{\"trackId\":\"" + trackId + "\",\"playQueuePrime\":true}"; - makeRequest(m_alexaServer + "/api/cloudplayer?deviceSerialNumber=" + device.serialNumber + "&deviceType=" - + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId + "&shuffle=false", null, - command, true); + public void playAmazonMusicTrack(Device device, String trackId) throws Exception { + if (trackId == null || trackId.isEmpty()) { + command(device, "{\"type\":\"PauseCommand\"}"); + } else { + String command = "{\"trackId\":\"" + trackId + "\",\"playQueuePrime\":true}"; + makeRequest("POST", + m_alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + + device.deviceOwnerCustomerId + "&shuffle=false", + null, command, true); + } + } + + public void playAmazonMusicPlayList(Device device, String playListId) throws Exception { + if (playListId == null || playListId.isEmpty()) { + command(device, "{\"type\":\"PauseCommand\"}"); + } else { + String command = "{\"playlistId\":\"" + playListId + "\",\"playQueuePrime\":true}"; + makeRequest("POST", + m_alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + + device.deviceOwnerCustomerId + "&shuffle=false", + null, command, true); + } + } + + public JsonNotificationSound[] getNotificationSounds(Device device) throws Exception { + String json = makeRequestAndReturnString( + "GET", m_alexaServer + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber + + "&deviceType=" + device.deviceType + "&softwareVersion=" + device.softwareVersion, + null, null, true); + JsonNotificationSounds result = parseJson(json, JsonNotificationSounds.class); + if (result.notificationSounds != null) { + return result.notificationSounds; + } + return new JsonNotificationSound[0]; + } + + public void notification(Device device, String type, String label, JsonNotificationSound sound) throws Exception { + + Date date = new Date(new Date().getTime()); + long createdDate = date.getTime(); + Date alarm = new Date(createdDate + 5000); // add 5 seconds, because amazon does not except calls for times in + // the past (compared with the server time) + long alarmTime = alarm.getTime(); + + JsonNotificationRequest request = new JsonNotificationRequest(); + request.type = type; + request.deviceSerialNumber = device.serialNumber; + request.deviceType = device.deviceType; + request.createdDate = createdDate; + request.alarmTime = alarmTime; + request.reminderLabel = label; + request.sound = sound; + request.originalDate = new SimpleDateFormat("yyyy-MM-dd").format(alarm); + request.originalTime = new SimpleDateFormat("HH:mm:ss.SSSS").format(alarm); + request.type = type; + request.id = "create" + type; + + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.serializeNulls(); + Gson gson = gsonBuilder.create(); + + String data = gson.toJson(request); + makeRequestAndReturnString("PUT", m_alexaServer + "/api/notifications/createReminder", null, data, true); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 46f4bfa0802ef..d653776d16ca7 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -24,7 +24,9 @@ import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; +import org.openhab.binding.amazonechocontrol.handler.EchoHandler; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -152,21 +154,38 @@ public synchronized void setDevices(ThingUID brigdeThingUID, Device[] deviceInfo if (serialNumber != null) { boolean alreadyfound = toRemove.remove(serialNumber); // new - if (!alreadyfound && deviceInformation.deviceFamily != null - && deviceInformation.deviceFamily.equals("ECHO")) { - - ThingUID thingUID = new ThingUID(THING_TYPE_ECHO, brigdeThingUID, serialNumber); - - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) - .withLabel(deviceInformation.accountName) - .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) - .withRepresentationProperty(DEVICE_PROPERTY_SERIAL_NUMBER).withBridge(brigdeThingUID) - .build(); - - logger.debug("Device [{}, {}] found.", serialNumber, deviceInformation.accountName); - - thingDiscovered(result); - lastDeviceInformations.put(serialNumber, thingUID); + if (!alreadyfound && deviceInformation.deviceFamily != null) { + ThingTypeUID thingTypeId; + if (deviceInformation.deviceFamily.equals("ECHO")) { + thingTypeId = THING_TYPE_ECHO; + } else if (deviceInformation.deviceFamily.equals("ROOK")) { + thingTypeId = THING_TYPE_ECHO_SPOT; + } else if (deviceInformation.deviceFamily.equals("KNIGHT")) { + thingTypeId = THING_TYPE_ECHO_SHOW; + } else if (deviceInformation.deviceFamily.equals("WHA")) { + thingTypeId = THING_TYPE_ECHO_WHA; + } else { + thingTypeId = THING_TYPE_UNKNOWN; + } + + ThingUID thingUID = new ThingUID(thingTypeId, brigdeThingUID, serialNumber); + + // Check if already created + if (EchoHandler.find(thingUID) == null) { + + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) + .withLabel(deviceInformation.accountName) + .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) + .withProperty(DEVICE_PROPERTY_FAMILY, deviceInformation.deviceFamily) + .withRepresentationProperty(DEVICE_PROPERTY_SERIAL_NUMBER).withBridge(brigdeThingUID) + .build(); + + logger.debug("Device [{}: {}] found. Mapped to thing type {}", deviceInformation.deviceFamily, + serialNumber, thingTypeId.getAsString()); + + thingDiscovered(result); + lastDeviceInformations.put(serialNumber, thingUID); + } } } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java index c7144e80ec214..b54fd735481d6 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java @@ -22,6 +22,7 @@ public class Device { public String deviceAccountId; public String deviceFamily; public String deviceType; + public String softwareVersion; public boolean online; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java index e856f528a208f..980226016d781 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java @@ -6,7 +6,6 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ - package org.openhab.binding.amazonechocontrol.internal.jsons; /** @@ -37,7 +36,7 @@ public class JsonMediaState { public String referenceId; public String service; public boolean shuffling; - public int timeLastShuffled; + // public long timeLastShuffled; parsing fails with some values, so do not use it public int volume; public class QueueEntry { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java new file mode 100644 index 0000000000000..08e3c11f4185d --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +/** + * The {@link JsonNotificationRequest} encapsulate the GSON data for a notification request + * + * @author Michael Geramb - Initial contribution + */ +public class JsonNotificationRequest { + public String type = "Reminder"; // "Reminder", "Alarm" + public String status = "ON"; + public long alarmTime; + public String originalTime; + public String originalDate; + public String timeZoneId; + public String reminderIndex; + public JsonNotificationSound sound; + public String deviceSerialNumber; + public String deviceType; + public String recurringPattern; + public String reminderLabel; + public boolean isSaveInFlight = true; + public String id = "createReminder"; + public boolean isRecurring = false; + public long createdDate; + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java new file mode 100644 index 0000000000000..03f6c6c7eef82 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +/** + * The {@link JsonNotificationSound} encapsulate the GSON data for a notification sound + * + * @author Michael Geramb - Initial contribution + */ +public class JsonNotificationSound { + public String displayName; + public String folder; + public String id = "system_alerts_melodic_01"; + public String providerId = "ECHO"; + public String sampleUrl; +} \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSounds.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSounds.java new file mode 100644 index 0000000000000..ed459cdcc305d --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSounds.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +/** + * The {@link JsonNotificationSounds} encapsulate the GSON data for a notification sounds + * + * @author Michael Geramb - Initial contribution + */ +public class JsonNotificationSounds { + public JsonNotificationSound[] notificationSounds; +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaylists.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaylists.java new file mode 100644 index 0000000000000..a522bc97abef0 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaylists.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import java.util.Map; + +/** + * The {@link JsonPlayerState} encapsulate the GSON data of playlist query + * + * @author Michael Geramb - Initial contribution + */ +public class JsonPlaylists { + + public Map playlists; + + public class PlayList { + public String playlistId; + public String title; + public int trackCount; + public int version; + } +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java new file mode 100644 index 0000000000000..09a9549b7ef97 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.statedescription; + +import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; + +import java.util.ArrayList; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider; +import org.eclipse.smarthome.core.types.StateDescription; +import org.eclipse.smarthome.core.types.StateOption; +import org.openhab.binding.amazonechocontrol.handler.EchoHandler; +import org.openhab.binding.amazonechocontrol.internal.Connection; +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.JsonNotificationSound; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists.PlayList; +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 Michael Geramb - Initial contribution + */ +@NonNullByDefault +@Component(service = { DynamicStateDescriptionProvider.class, + AmazonEchoDynamicStateDescriptionProvider.class }, immediate = true) +public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDescriptionProvider { + + private final Logger logger = LoggerFactory.getLogger(EchoHandler.class); + + public AmazonEchoDynamicStateDescriptionProvider() { + + } + + @Override + public @Nullable StateDescription getStateDescription(Channel channel, + @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { + + if (originalStateDescription == null) { + return null; + } + if (CHANNEL_TYPE_BLUETHOOTH_ID_SELECTION.equals(channel.getChannelTypeUID())) { + + EchoHandler handler = EchoHandler.find(channel.getUID().getThingUID()); + if (handler == null) { + return originalStateDescription; + } + + BluetoothState bluetoothState = handler.findBluetoothState(); + if (bluetoothState == null) { + return originalStateDescription; + } + + if (bluetoothState.pairedDeviceList == null) { + return originalStateDescription; + } + + ArrayList options = new ArrayList(); + options.add(new StateOption("", "")); + for (PairedDevice device : bluetoothState.pairedDeviceList) { + if (device.address != null && device.friendlyName != null) { + options.add(new StateOption(device.address, device.friendlyName)); + } + } + StateDescription result = new StateDescription(originalStateDescription.getMinimum(), + originalStateDescription.getMaximum(), originalStateDescription.getStep(), + originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); + return result; + + } else if (CHANNEL_TYPE_AMAZON_MUSIC_PLAY_LIST_ID.equals(channel.getChannelTypeUID())) { + + EchoHandler handler = EchoHandler.find(channel.getUID().getThingUID()); + if (handler == null) { + return originalStateDescription; + } + Connection connection = handler.findConnection(); + if (connection == null) { + return originalStateDescription; + } + Device device = handler.findDevice(); + if (device == null) { + return originalStateDescription; + } + JsonPlaylists playLists; + try { + playLists = connection.getPlaylists(device); + } catch (Exception e) { + logger.warn("Get playlist failed: {}", e); + return originalStateDescription; + } + ArrayList options = new ArrayList(); + options.add(new StateOption("", "")); + if (playLists.playlists != null) { + for (PlayList[] innerLists : playLists.playlists.values()) { + if (innerLists.length > 0) { + PlayList playList = innerLists[0]; + if (playList.playlistId != null && playList.title != null) { + options.add(new StateOption(playList.playlistId, + String.format("%s [%d]", playList.title, playList.trackCount))); + } + } + } + } + StateDescription result = new StateDescription(originalStateDescription.getMinimum(), + originalStateDescription.getMaximum(), originalStateDescription.getStep(), + originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); + return result; + } else if (CHANNEL_TYPE_PLAY_ALARM_SOUND.equals(channel.getChannelTypeUID())) { + + EchoHandler handler = EchoHandler.find(channel.getUID().getThingUID()); + if (handler == null) { + return originalStateDescription; + } + Connection connection = handler.findConnection(); + if (connection == null) { + return originalStateDescription; + } + Device device = handler.findDevice(); + if (device == null) { + return originalStateDescription; + } + + JsonNotificationSound[] notificationSounds; + try { + notificationSounds = connection.getNotificationSounds(device); + } catch (Exception e) { + logger.warn("Get notification sounds failed: {}", e); + return originalStateDescription; + } + ArrayList options = new ArrayList(); + options.add(new StateOption("", "")); + if (notificationSounds != null) { + 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, + String.format("%s [%s]", notificationSound.displayName, providerSoundId))); + + } + } + } + StateDescription result = new StateDescription(originalStateDescription.getMinimum(), + originalStateDescription.getMaximum(), originalStateDescription.getStep(), + originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); + return result; + } + return originalStateDescription; + } + +} From 7acb509fcb7a8f330838f490a3990d5d6275d50b Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sat, 27 Jan 2018 13:34:49 +0100 Subject: [PATCH 13/56] [amazonechocontrol] fix wrong readme item sample Signed-off-by: Michael Geramb (github: mgeramb) --- .../org.openhab.binding.amazonechocontrol/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index a3154c67054e1..be455b3d31d83 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -148,7 +148,7 @@ String Echo_Living_Room_AmazonMusicTrackId "Amazon Music Track Id" String Echo_Living_Room_AmazonMusicPlayListId "Amazon Music Playlist Id (Write Only)" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusicPlayListId"} String Echo_Living_Room_AmazonMusicPlayListIdLastUsed "Amazon Music Playlist Id last used" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusicPlayListIdLastUsed"} Switch Echo_Living_Room_AmazonMusic "Amazon Music" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusic"} -String Echo_Living_Room_Remind "Remind" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusic"} +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"} ``` @@ -174,9 +174,9 @@ sitemap amzonechocontrol label="Echo Devices" Text item=Echo_Living_Room_AmazonMusicTrackId Text item=Echo_Living_Room_AmazonMusicPlayListId Text item=Echo_Living_Room_AmazonMusicPlayListIdLastUsed - Switch item=Echo_Living_Room_AmazonMusic - Text item=Echo_Living_Room_Remind - Text item=Echo_Living_Room_PlayAlarmSound + Switch item=Echo_Living_Room_AmazonMusic + Text item=Echo_Living_Room_Remind + Text item=Echo_Living_Room_PlayAlarmSound } } ``` From d42b52ebacf080e991ee03dbc781786e60c2e626 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Fri, 9 Mar 2018 21:31:49 +0100 Subject: [PATCH 14/56] [amazonechocontrol] New Features: Remind / AlarmSound can be stopped and will be removed Start Flash Briefing Start Weather News Start Traffic News Start Automation Routines FlashBriefing Profiles Bugfixes: Stopping update state after some time Storing session for account things defined in thing file Signed-off-by: Michael Geramb (github: mgeramb) --- .../i18n/amazonechocontrol_de.properties | 60 +++- .../ESH-INF/thing/thing-types.xml | 149 +++++++++- .../README.md | 82 +++-- .../AmazonEchoControlBindingConstants.java | 27 +- .../handler/AccountHandler.java | 280 +++++++++++++++--- .../handler/EchoHandler.java | 121 +++++++- .../handler/FlashBriefingProfileHandler.java | 228 ++++++++++++++ .../handler/SmartHomeBaseHandler.java | 104 +++++++ .../handler/SmartHomeDimmerHandler.java | 70 +++++ .../handler/SmartHomeSwitchHandler.java | 46 +++ .../internal/AccountConfiguration.java | 3 +- .../AmazonEchoControlHandlerFactory.java | 12 + .../internal/Connection.java | 171 ++++++++++- .../internal/StateStorage.java | 111 +++++++ .../discovery/AmazonEchoDiscovery.java | 102 ++++++- .../discovery/IAmazonEchoDiscovery.java | 2 +- .../internal/jsons/JsonAutomation.java | 39 +++ .../internal/jsons/JsonDevices.java | 1 + .../internal/jsons/JsonEnabledFeeds.java | 19 ++ .../internal/jsons/JsonFeed.java | 21 ++ .../internal/jsons/JsonNetworkDetails.java | 19 ++ .../jsons/JsonNotificationResponse.java | 61 ++++ .../internal/jsons/JsonSmartHomeDevice.java | 21 ++ .../jsons/JsonStartRoutineRequest.java | 20 ++ ...onEchoDynamicStateDescriptionProvider.java | 31 +- 25 files changed, 1716 insertions(+), 84 deletions(-) create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAutomation.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEnabledFeeds.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties index 86fa4e638fa9c..6df809db10e80 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties @@ -1,10 +1,9 @@ -# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE -# FIXME: please do not add the file to the repo if you add or change no content # binding binding.amazonechocontrol.name = Amazon Echo Steuerung Binding -binding.amazonechocontrol.description = Binding zum Steuern von Amazon Echo (Alexa). Diese Binding ermöglicht openHAB die Lautstärke, den Wiedergabe Status und die Bluetooth-Verbinding deines Amazon Echo Gerätes zu steuern. +binding.amazonechocontrol.description = Binding zum Steuern von Amazon Echo (Alexa). Diese Binding ermöglicht openHAB die Lautstärke, den Wiedergabe Status und die Bluetooth-Verbindung deines Amazon Echo Gerätes zu steuern. # thing types + thing-type.amazonechocontrol.account.label = Amazon Konto thing-type.amazonechocontrol.account.description = Amazon Konto bei dem dein Amazon Echo registriert ist. Hinweis: 2 Faktor Authentifizierung ist nicht unterstützt. @@ -20,6 +19,9 @@ thing-type.config.amazonechocontrol.account.password.description = Kennwort des thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.label = Status-Aktualisierungs-Intervall thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.description = Aktualtisierungs-Intervall für den Status in Sekunden. Kleinere Zeiten verursachen höheren Netzwerkverkehr. +thing-type.config.amazonechocontrol.account.discoverSmartHomeDevices.label = Sucht Smart Home Geräte +thing-type.config.amazonechocontrol.account.discoverSmartHomeDevices.description = Sucht Smart Home Geräte die über einen Smart Home Alexa Skill verbunden sind. Der OpenHAB Alexa Skill wird ignoriert. + thing-type.amazonechocontrol.echo.label = Amazon Echo thing-type.amazonechocontrol.echo.description = Amazon Echo Gerät (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) @@ -45,9 +47,25 @@ thing-type.config.amazonechocontrol.wha.serialNumber.description = Die Seriennum thing-type.amazonechocontrol.unknown.label = Unbekanntes Echo Gerät oder unbekannte App thing-type.amazonechocontrol.unknown.description = Unbekanntes Echo Gerät. Warnung: Möglicherweise werden nicht alle Kanäle vom Gerät unterstützt +thing-type.amazonechocontrol.flashbriefingprofile.label = Tägliche Zusammenfassungsprofile +thing-type.amazonechocontrol.flashbriefingprofile.description = Speichert und läd eine Tägliches Zusammenfassungskonfiguration + + thing-type.config.amazonechocontrol.unknown.serialNumber.label = Seriennummer thing-type.config.amazonechocontrol.unknown.serialNumber.description = Die Seriennummer findest du in der Alexa App. +thing-type.amazonechocontrol.smarthomeswitch.label = Schalter +thing-type.amazonechocontrol.smarthomeswitch.description = Smart Home Switch + +thing-type.config.amazonechocontrol.smarthomeswitch.entityId.label = Entity ID +thing-type.config.amazonechocontrol.smarthomeswitch.entityId.description = Suchfunktion von OpenHAB verwenden um die ID zu erfahren. + +thing-type.amazonechocontrol.smarthomedimmer.label = Dimmer +thing-type.amazonechocontrol.smarthomedimmer.description = Smart Home Dimmer + +thing-type.config.amazonechocontrol.smarthomedimmer.entityId.label = Entity ID +thing-type.config.amazonechocontrol.smarthomedimmer.entityId.description = Suchfunktion von OpenHAB verwenden um die ID zu erfahren. + # channel types channel-type.amazonechocontrol.bluetoothDeviceName.label = Bluetooth Gerät @@ -71,11 +89,11 @@ channel-type.amazonechocontrol.amazonMusicPlayListIdLastUsed.description = Zulet channel-type.amazonechocontrol.providerDisplayName.label = Anbieter Name channel-type.amazonechocontrol.providerDisplayName.description = Name des Musikanbieters -channel-type.amazonechocontrol.bluetoothId.label = Bluetooth Verbinding +channel-type.amazonechocontrol.bluetoothId.label = Bluetooth Verbindung channel-type.amazonechocontrol.bluetoothId.description = MAC-Adresse des verbundenen Bluetoothgerätes -channel-type.amazonechocontrol.bluetoothIdSelection.label = Bluetooth Verbindingungsauswahl -channel-type.amazonechocontrol.bluetoothIdSelection.description = Bluetooth Verbindingungsauswahl (Derzeit nur in PaperUI) +channel-type.amazonechocontrol.bluetoothIdSelection.label = Bluetooth Verbindungungsauswahl +channel-type.amazonechocontrol.bluetoothIdSelection.description = Bluetooth Verbindungungsauswahl (Derzeit nur in PaperUI) channel-type.amazonechocontrol.remind.label = Erinnere channel-type.amazonechocontrol.remind.description = Spricht die Erinnerung und schickt eine Benachricht an die Alexa-APP (Nur schreiben) @@ -83,6 +101,18 @@ channel-type.amazonechocontrol.remind.description = Spricht die Erinnerung und s channel-type.amazonechocontrol.playAlarmSound.label = Spielt Alarm Sound channel-type.amazonechocontrol.playAlarmSound.description = Spielt Alarm Sound ab (Nur schreiben) +channel-type.amazonechocontrol.playFlashBriefing.label = Tägliche Zusammenfassung +channel-type.amazonechocontrol.playFlashBriefing.description = Startet die tägliche Zusammenfassung (Nur schreiben) + +channel-type.amazonechocontrol.playWeatherReport.label = Wetterbericht +channel-type.amazonechocontrol.playWeatherReport.description = Startet den Wetterbericht (Nur schreiben) + +channel-type.amazonechocontrol.playTrafficNews.label = Verkehrsnachrichten +channel-type.amazonechocontrol.playTrafficNews.description = Started die Verkehrsnachrichten (Nur schreiben) + +channel-type.amazonechocontrol.startRoutine.label = Started eine Routine +channel-type.amazonechocontrol.startRoutine.description = Tippen sie ein, was Sie normalerweise zu Alexa sagen um eine Routine zu starten, ohne "Alexa" vorangestellt (Nur schreiben) + channel-type.amazonechocontrol.imageUrl.label = Bild URL channel-type.amazonechocontrol.imageUrl.description = URL des Album Covers oder des Radiostations Logos @@ -98,7 +128,7 @@ channel-type.amazonechocontrol.subtitle2.description = Untertitel 2 channel-type.amazonechocontrol.radio.label = TuneIn Radio channel-type.amazonechocontrol.radio.description = Radio eingestalten -channel-type.amazonechocontrol.bluetooth.label = Bluetooth Verbinding +channel-type.amazonechocontrol.bluetooth.label = Bluetooth Verbindung channel-type.amazonechocontrol.bluetooth.description = Verbindet zum letzten benutzten Bluetooth Gerätes channel-type.amazonechocontrol.loop.label = Wiederholung @@ -113,5 +143,21 @@ channel-type.amazonechocontrol.player.description = Musikwiedergabe channel-type.amazonechocontrol.volume.label = Lautstärke channel-type.amazonechocontrol.volume.description = Wiedergabelautstärke +channel-type.amazonechocontrol.save.label = Speichern +channel-type.amazonechocontrol.save.description = Speichert die aktuelle tägliche Zusammenfassung Konfiguration (Nur schreiben) + +channel-type.amazonechocontrol.active.label = Aktiv +channel-type.amazonechocontrol.active.description = Aktiviert diese tägliche Zusammenfassung Konfiguration + +channel-type.amazonechocontrol.playOnDevice.label = Wiedergabe am Gerät +channel-type.amazonechocontrol.playOnDevice.description = Started die Wiedergabe am Gerät (Seriennummer oder Name, nur schreiben) + +channel-type.amazonechocontrol.switch.label = Schalter +channel-type.amazonechocontrol.switch.description = Schaltet das Gerät ein oder aus + +channel-type.amazonechocontrol.dimmer.label = Dimmer +channel-type.amazonechocontrol.dimmer.description = Dimmer Steuerung + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 23337c4296377..d58d2ce36fd69 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -42,8 +42,13 @@ Refresh state interval in seconds. Lower time causes more network traffic. Seconds - - + @@ -77,6 +82,10 @@ + + + + serialNumber @@ -119,6 +128,10 @@ + + + + serialNumber @@ -132,7 +145,7 @@ - + @@ -161,6 +174,10 @@ + + + + serialNumber @@ -238,6 +255,10 @@ + + + + serialNumber @@ -249,7 +270,101 @@ + + + + + + + + + Store and load a flash briefing configuration + + + + + + + + + + + + + + + + Smart Home Switch + + + + + + entityId + + + + + Use the search feature of openHAB to get the id + + + + + + + + + + + + Smart Home Dimmer + + + + + + + entityId + + + + + Let discover the device to get the id + + + + + + + Switch + + Save the current flash briefing configuration (Write only) + + + + Switch + + Activate this flash briefing configuration + + + + String + + Plays the briefing on the device (serial number or name, write only) + + + + Switch + + Turns the device on or off + + + + Dimmer + + Dimmer control + String @@ -267,13 +382,37 @@ String - Speak the reminder and send a notification to the Alexa app (Write Only) + Speak the reminder and send a notification to the Alexa app + + + + Switch + + Starts the flash briefing (Write Only) + + + + Switch + + Starts the weather report (Write Only) + + + + Switch + + Starts the traffic news (Write Only) + + + + String + + Type in what you normally say to Alexa without the preceding "Alexa," (Write Only) String - Plays an alarm sound (Write Only) + Plays an alarm sound diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index be455b3d31d83..dc01d15aa08b2 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -15,6 +15,11 @@ The binding provide features to control and view the current state of echo dot d - show album art image in sitemap - speak a reminder message - plays an alarm sound +- start traffic news +- start daily briefing +- start weather report +- start automation routine +- have multiple configurations of flash briefings Some ideas what you can do in your home by using rules and other openHAB controlled devices: @@ -22,7 +27,11 @@ Some ideas what you can do in your home by using rules and other openHAB control - If the amplifier was turned of, the echo stop playing and disconnect the bluetooth - The echo starts playing radio if the light was turned on - The echo starts playing radio at specified time -- Remind you we a voice message, that a window is open for a long time and it is winter +- Remind you with a voice message, that a window is open for a long time and it is winter +- Start a routine which welcome you, if you come home +- Start a routine which switch a smart home device connected to alexa +- Start your briefing if you turn on the light first time in the morning +- Have different flash briefing in the morning and evening ## Note ## @@ -41,16 +50,17 @@ All the display options are updated by polling the amazon server. The polling ti ## Supported Things -| Thing type id | Name | -|---------------------|---------------------------------------| -| account | Amazon Account | -| echo | Amazon Echo Device | -| echospot | Amazon Echo Spot Device | -| echoshow | Amazon Echo Show Device | -| wha | Amazon Echo Whole House Audio Control | -| unknown | Unknown Echo Device or App | +| Thing type id | Name | +|----------------------|---------------------------------------| +| account | Amazon Account | +| echo | Amazon Echo Device | +| echospot | Amazon Echo Spot Device | +| echoshow | Amazon Echo Show Device | +| wha | Amazon Echo Whole House Audio Control | +| flashbriefingprofile | Flash briefing profile | +| unknown | Unknown Echo Device or App\* | -The unknown device will provide all channels, but maybe not all of them supported by your device. +\* The unknown device will provide all channels, but maybe not all of them supported by your device. ## Discovery @@ -73,7 +83,9 @@ The Amazon Account device need the following configurations: 2 factor authentication is not supported! -All Amazon devices needs the following configurations: +### Amazon devices + +All Amazon devices (echo, echospot, echoshow, wha, unknown) needs the following configurations: | Configuration name | Description | |--------------------------|----------------------------------------------------| @@ -81,6 +93,10 @@ All Amazon devices needs the following configurations: You will find the serial number in the alexa app. +### Flash briefing profile + +The flashbriefingprofile thing 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. + ## Channels | Channel Type ID | Item Type | Access Mode | Thing Type | Description @@ -103,25 +119,33 @@ You will find the serial number in the alexa app. | amazonMusicPlayListId | String | W | echo, echoshow, echospot, wha, unknown | Write Only! Start playing of a Amazon Music playlist by specifying it's id od stops playing if a empty string was provided. Selection will only work in PaperUI | amazonMusicPlayListIdLastUsed | String | R | echo, echoshow, echospot, wha, unknown | The last play list id started from openHAB | amazonMusic | Switch | R/W | echo, echoshow, echospot, wha, unknown | Start playing of the last used Amazon Music song (works after at least one song was started after the openhab start) -| remind | String | W | echo, echoshow, echospot, unknown | Write Only! Speak the reminder and sends a notification to the Alexa app (Currently the reminder is played and notified two times, this seems to be a bug in the amazon software) -| playAlarmSound | String | W | echo, echoshow, echospot, unknown | Write Only! Plays an alarm sound. In PaperUI will be a selection box available. For rules use the value shown in the square brackets +| remind | String | R/W | echo, echoshow, echospot, unknown | Write Only! Speak the reminder and sends a notification to the Alexa app (Currently the reminder is played and notified two times, this seems to be a bug in the amazon software) +| playAlarmSound | String | R/W | echo, echoshow, echospot, unknown | Write Only! Plays an alarm sound. In PaperUI will be a selection box available. For rules use the value shown in the square brackets +| playFlashBriefing | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the flash briefing +| playWeatherReport | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the weather report +| playTrafficNews | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the traffic news +| startRoutine | Switch | W | echo, echoshow, echospot, unknown | Write Only! Type in what you normally say to Alexa without the preceding "Alexa," +| save | Switch | W | flashbriefingprofile | Write Only! 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. ## Full Example ### amzonechocontrol.things ``` -Bridge amazonechocontrol:account:account1 [amazonSite="amazon.de", email="myaccountemail@myprovider.com", password="secure", pollingIntervalInSeconds=60] +Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" [amazonSite="amazon.de", email="myaccountemail@myprovider.com", password="secure", pollingIntervalInSeconds=60] { - Thing echo echo1 "Alexa" @ "Living Room" [serialNumber="SERIAL_NUMBER"] - Thing echoshow echo2 "Alexa" @ "Kitchen" [serialNumber="SERIAL_NUMBER"] - Thing echospot echo3 "Alexa" @ "Sleeping Room" [serialNumber="SERIAL_NUMBER"] - Thing wha echo4 "Alexa" @ "Ground Floor Music Group" [serialNumber="SERIAL_NUMBER"] - Thing unknown echo4 "Alexa" @ "Very new echo device" [serialNumber="SERIAL_NUMBER"] + Thing echo echo1 "Alexa" @ "Living Room" [serialNumber="SERIAL_NUMBER"] + Thing echoshow echo2 "Alexa" @ "Kitchen" [serialNumber="SERIAL_NUMBER"] + Thing echospot echo3 "Alexa" @ "Sleeping Room" [serialNumber="SERIAL_NUMBER"] + Thing wha echo4 "Alexa" @ "Ground Floor Music Group" [serialNumber="SERIAL_NUMBER"] + Thing unknown echo5 "Alexa" @ "Very new echo device" [serialNumber="SERIAL_NUMBER"] + Thing flashbriefingprofile flashbriefing1 "Flash Briefing" @ "Flash Briefings" } ``` -You will find the serial number in the Alexa app. +You will find the serial number in the Alexa app. ### amzonechocontrol.items: @@ -150,6 +174,14 @@ String Echo_Living_Room_AmazonMusicPlayListIdLastUsed "Amazon Music Playlist Id Switch Echo_Living_Room_AmazonMusic "Amazon Music" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusic"} 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"} +Switch Echo_Living_Room_PlayFlashBriefing "Play Flash Briefing" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playFlashBriefing"} +Switch Echo_Living_Room_PlayWeatherReport "Play Weather Report" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playWeatherReport"} +Switch Echo_Living_Room_PlayTrafficNews "Play Traffic News" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playTrafficNews"} +String Echo_Living_Room_StartRoutine "Start Routine" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:startRoutine"} + +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"} ``` ### amzonechocontrol.sitemap: @@ -177,6 +209,16 @@ sitemap amzonechocontrol label="Echo Devices" Switch item=Echo_Living_Room_AmazonMusic Text item=Echo_Living_Room_Remind Text item=Echo_Living_Room_PlayAlarmSound + Switch item=Echo_Living_Room_PlayFlashBriefing + Switch item=Echo_Living_Room_PlayWeatherReport + Switch item=Echo_Living_Room_PlayTrafficNews + Text item=Echo_Living_Room_StartRoutine + } + + Frame label="Flash Briefing 1" { + Switch item=FlashBriefing_Technical_Save + Switch item=FlashBriefing_Technical_Active + Text item=FlashBriefing_Technical_Play } } ``` diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java index 8cbf1f71ed0b7..a22d535d42c9d 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java @@ -35,8 +35,15 @@ public class AmazonEchoControlBindingConstants { public static final ThingTypeUID THING_TYPE_ECHO_WHA = new ThingTypeUID(BINDING_ID, "wha"); public static final ThingTypeUID THING_TYPE_UNKNOWN = new ThingTypeUID(BINDING_ID, "unknown"); + public static final ThingTypeUID THING_TYPE_FLASH_BRIEFING_PROFILE = new ThingTypeUID(BINDING_ID, + "flashbriefingprofile"); + + public static final ThingTypeUID THING_TYPE_SMART_HOME_SWITCH = new ThingTypeUID(BINDING_ID, "smarthomeswitch"); + public static final ThingTypeUID THING_TYPE_SMART_HOME_DIMMER = new ThingTypeUID(BINDING_ID, "smarthomedimmer"); + public static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet(Arrays.asList( - THING_TYPE_ACCOUNT, THING_TYPE_ECHO, THING_TYPE_ECHO_SPOT, THING_TYPE_ECHO_WHA, THING_TYPE_UNKNOWN)); + THING_TYPE_ACCOUNT, THING_TYPE_ECHO, THING_TYPE_ECHO_SPOT, THING_TYPE_ECHO_WHA, THING_TYPE_UNKNOWN, + THING_TYPE_SMART_HOME_SWITCH, THING_TYPE_SMART_HOME_DIMMER, THING_TYPE_FLASH_BRIEFING_PROFILE)); // List of all Channel ids public static final String CHANNEL_PLAYER = "player"; @@ -61,6 +68,17 @@ public class AmazonEchoControlBindingConstants { public static final String CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID_LAST_USED = "amazonMusicPlayListIdLastUsed"; public static final String CHANNEL_REMIND = "remind"; public static final String CHANNEL_PLAY_ALARM_SOUND = "playAlarmSound"; + public static final String CHANNEL_PLAY_FLASH_BRIEFING = "playFlashBriefing"; + public static final String CHANNEL_PLAY_WEATER_REPORT = "playWeatherReport"; + public static final String CHANNEL_PLAY_TRAFFIC_NEWS = "playTrafficNews"; + public static final String CHANNEL_START_ROUTINE = "startRoutine"; + + public static final String CHANNEL_SAVE = "save"; + public static final String CHANNEL_ACTIVE = "active"; + public static final String CHANNEL_PLAY_ON_DEVICE = "playOnDevice"; + + public static final String CHANNEL_SWITCH = "switch"; + public static final String CHANNEL_DIMMER = "dimmer"; // List of channel Type UIDs public static final ChannelTypeUID CHANNEL_TYPE_BLUETHOOTH_ID_SELECTION = new ChannelTypeUID(BINDING_ID, @@ -69,8 +87,15 @@ public class AmazonEchoControlBindingConstants { "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"); + // 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_ENTITY_ID = "entityId"; + + public static final String DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE = "configurationJson"; + } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index 0a16892b3b134..dfb0aa87d99bb 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -30,14 +30,19 @@ import org.openhab.binding.amazonechocontrol.internal.AccountConfiguration; import org.openhab.binding.amazonechocontrol.internal.Connection; import org.openhab.binding.amazonechocontrol.internal.ConnectionException; +import org.openhab.binding.amazonechocontrol.internal.StateStorage; import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; import org.openhab.binding.amazonechocontrol.internal.discovery.IAmazonEchoDiscovery; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevice; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.Gson; + /** * Handles the connection to the amazon server. * @@ -46,20 +51,39 @@ public class AccountHandler extends BaseBridgeHandler implements IAmazonEchoDiscovery { private final Logger logger = LoggerFactory.getLogger(AccountHandler.class); + private StateStorage stateStorage; private AccountConfiguration config; private Connection connection; - private List childs = new ArrayList<>(); + private List echoHandlers = new ArrayList<>(); + private List smartHomeHandlers = new ArrayList<>(); + private List flashBriefingProfileHandlers = new ArrayList<>(); private Object synchronizeConnection = new Object(); private Map jsonSerialNumberDeviceMapping = new HashMap<>(); private ScheduledFuture refreshJob; private ScheduledFuture refreshLogin; + private boolean updateSmartHomeDeviceList; + private boolean discoverFlashProfiles; + private boolean smartHodeDeviceListEnabled; + private String currentFlashBriefingJson = ""; public AccountHandler(@NonNull Bridge bridge) { super(bridge); + + stateStorage = new StateStorage(bridge); AmazonEchoDiscovery.setHandlerExist(); } + public Device[] getLastKnownDevices() { + Map temp = jsonSerialNumberDeviceMapping; + if (temp == null) { + return new Device[0]; + } + Device[] devices = new Device[temp.size()]; + temp.values().toArray(devices); + return devices; + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { logger.trace("Command '{}' received for channel '{}'", command, channelUID); @@ -71,26 +95,53 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void initialize() { + start(); } @Override public void childHandlerInitialized(@NonNull ThingHandler childHandler, @NonNull Thing childThing) { super.childHandlerInitialized(childHandler, childThing); + if (childHandler instanceof EchoHandler) { EchoHandler echoHandler = (EchoHandler) childHandler; - synchronized (childs) { - childs.add(echoHandler); + synchronized (echoHandlers) { + echoHandlers.add(echoHandler); + } + Connection temp = connection; + if (temp != null) { + initializeEchoHandler(echoHandler, temp); + } - Connection temp = connection; - if (temp != null) { - initializeChild(echoHandler, temp); + } + if (childHandler instanceof FlashBriefingProfileHandler) { + FlashBriefingProfileHandler flashBriefingProfileHandler = (FlashBriefingProfileHandler) childHandler; + synchronized (flashBriefingProfileHandlers) { + flashBriefingProfileHandlers.add(flashBriefingProfileHandler); + } + Connection temp = connection; + if (temp != null) { + if (currentFlashBriefingJson.isEmpty()) { + updateFlashBriefingProfiles(temp); } + + flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson); } } + if (childHandler instanceof SmartHomeBaseHandler) { + SmartHomeBaseHandler smartHomeHandler = (SmartHomeBaseHandler) childHandler; + synchronized (smartHomeHandlers) { + smartHomeHandlers.add(smartHomeHandler); + } + Connection temp = connection; + if (temp != null) { + smartHomeHandler.initialize(temp); + } + } + } - private void initializeChild(@NonNull EchoHandler echoHandler, @NonNull Connection temp) { + private void initializeEchoHandler(@NonNull EchoHandler echoHandler, @NonNull Connection temp) { intializeChildDevice(temp, echoHandler); Device device = findDeviceJson(echoHandler); @@ -114,13 +165,28 @@ private void initializeChild(@NonNull EchoHandler echoHandler, @NonNull Connecti @Override public void childHandlerDisposed(@NonNull ThingHandler childHandler, @NonNull Thing childThing) { if (childHandler instanceof EchoHandler) { - synchronized (childs) { - childs.remove(childHandler); + synchronized (echoHandlers) { + echoHandlers.remove(childHandler); + } + + AmazonEchoDiscovery instance = AmazonEchoDiscovery.instance; + if (instance != null) { + instance.removeExistingEchoHandler(childThing.getUID()); + } + } + if (childHandler instanceof FlashBriefingProfileHandler) { + synchronized (flashBriefingProfileHandlers) { + flashBriefingProfileHandlers.remove(childHandler); + } + } + if (childHandler instanceof SmartHomeBaseHandler) { + synchronized (smartHomeHandlers) { + smartHomeHandlers.remove(childHandler); } AmazonEchoDiscovery instance = AmazonEchoDiscovery.instance; if (instance != null) { - instance.removeExisting(childThing.getUID()); + instance.removeExistingSmartHomeHandler(childThing.getUID()); } } super.childHandlerDisposed(childHandler, childThing); @@ -179,6 +245,15 @@ private void start() { cleanup(); return; } + if (config.discoverSmartHomeDevices != null && config.discoverSmartHomeDevices) { + if (!smartHodeDeviceListEnabled) { + updateSmartHomeDeviceList = true; + } + smartHodeDeviceListEnabled = true; + } else { + smartHodeDeviceListEnabled = false; + } + if (config.pollingIntervalInSeconds < 10) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Polling interval less than 10 seconds not allowed"); @@ -193,10 +268,16 @@ private void start() { } } updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login"); + if (refreshLogin != null) { + refreshLogin.cancel(false); + } refreshLogin = scheduler.scheduleWithFixedDelay(() -> { checkLogin(); }, 0, 60, TimeUnit.SECONDS); + if (refreshJob != null) { + refreshJob.cancel(false); + } refreshJob = scheduler.scheduleWithFixedDelay(() -> { refreshData(); }, 4, config.pollingIntervalInSeconds, TimeUnit.SECONDS); @@ -229,7 +310,7 @@ private void checkLogin() { if (loginTime != null && currentTime - loginTime.getTime() > 86400000 * 5) // 5 days { // Recreate session - this.updateProperty("sessionStorage", ""); + this.stateStorage.storeState("sessionStorage", ""); temp = new Connection(temp.getEmail(), temp.getPassword(), temp.getAmazonSite()); } boolean loginIsValid = true; @@ -237,7 +318,7 @@ private void checkLogin() { try { // read session data from property - String sessionStore = this.thing.getProperties().get("sessionStorage"); + String sessionStore = this.stateStorage.findState("sessionStorage"); // try use the session data if (!temp.tryRestoreLogin(sessionStore)) { @@ -263,8 +344,9 @@ private void checkLogin() { if (serializedStorage == null) { serializedStorage = ""; } - this.updateProperty("sessionStorage", serializedStorage); + this.stateStorage.storeState("sessionStorage", serializedStorage); } + updateSmartHomeDeviceList = true; connection = temp; } catch (UnknownHostException e) { loginIsValid = false; @@ -276,7 +358,7 @@ private void checkLogin() { } if (loginIsValid) { // update the device list - updateDeviceList(); + updateDeviceList(false); updateStatus(ThingStatus.ONLINE); AmazonEchoDiscovery.addDiscoveryHandler(this); } @@ -300,28 +382,27 @@ private void refreshData() { if (temp == null) { return; } - synchronized (childs) { - updateDeviceList(); + updateDeviceList(false); - JsonBluetoothStates states = null; - if (temp.getIsLoggedIn()) { - try { - states = temp.getBluetoothConnectionStates(); - } catch (Exception e) { - logger.info("getBluetoothConnectionStates failed: {}", e); - } + JsonBluetoothStates states = null; + if (temp.getIsLoggedIn()) { + try { + states = temp.getBluetoothConnectionStates(); + } catch (Exception e) { + logger.info("getBluetoothConnectionStates failed: {}", e); } + } - for (EchoHandler child : childs) { - Device device = findDeviceJson(child); - BluetoothState state = null; - if (states != null) { - state = states.findStateByDevice(device); - } - child.updateState(device, state); + for (EchoHandler child : echoHandlers) { + Device device = findDeviceJson(child); + BluetoothState state = null; + if (states != null) { + state = states.findStateByDevice(device); } + child.updateState(device, state); } + updateStatus(ThingStatus.ONLINE); } catch (Exception e) { @@ -332,6 +413,10 @@ private void refreshData() { public Device findDeviceJson(EchoHandler echoHandler) { String serialNumber = echoHandler.findSerialNumber(); + return findDeviceJson(serialNumber); + } + + public Device findDeviceJson(String serialNumber) { Device result = null; if (!serialNumber.isEmpty()) { Map temp = jsonSerialNumberDeviceMapping; @@ -343,12 +428,36 @@ public Device findDeviceJson(EchoHandler echoHandler) { return result; } + public Device findDeviceJsonBySerialOrName(String serialOrName) { + if (!serialOrName.isEmpty()) { + String serialOrNameLowerCase = serialOrName.toLowerCase(); + Map temp = jsonSerialNumberDeviceMapping; + for (Device device : temp.values()) { + if (device.serialNumber != null && device.serialNumber.toLowerCase().equals(serialOrNameLowerCase)) { + return device; + } + } + for (Device device : temp.values()) { + if (device.accountName != null && device.accountName.toLowerCase().equals(serialOrNameLowerCase)) { + return device; + } + } + } + return null; + } + @Override - synchronized public void updateDeviceList() { + public void updateDeviceList(boolean manualScan) { + if (manualScan) { + updateSmartHomeDeviceList = true; + discoverFlashProfiles = true; + } + Connection temp = connection; if (temp == null) { return; } + AmazonEchoDiscovery discoveryService = AmazonEchoDiscovery.instance; Device[] devices = null; try { @@ -365,18 +474,119 @@ synchronized public void updateDeviceList() { } jsonSerialNumberDeviceMapping = newJsonSerialDeviceMapping; - AmazonEchoDiscovery discoveryService = AmazonEchoDiscovery.instance; if (discoveryService != null) { discoveryService.setDevices(getThing().getUID(), devices); } } - synchronized (childs) { - for (EchoHandler child : childs) { + synchronized (echoHandlers) { + for (EchoHandler child : echoHandlers) { if (child != null) { - initializeChild(child, temp); + initializeEchoHandler(child, temp); } } } + synchronized (smartHomeHandlers) { + for (SmartHomeBaseHandler child : smartHomeHandlers) { + if (child != null) { + child.initialize(temp); + } + } + } + updateFlashBriefingHandlers(temp); + + if (discoveryService != null && updateSmartHomeDeviceList && smartHodeDeviceListEnabled) { + updateSmartHomeDeviceList = false; + List smartHomeDevices = null; + try { + smartHomeDevices = temp.getSmartHomeDevices(); + } catch (Exception e) { + logger.warn("Update smart home list failed {}", e); + } + if (smartHomeDevices != null) { + discoveryService.setSmartHomeDevices(getThing().getUID(), smartHomeDevices); + } + } + + } + + public void setEnabledFlashBriefingsJson(String flashBriefingJson) { + Connection temp = connection; + Gson gson = new Gson(); + JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class); + if (temp != null) { + try { + temp.setEnabledFlashBriefings(feeds); + } catch (Exception e) { + logger.warn("Set flashbriefing profile failed {}", e); + } + } + updateFlashBriefingHandlers(); + } + + public void updateFlashBriefingHandlers() { + Connection temp = connection; + if (temp != null) { + updateFlashBriefingHandlers(temp); + } + } + + private void updateFlashBriefingHandlers(Connection temp) { + synchronized (smartHomeHandlers) { + if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) { + updateFlashBriefingProfiles(temp); + } + + for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) { + if (child != null) { + child.initialize(this, currentFlashBriefingJson); + } + } + if (flashBriefingProfileHandlers.isEmpty()) { + discoverFlashProfiles = true; // discover at least one device + } + AmazonEchoDiscovery discoveryService = AmazonEchoDiscovery.instance; + if (discoveryService != null) { + if (discoverFlashProfiles) { + discoverFlashProfiles = false; + discoveryService.discoverFlashBriefingProfiles(getThing().getUID(), this.currentFlashBriefingJson); + } + } + } + } + + public Connection findConnection() { + return this.connection; + } + + public String getEnabledFlashBriefingsJson() { + Connection temp = this.connection; + if (temp == null) { + return ""; + } + updateFlashBriefingProfiles(temp); + return this.currentFlashBriefingJson; + } + + private void updateFlashBriefingProfiles(Connection temp) { + try { + JsonFeed[] feeds = temp.getEnabledFlashBriefings(); + // Make a copy and remove changeable parts + JsonFeed[] forSerializer = new JsonFeed[feeds.length]; + for (int i = 0; i < feeds.length; i++) { + JsonFeed source = feeds[i]; + JsonFeed copy = new JsonFeed(); + copy.feedId = source.feedId; + copy.skillId = source.skillId; + // Do not copy imageUrl here, because it will change + forSerializer[i] = copy; + } + Gson gson = new Gson(); + this.currentFlashBriefingJson = gson.toJson(forSerializer); + + } catch (Exception e) { + logger.warn("get flash briefing profiles fails {}", e); + } + } private void intializeChildDevice(@NonNull Connection connection, @NonNull EchoHandler child) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index e6073658c11c3..64358a41d1dba 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -39,6 +39,7 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState.QueueEntry; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState.PlayerInfo; @@ -71,6 +72,9 @@ public class EchoHandler extends BaseThingHandler { private boolean disableUpdate = false; private boolean updateRemind = true; private boolean updateAlarm = true; + private boolean updateRoutine = true; + private @Nullable JsonNotificationResponse currentNotification; + private @Nullable ScheduledFuture currentNotifcationUpdateTimer; public EchoHandler(Thing thing) { super(thing); @@ -95,6 +99,7 @@ public void dispose() { synchronized (instances) { instances.remove(this.getThing().getUID()); } + stopCurrentNotification(); ScheduledFuture updateStateJob = this.updateStateJob; this.updateStateJob = null; if (updateStateJob != null) { @@ -135,9 +140,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { int waitForUpdate = 1000; boolean needBluetoothRefresh = false; - ScheduledFuture updateStateJob = this.updateStateJob; String lastKnownBluetoothId = this.lastKnownBluetoothId; + ScheduledFuture updateStateJob = this.updateStateJob; this.updateStateJob = null; if (updateStateJob != null) { updateStateJob.cancel(false); @@ -285,7 +290,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { // radio commands if (channelId.equals(CHANNEL_RADIO_STATION_ID)) { if (command instanceof StringType) { - String stationId = ((StringType) command).toFullString(); if (stationId != null && !stationId.isEmpty()) { waitForUpdate = 3000; @@ -303,23 +307,27 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else if (command == OnOffType.OFF) { temp.playRadio(device, ""); } - } // notification if (channelId.equals(CHANNEL_REMIND)) { if (command instanceof StringType) { + stopCurrentNotification(); String reminder = ((StringType) command).toFullString(); if (reminder != null && !reminder.isEmpty()) { waitForUpdate = 3000; updateRemind = true; - temp.notification(device, "Reminder", reminder, null); + currentNotification = temp.notification(device, "Reminder", reminder, null); + currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> { + updateNotificationTimerState(); + }, 1, 1, TimeUnit.SECONDS); } } } if (channelId.equals(CHANNEL_PLAY_ALARM_SOUND)) { if (command instanceof StringType) { + stopCurrentNotification(); String alarmSound = ((StringType) command).toFullString(); if (alarmSound != null && !alarmSound.isEmpty()) { waitForUpdate = 3000; @@ -333,10 +341,49 @@ public void handleCommand(ChannelUID channelUID, Command command) { sound.providerId = "ECHO"; sound.id = alarmSound; } - temp.notification(device, "Alarm", null, sound); + currentNotification = temp.notification(device, "Alarm", null, sound); + currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> { + updateNotificationTimerState(); + }, 1, 1, TimeUnit.SECONDS); + } } } + + // routine commands + if (channelId.equals(CHANNEL_PLAY_FLASH_BRIEFING)) { + + if (command == OnOffType.ON) { + waitForUpdate = 1000; + temp.executeSequenceCommand(device, "Alexa.FlashBriefing.Play"); + } + } + if (channelId.equals(CHANNEL_PLAY_TRAFFIC_NEWS)) { + + if (command == OnOffType.ON) { + waitForUpdate = 1000; + temp.executeSequenceCommand(device, "Alexa.Traffic.Play"); + } + } + if (channelId.equals(CHANNEL_PLAY_WEATER_REPORT)) { + + if (command == OnOffType.ON) { + waitForUpdate = 1000; + temp.executeSequenceCommand(device, "Alexa.Weather.Play"); + } + } + + if (channelId.equals(CHANNEL_START_ROUTINE)) { + if (command instanceof StringType) { + String utterance = ((StringType) command).toFullString(); + if (utterance != null && !utterance.isEmpty()) { + waitForUpdate = 1000; + updateRoutine = true; + temp.startRoutine(device, utterance); + } + } + } + // force update of the state this.disableUpdate = true; final boolean bluetoothRefresh = needBluetoothRefresh; @@ -369,6 +416,57 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } + private void stopCurrentNotification() { + ScheduledFuture tempCurrentNotifcationUpdateTimer = currentNotifcationUpdateTimer; + if (tempCurrentNotifcationUpdateTimer != null) { + currentNotifcationUpdateTimer = null; + tempCurrentNotifcationUpdateTimer.cancel(true); + } + JsonNotificationResponse tempCurrentNotification = currentNotification; + if (tempCurrentNotification != null) { + currentNotification = null; + Connection tempConnection = this.connection; + if (tempConnection != null) { + try { + tempConnection.stopNotification(tempCurrentNotification); + } catch (Exception e) { + logger.warn("Stop notification failed: {}", e); + } + } + } + } + + private void updateNotificationTimerState() { + boolean stopCurrentNotifcation = true; + JsonNotificationResponse tempCurrentNotification = currentNotification; + try { + if (tempCurrentNotification != null) { + Connection tempConnection = connection; + if (tempConnection != null) { + JsonNotificationResponse newState = tempConnection.getNotificationState(tempCurrentNotification); + if (newState.status != null && newState.status.equals("ON")) { + stopCurrentNotifcation = false; + } + } + } + } catch (Exception e) { + logger.warn("update notification state fails: {}", e); + } + if (stopCurrentNotifcation) { + if (tempCurrentNotification != null && tempCurrentNotification.type != null) { + if (tempCurrentNotification.type.equals("Reminder")) { + updateState(CHANNEL_REMIND, new StringType("")); + updateRemind = false; + } + if (tempCurrentNotification.type.equals("Alarm")) { + updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType("")); + updateAlarm = false; + } + } + stopCurrentNotification(); + } + } + public void updateState(@Nullable Device device, @Nullable BluetoothState bluetoothState) { if (this.disableUpdate) { return; @@ -552,12 +650,21 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } // Update states - if (updateRemind) { + if (updateRemind && currentNotifcationUpdateTimer == null) { + updateRemind = false; updateState(CHANNEL_REMIND, new StringType("")); } - if (updateAlarm) { + if (updateAlarm && currentNotifcationUpdateTimer == null) { + updateAlarm = false; updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType("")); } + if (updateRoutine) { + updateRoutine = false; + updateState(CHANNEL_START_ROUTINE, new StringType("")); + } + updateState(CHANNEL_PLAY_FLASH_BRIEFING, OnOffType.OFF); + updateState(CHANNEL_PLAY_WEATER_REPORT, OnOffType.OFF); + updateState(CHANNEL_PLAY_TRAFFIC_NEWS, OnOffType.OFF); updateState(CHANNEL_AMAZON_MUSIC_TRACK_ID, new StringType(amazonMusicTrackId)); updateState(CHANNEL_AMAZON_MUSIC, playing && amazonMusic ? OnOffType.ON : OnOffType.OFF); updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID, new StringType(amazonMusicPlayListId)); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java new file mode 100644 index 0000000000000..39be9bc1348ec --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.handler; + +import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; + +import java.util.HashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.openhab.binding.amazonechocontrol.internal.StateStorage; +import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link FlashBriefingProfileHandler} is responsible for storing and loading of a flash briefing configuration + * + * @author Michael Geramb - Initial contribution + */ +public class FlashBriefingProfileHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(FlashBriefingProfileHandler.class); + private static HashMap instances = new HashMap(); + + AccountHandler handler; + StateStorage stateStorage; + boolean updatePlayOnDevice = true; + String currentConfigurationJson = ""; + private @Nullable ScheduledFuture updateStateJob; + + public FlashBriefingProfileHandler(Thing thing) { + super(thing); + stateStorage = new StateStorage(thing); + } + + public AccountHandler findAccountHandler() { + return this.handler; + } + + public static @Nullable FlashBriefingProfileHandler find(ThingUID uid) { + synchronized (instances) { + return instances.get(uid); + } + } + + public static boolean exist(String profileJson) { + synchronized (instances) { + for (FlashBriefingProfileHandler handler : instances.values()) { + if (handler.currentConfigurationJson.equals(profileJson)) { + return true; + } + } + } + return false; + } + + @Override + public void initialize() { + updatePlayOnDevice = true; + logger.info(getClass().getSimpleName() + " initialized"); + synchronized (instances) { + instances.put(this.getThing().getUID(), this); + } + if (this.currentConfigurationJson != null && !this.currentConfigurationJson.isEmpty()) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE); + } + } + + @Override + public void dispose() { + synchronized (instances) { + instances.remove(getThing().getUID(), this); + } + ScheduledFuture updateStateJob = this.updateStateJob; + this.updateStateJob = null; + if (updateStateJob != null) { + updateStateJob.cancel(false); + } + removeFromDiscovery(); + super.dispose(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + AccountHandler temp = this.handler; + if (temp == null) { + return; + } + int waitForUpdate = -1; + + ScheduledFuture updateStateJob = this.updateStateJob; + this.updateStateJob = null; + if (updateStateJob != null) { + updateStateJob.cancel(false); + } + + try { + String channelId = channelUID.getId(); + if (command instanceof RefreshType) { + waitForUpdate = 0; + } + + if (channelId.equals(CHANNEL_SAVE)) { + if (command.equals(OnOffType.ON)) { + saveCurrentProfile(temp); + waitForUpdate = 500; + } + } + if (channelId.equals(CHANNEL_ACTIVE)) { + if (command.equals(OnOffType.ON)) { + String currentConfigurationJson = this.currentConfigurationJson; + if (currentConfigurationJson != null && !currentConfigurationJson.isEmpty()) { + temp.setEnabledFlashBriefingsJson(currentConfigurationJson); + + updateState(CHANNEL_ACTIVE, OnOffType.ON); + waitForUpdate = 500; + } + } + } + if (channelId.equals(CHANNEL_PLAY_ON_DEVICE)) { + if (command instanceof StringType) { + String deviceSerialOrName = ((StringType) command).toFullString(); + String currentConfigurationJson = this.currentConfigurationJson; + if (currentConfigurationJson != null && !currentConfigurationJson.isEmpty()) { + + String old = temp.getEnabledFlashBriefingsJson(); + temp.setEnabledFlashBriefingsJson(currentConfigurationJson); + + Device device = temp.findDeviceJsonBySerialOrName(deviceSerialOrName); + if (device == null) { + logger.warn("Device '{}' not found", deviceSerialOrName); + } else { + temp.findConnection().executeSequenceCommand(device, "Alexa.FlashBriefing.Play"); + + scheduler.schedule(() -> temp.setEnabledFlashBriefingsJson(old), 1000, + TimeUnit.MILLISECONDS); + + updateState(CHANNEL_ACTIVE, OnOffType.ON); + } + updatePlayOnDevice = true; + waitForUpdate = 1000; + + } + } + } + } catch (Exception e) { + logger.warn("Handle command failed {}", e); + } + if (waitForUpdate >= 0) { + this.updateStateJob = scheduler.schedule(() -> temp.updateFlashBriefingHandlers(), waitForUpdate, + TimeUnit.MILLISECONDS); + } + } + + public void initialize(AccountHandler handler, String currentConfigurationJson) { + + updateState(CHANNEL_SAVE, OnOffType.OFF); + if (updatePlayOnDevice) { + updateState(CHANNEL_PLAY_ON_DEVICE, new StringType("")); + } + if (this.handler != handler) { + + this.handler = handler; + String configurationJson = this.stateStorage.findState("configurationJson"); + if (configurationJson == null || configurationJson.isEmpty()) { + this.currentConfigurationJson = saveCurrentProfile(handler); + + } else { + removeFromDiscovery(); + this.currentConfigurationJson = configurationJson; + } + if (this.currentConfigurationJson != null && !this.currentConfigurationJson.isEmpty()) { + updateStatus(ThingStatus.ONLINE); + + } else { + updateStatus(ThingStatus.UNKNOWN); + } + } + if (this.currentConfigurationJson.equals(currentConfigurationJson)) { + updateState(CHANNEL_ACTIVE, OnOffType.ON); + } else { + updateState(CHANNEL_ACTIVE, OnOffType.OFF); + } + + } + + private String saveCurrentProfile(AccountHandler connection) { + String configurationJson = ""; + try { + configurationJson = connection.getEnabledFlashBriefingsJson(); + removeFromDiscovery(); + this.currentConfigurationJson = configurationJson; + } catch (Exception e) { + logger.warn("get flash briefing configuration failed {}", e); + } + if (!configurationJson.isEmpty()) { + this.stateStorage.storeState("configurationJson", configurationJson); + } + return configurationJson; + } + + private void removeFromDiscovery() { + AmazonEchoDiscovery instance = AmazonEchoDiscovery.instance; + if (instance != null) { + instance.removeExistingFlashBriefingProfile(this.currentConfigurationJson); + } + } +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java new file mode 100644 index 0000000000000..b1a2acb7aa829 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.handler; + +import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.DEVICE_PROPERTY_ENTITY_ID; + +import java.util.HashMap; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SmartHomeBaseHandler} is the base class for all smart home devices provided by alexa skills + * + * @author Michael Geramb - Initial contribution + */ +public abstract class SmartHomeBaseHandler extends BaseThingHandler { + + private static HashMap instances = new HashMap(); + private final Logger logger = LoggerFactory.getLogger(SmartHomeDimmerHandler.class); + + private Connection connection; + + protected Connection findConnection() { + return this.connection; + } + + protected SmartHomeBaseHandler(Thing thing) { + super(thing); + + } + + @Override + public void initialize() { + logger.info(getClass().getSimpleName() + " initialized"); + synchronized (instances) { + instances.put(this.getThing().getUID(), this); + } + updateStatus(ThingStatus.ONLINE); + } + + public void initialize(@NonNull Connection connection) { + this.connection = connection; + } + + @Override + public void dispose() { + synchronized (instances) { + instances.remove(this.getThing().getUID()); + } + super.dispose(); + } + + private String findEntityId() { + String id = (String) getConfig().get(DEVICE_PROPERTY_ENTITY_ID); + if (id == null) { + return ""; + } + return id; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Connection temp = findConnection(); + if (temp == null) { + return; + } + String entityId = findEntityId(); + if (entityId.isEmpty()) { + return; + } + String channelId = channelUID.getId(); + try { + handleCommand(temp, entityId, channelId, command); + } catch (Exception e) { + logger.warn("handle command {} for {} failed: {}", command, channelUID, e); + } + } + + protected abstract void handleCommand(Connection connection, String entityId, String channelId, Command command) + throws Exception; + + public static @Nullable SmartHomeBaseHandler find(ThingUID uid) { + synchronized (instances) { + return instances.get(uid); + } + } + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java new file mode 100644 index 0000000000000..14d9fbdedff2e --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.handler; + +import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; + +import java.util.Locale; + +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.types.Command; +import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SmartHomeDimmerHandler} is responsible for the handling a smarthome dimmer device + * + * @author Michael Geramb - Initial contribution + */ +public class SmartHomeDimmerHandler extends SmartHomeBaseHandler { + + private final Logger logger = LoggerFactory.getLogger(SmartHomeDimmerHandler.class); + + public SmartHomeDimmerHandler(Thing thing) { + super(thing); + + } + + @Override + public void initialize() { + logger.info("SmartHomeDimmerHandler initialized"); + super.initialize(); + } + + @Override + public void handleCommand(Connection connection, String entityId, String channelId, Command command) + throws Exception { + + if (channelId.equals(CHANNEL_SWITCH) || channelId.equals(CHANNEL_DIMMER)) { + if (command == OnOffType.ON) { + connection.sendSmartHomeDeviceCommand(entityId, "turnOn", null, null); + updateState(CHANNEL_SWITCH, OnOffType.ON); + } + if (command == OnOffType.OFF) { + connection.sendSmartHomeDeviceCommand(entityId, "turnOff", null, null); + updateState(CHANNEL_SWITCH, OnOffType.OFF); + } + } + if (channelId.equals(CHANNEL_DIMMER)) { + if (command instanceof PercentType) { + PercentType value = (PercentType) command; + double percent = value.doubleValue(); + if (percent >= 0 && percent <= 100) { + String percentValue = String.format(Locale.ROOT, "%.2f", (percent / 100)); + connection.sendSmartHomeDeviceCommand(entityId, "setPercentage", "percentage", percentValue); + updateState(CHANNEL_DIMMER, value); + } + } + } + } + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java new file mode 100644 index 0000000000000..cac93b8542f68 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.handler; + +import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.CHANNEL_SWITCH; + +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.types.Command; +import org.openhab.binding.amazonechocontrol.internal.Connection; + +/** + * The {@link SmartHomeSwitchHandler} is responsible for the handling a smarthome switch device + * + * @author Michael Geramb - Initial contribution + */ +public class SmartHomeSwitchHandler extends SmartHomeBaseHandler { + + public SmartHomeSwitchHandler(Thing thing) { + super(thing); + + } + + @Override + public void handleCommand(Connection connection, String entityId, String channelId, Command command) + throws Exception { + + if (channelId.equals(CHANNEL_SWITCH)) { + if (command == OnOffType.ON) { + connection.sendSmartHomeDeviceCommand(entityId, "turnOn", null, null); + updateState(CHANNEL_SWITCH, OnOffType.ON); + } + if (command == OnOffType.OFF) { + connection.sendSmartHomeDeviceCommand(entityId, "turnOff", null, null); + updateState(CHANNEL_SWITCH, OnOffType.OFF); + } + } + } + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java index a7e2869c7f6ef..8af39325b12d6 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java @@ -15,10 +15,9 @@ * @author Michael Geramb - Initial Contribution */ public class AccountConfiguration { - public String email; public String password; public String amazonSite; public Integer pollingIntervalInSeconds; - + public Boolean discoverSmartHomeDevices; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index e498439cca3b5..3e6a4f0289c5f 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -20,6 +20,9 @@ import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; import org.openhab.binding.amazonechocontrol.handler.AccountHandler; import org.openhab.binding.amazonechocontrol.handler.EchoHandler; +import org.openhab.binding.amazonechocontrol.handler.FlashBriefingProfileHandler; +import org.openhab.binding.amazonechocontrol.handler.SmartHomeDimmerHandler; +import org.openhab.binding.amazonechocontrol.handler.SmartHomeSwitchHandler; import org.osgi.service.component.annotations.Component; /** @@ -45,6 +48,15 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { AccountHandler bridgeHandler = new AccountHandler((Bridge) thing); return bridgeHandler; } + if (thingTypeUID.equals(THING_TYPE_FLASH_BRIEFING_PROFILE)) { + return new FlashBriefingProfileHandler(thing); + } + if (thingTypeUID.equals(THING_TYPE_SMART_HOME_DIMMER)) { + return new SmartHomeDimmerHandler(thing); + } + if (thingTypeUID.equals(THING_TYPE_SMART_HOME_SWITCH)) { + return new SmartHomeSwitchHandler(thing); + } if (SUPPORTED_THING_TYPES_UIDS.contains(THING_TYPE_ECHO)) { return new EchoHandler(thing); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index f77da24f28dc3..d3c63a94e1dc7 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -18,6 +18,7 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; @@ -27,20 +28,28 @@ import javax.net.ssl.HttpsURLConnection; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAutomation; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonEnabledFeeds; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNetworkDetails; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationRequest; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSounds; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevice; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonStartRoutineRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; import com.google.gson.JsonSyntaxException; /** @@ -445,6 +454,7 @@ private String makeRequestAndReturnString(String verb, String url, String refere public boolean verifyLogin() throws Exception { String response = makeRequestAndReturnString(m_alexaServer + "/api/bootstrap?version=0"); Boolean result = response.contains("\"authenticated\":true"); + return result; } @@ -572,6 +582,100 @@ public void playAmazonMusicPlayList(Device device, String playListId) throws Exc } } + // commands: Alexa.Weather.Play, Alexa.Traffic.Play, Alexa.FlashBriefing.Play + public void executeSequenceCommand(Device device, String command) throws Exception { + String json = "{ \"behaviorId\": \"amzn1.alexa.automation.00000000-0000-0000-0000-000000000000\", " + + " \"sequenceJson\": \"{\\\"@type\\\":\\\"com.amazon.alexa.behaviors.model.Sequence\\\",\\\"startNode\\\":{\\\"@type\\\":\\\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\\\",\\\"type\\\":\\\"" + + command + "\\\",\\\"operationPayload\\\":{\\\"deviceType\\\":\\\"" + device.deviceType + + "\\\",\\\"deviceSerialNumber\\\":\\\"" + device.serialNumber + "\\\",\\\"customerId\\\":\\\"" + + device.deviceOwnerCustomerId + "\\\",\\\"locale\\\":\\\"\\\"}}}\",\n" + " \"status\": \"ENABLED\" }"; + + makeRequest("POST", m_alexaServer + "/api/behaviors/preview", null, json, true); + } + + public void startRoutine(Device device, String utterance) throws Exception { + JsonAutomation found = null; + String deviceLocale = null; + for (JsonAutomation routine : getRoutines()) { + if (routine.triggers != null && routine.sequence != null) { + for (JsonAutomation.Trigger trigger : routine.triggers) { + if (trigger.payload != null && trigger.payload.utterance != null + && trigger.payload.utterance.equalsIgnoreCase(utterance)) { + found = routine; + deviceLocale = trigger.payload.locale; + break; + } + } + } + } + if (found != null) { + Gson gson = new Gson(); + String sequenceJson = gson.toJson(found.sequence); + + JsonStartRoutineRequest request = new JsonStartRoutineRequest(); + request.behaviorId = found.automationId; + + // replace tokens + + // "deviceType":"ALEXA_CURRENT_DEVICE_TYPE" + String deviceType = "\"deviceType\":\"ALEXA_CURRENT_DEVICE_TYPE\""; + String newDeviceType = "\"deviceType\":\"" + device.deviceType + "\""; + sequenceJson = sequenceJson.replace(deviceType.subSequence(0, deviceType.length()), + newDeviceType.subSequence(0, newDeviceType.length())); + + // "deviceSerialNumber":"ALEXA_CURRENT_DSN" + String deviceSerial = "\"deviceSerialNumber\":\"ALEXA_CURRENT_DSN\""; + String newDeviceSerial = "\"deviceSerialNumber\":\"" + device.serialNumber + "\""; + sequenceJson = sequenceJson.replace(deviceSerial.subSequence(0, deviceSerial.length()), + newDeviceSerial.subSequence(0, newDeviceSerial.length())); + + // "customerId": "ALEXA_CUSTOMER_ID" + String customerId = "\"customerId\":\"ALEXA_CUSTOMER_ID\""; + String newCustomerId = "\"customerId\":\"" + device.deviceOwnerCustomerId + "\""; + sequenceJson = sequenceJson.replace(customerId.subSequence(0, customerId.length()), + newCustomerId.subSequence(0, newCustomerId.length())); + + // "locale": "ALEXA_CURRENT_LOCALE" + String locale = "\"locale\":\"ALEXA_CURRENT_LOCALE\""; + String newlocale = deviceLocale != null ? "\"locale\":\"" + deviceLocale + "\"" : "\"locale\":null"; + sequenceJson = sequenceJson.replace(locale.subSequence(0, locale.length()), + newlocale.subSequence(0, newlocale.length())); + + request.sequenceJson = sequenceJson; + + String requestJson = gson.toJson(request); + makeRequest("POST", m_alexaServer + "/api/behaviors/preview", null, requestJson, true); + } else { + logger.warn("Routine {} not found", utterance); + } + } + + public JsonAutomation[] getRoutines() throws Exception { + String json = makeRequestAndReturnString("GET", m_alexaServer + "/api/behaviors/automations", null, null, true); + JsonAutomation[] result = parseJson(json, JsonAutomation[].class); + return result; + } + + public JsonFeed[] getEnabledFlashBriefings() throws Exception { + String json = makeRequestAndReturnString("GET", m_alexaServer + "/api/content-skills/enabled-feeds", null, null, + true); + JsonEnabledFeeds result = parseJson(json, JsonEnabledFeeds.class); + if (result.enabledFeeds != null) { + return result.enabledFeeds; + } + return new JsonFeed[0]; + } + + public void setEnabledFlashBriefings(JsonFeed[] enabledFlashBriefing) throws Exception { + JsonEnabledFeeds enabled = new JsonEnabledFeeds(); + enabled.enabledFeeds = enabledFlashBriefing; + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.serializeNulls(); + Gson gson = gsonBuilder.create(); + String json = gson.toJson(enabled); + makeRequest("POST", m_alexaServer + "/api/content-skills/enabled-feeds", null, json, true); + } + public JsonNotificationSound[] getNotificationSounds(Device device) throws Exception { String json = makeRequestAndReturnString( "GET", m_alexaServer + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber @@ -584,7 +688,8 @@ public JsonNotificationSound[] getNotificationSounds(Device device) throws Excep return new JsonNotificationSound[0]; } - public void notification(Device device, String type, String label, JsonNotificationSound sound) throws Exception { + public JsonNotificationResponse notification(Device device, String type, String label, JsonNotificationSound sound) + throws Exception { Date date = new Date(new Date().getTime()); long createdDate = date.getTime(); @@ -610,8 +715,70 @@ public void notification(Device device, String type, String label, JsonNotificat Gson gson = gsonBuilder.create(); String data = gson.toJson(request); - makeRequestAndReturnString("PUT", m_alexaServer + "/api/notifications/createReminder", null, data, true); + String response = makeRequestAndReturnString("PUT", m_alexaServer + "/api/notifications/createReminder", null, + data, true); + JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); + return result; + + } + + public void stopNotification(JsonNotificationResponse notification) throws Exception { + makeRequestAndReturnString("DELETE", m_alexaServer + "/api/notifications/" + notification.id, null, null, true); + } + + public JsonNotificationResponse getNotificationState(JsonNotificationResponse notification) throws Exception { + String response = makeRequestAndReturnString("GET", m_alexaServer + "/api/notifications/" + notification.id, + null, null, true); + JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); + return result; + } + + public List getSmartHomeDevices() throws Exception { + try { + String json = makeRequestAndReturnString("GET", m_alexaServer + "/api/phoenix", null, null, true); + logger.debug("getSmartHomeDevices result: {}", json); + + JsonNetworkDetails networkDetails = parseJson(json, JsonNetworkDetails.class); + Gson gson = new Gson(); + Object jsonObject = gson.fromJson(networkDetails.networkDetail, Object.class); + List result = new ArrayList(); + searchSmartHomeDevicesRecursive(gson, jsonObject, result); + return result; + } catch (Exception e) { + logger.error("getSmartHomeDevices fails: {}", e.getMessage()); + throw e; + } + } + + private void searchSmartHomeDevicesRecursive(Gson gson, Object jsonNode, List result) { + + if (jsonNode instanceof Map) { + @SuppressWarnings("rawtypes") + Map map = (Map) jsonNode; + if (map.containsKey("entityId") && map.containsKey("friendlyName") && map.containsKey("actions")) { + // device node found, create type element and add it to the results + JsonElement element = gson.toJsonTree(jsonNode); + JsonSmartHomeDevice device = gson.fromJson(element, JsonSmartHomeDevice.class); + result.add(device); + } else { + for (Object key : map.keySet()) { + Object value = map.get(key); + searchSmartHomeDevicesRecursive(gson, value, result); + } + } + } + } + + public void sendSmartHomeDeviceCommand(String entityId, String action, String parameterName, String parameter) + throws Exception { + + String command = "{" + "\"controlRequests\": [{" + "\"entityId\": \"" + entityId + "\", " + + "\"entityType\": \"APPLIANCE\", " + "\"parameters\": {" + "\"action\": \"" + action + "\"" + + (parameterName != null ? ", \"" + parameterName + "\": \"" + parameter + "\"" : "") + " }" + "}]" + + "}"; + String json = makeRequestAndReturnString("PUT", m_alexaServer + "/api/phoenix/state", null, command, true); + json.toString(); } } \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java new file mode 100644 index 0000000000000..c4f05a7bfd220 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Properties; + +import org.eclipse.smarthome.config.core.ConfigConstants; +import org.eclipse.smarthome.core.thing.Thing; +import org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Store and load the state in and from a file + * + * @author Michael Geramb - Initial Contribution + */ +public class StateStorage { + + private final Logger logger = LoggerFactory.getLogger(StateStorage.class); + + File propertyFile; + Thing thing; + Properties properties; + + public StateStorage(Thing thing) { + this.thing = thing; + propertyFile = new File( + ConfigConstants.getUserDataFolder() + File.separator + AmazonEchoControlBindingConstants.BINDING_ID + + File.separator + thing.getUID().getAsString().replace(':', '_') + ".properties"); + } + + public void storeState(String key, String value) { + synchronized (this) { + initProperties(); + if (value == null || value.isEmpty()) { + properties.remove(key); + } else { + properties.setProperty(key, value); + } + // store the property also in OH to see it in the GUI + thing.setProperty(key, value); + saveProperties(); + } + } + + public String findState(String key) { + synchronized (this) { + initProperties(); + Object value = properties.get(key); + if (value == null) { + // upgrade from BETA 9 configuration + String oldValue = thing.getProperties().get(key); + if (oldValue != null && !oldValue.isEmpty()) { + value = oldValue; + storeState(key, oldValue); + } + } + if (value != null) { + return value.toString(); + } + return null; + } + } + + private void saveProperties() { + + try { + logger.debug("Create file {}.", propertyFile); + String directoryName = propertyFile.getParent(); + File directory = new File(directoryName); + if (!directory.exists()) { + directory.mkdirs(); + } + + FileWriter fileWriter = new FileWriter(propertyFile); + properties.store(fileWriter, "Save properties"); + fileWriter.close(); + } catch (IOException e) { + logger.error("Saving properties failed {}", e); + } + + } + + private void initProperties() { + if (properties == null) { + Properties p = new Properties(); + + if (propertyFile.exists()) { + try { + FileReader fileReader = new FileReader(propertyFile); + p.load(fileReader); + fileReader.close(); + } catch (IOException e) { + logger.error("Error occured on writing the property file.", e); + } + } + properties = p; + } + } +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index d653776d16ca7..65854ce91ed17 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -11,11 +11,13 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -27,7 +29,10 @@ import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; import org.openhab.binding.amazonechocontrol.handler.EchoHandler; +import org.openhab.binding.amazonechocontrol.handler.FlashBriefingProfileHandler; +import org.openhab.binding.amazonechocontrol.handler.SmartHomeBaseHandler; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevice; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.slf4j.Logger; @@ -47,6 +52,8 @@ public class AmazonEchoDiscovery extends AbstractDiscoveryService { private final @NonNull Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); private final @NonNull Map lastDeviceInformations = new HashMap<>(); + private final @NonNull Map lastSmartHomeDeviceInformations = new HashMap<>(); + private final @NonNull HashSet discoverdFlashBriefings = new HashSet(); public static void addDiscoveryHandler(IAmazonEchoDiscovery discoveryService) { synchronized (discoveryServices) { @@ -80,6 +87,11 @@ public static void setHandlerExist() { @Override protected void startScan() { + startScan(true); + } + + protected void startScan(boolean manual) { + if (startScanStateJob != null) { startScanStateJob.cancel(false); startScanStateJob = null; @@ -105,7 +117,7 @@ protected void startScan() { } for (IAmazonEchoDiscovery discovery : accounts) { - discovery.updateDeviceList(); + discovery.updateDeviceList(manual); } } @@ -122,7 +134,7 @@ protected void startBackgroundDiscovery() { startScanStateJob = scheduler.schedule(() -> { - startScan(); + startScan(false); }, 3000, TimeUnit.MILLISECONDS); } @@ -146,6 +158,52 @@ public void activate(Map config) { } }; + public synchronized void setSmartHomeDevices(ThingUID brigdeThingUID, + List deviceInformations) { + Set toRemove = new HashSet(lastSmartHomeDeviceInformations.keySet()); + for (JsonSmartHomeDevice deviceInformation : deviceInformations) { + if (deviceInformation.manufacturerName != null && deviceInformation.manufacturerName.equals("openHAB")) { + // Ignore devices provided by the openHAB skill + continue; + } + String entityId = deviceInformation.entityId; + if (entityId != null) { + boolean alreadyfound = toRemove.remove(entityId); + if (!alreadyfound && deviceInformation.actions != null) { + List actions = Arrays.asList(deviceInformation.actions); + if (actions.contains("turnOn") && actions.contains("turnOff")) { + + ThingTypeUID thingTypeId; + if (actions.contains("setPercentage")) { + thingTypeId = THING_TYPE_SMART_HOME_DIMMER; + } else { + thingTypeId = THING_TYPE_SMART_HOME_SWITCH; + } + + ThingUID thingUID = new ThingUID(thingTypeId, brigdeThingUID, entityId); + + // Check if already created + if (SmartHomeBaseHandler.find(thingUID) == null) { + + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) + .withLabel(deviceInformation.friendlyName) + .withProperty(DEVICE_PROPERTY_ENTITY_ID, entityId) + .withRepresentationProperty(DEVICE_PROPERTY_ENTITY_ID).withBridge(brigdeThingUID) + .build(); + + logger.debug("Device [{}: {}] found. Mapped to thing type {}", + deviceInformation.friendlyName, entityId, thingTypeId.getAsString()); + + thingDiscovered(result); + lastSmartHomeDeviceInformations.put(entityId, thingUID); + } + } + + } + } + } + } + public synchronized void setDevices(ThingUID brigdeThingUID, Device[] deviceInformations) { Set toRemove = new HashSet(lastDeviceInformations.keySet()); @@ -191,7 +249,31 @@ public synchronized void setDevices(ThingUID brigdeThingUID, Device[] deviceInfo } } - public synchronized void removeExisting(@NonNull ThingUID uid) { + public synchronized void discoverFlashBriefingProfiles(ThingUID brigdeThingUID, String currentFlashBriefingJson) { + if (currentFlashBriefingJson.isEmpty()) { + return; + } + if (discoverdFlashBriefings.contains(currentFlashBriefingJson)) { + return; + } + if (!FlashBriefingProfileHandler.exist(currentFlashBriefingJson)) { + if (!discoverdFlashBriefings.contains(currentFlashBriefingJson)) { + + String id = UUID.randomUUID().toString(); + ThingUID thingUID = new ThingUID(THING_TYPE_FLASH_BRIEFING_PROFILE, brigdeThingUID, id); + + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("FlashBriefing") + .withProperty(DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE, currentFlashBriefingJson) + .withBridge(brigdeThingUID).build(); + logger.debug("Flash Briefing {} discovered", currentFlashBriefingJson); + + thingDiscovered(result); + discoverdFlashBriefings.add(currentFlashBriefingJson); + } + } + } + + public synchronized void removeExistingEchoHandler(@NonNull ThingUID uid) { for (String id : lastDeviceInformations.keySet()) { if (lastDeviceInformations.get(id).equals(uid)) { lastDeviceInformations.remove(id); @@ -199,4 +281,18 @@ public synchronized void removeExisting(@NonNull ThingUID uid) { } } + public synchronized void removeExistingSmartHomeHandler(@NonNull ThingUID uid) { + for (String id : lastSmartHomeDeviceInformations.keySet()) { + if (lastSmartHomeDeviceInformations.get(id).equals(uid)) { + lastSmartHomeDeviceInformations.remove(id); + } + } + } + + public synchronized void removeExistingFlashBriefingProfile(String currentFlashBriefingJson) { + if (currentFlashBriefingJson != null) { + discoverdFlashBriefings.remove(currentFlashBriefingJson); + } + } + } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java index 2f36b6d9fea51..4186c5f160bee 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java @@ -14,5 +14,5 @@ * @author Michael Geramb - Initial contribution */ public interface IAmazonEchoDiscovery { - void updateDeviceList(); + void updateDeviceList(boolean manual); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAutomation.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAutomation.java new file mode 100644 index 0000000000000..2918115355caa --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAutomation.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import java.util.TreeMap; + +/** + * The {@link JsonAutomation} encapsulate the GSON data of automation query + * + * @author Michael Geramb - Initial contribution + */ +public class JsonAutomation { + public String automationId; + public String name; + public Trigger[] triggers; + public TreeMap sequence; + public String status; + public long creationTimeEpochMillis; + public long lastUpdatedTimeEpochMillis; + + public class Trigger { + public Payload payload; + public String id; + public String type; + } + + public class Payload { + public String customerId; + public String utterance; + public String locale; + public String marketplaceId; + } +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java index b54fd735481d6..84f99152bae2b 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java @@ -24,6 +24,7 @@ public class Device { public String deviceType; public String softwareVersion; public boolean online; + public String[] capabilities; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEnabledFeeds.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEnabledFeeds.java new file mode 100644 index 0000000000000..6429abd6cdc08 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEnabledFeeds.java @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +/** + * The {@link JsonEnabledFeeds} encapsulate the GSON data of the enabled feeds list + * + * @author Michael Geramb - Initial contribution + */ +public class JsonEnabledFeeds { + public JsonFeed[] enabledFeeds; + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java new file mode 100644 index 0000000000000..a912e9baf4ad6 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +/** + * The {@link JsonFeed} encapsulate the GSON data of feed + * + * @author Michael Geramb - Initial contribution + */ +public class JsonFeed { + public Object feedId; + public String name; + public String skillId; + public String imageUrl; +} \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java new file mode 100644 index 0000000000000..5938d36f5de34 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +/** + * The {@link JsonNetworkDetails} encapsulate the GSON data of a network query + * + * @author Michael Geramb - Initial contribution + */ +public class JsonNetworkDetails { + public String networkDetail; + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java new file mode 100644 index 0000000000000..5362f7ef19fe2 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +/** + * The {@link JsonNotificationResponse} encapsulate the GSON data for the result of a notification request + * + * @author Michael Geramb - Initial contribution + */ +public class JsonNotificationResponse { + // This is only a partial definition, see the example JSON below + public long alarmTime; + public long createdDate; + public String deviceSerialNumber; + public String deviceType; + public String id; + public String status; + public String type; +} + +/* + * Example JSON: + * { + *    "alarmTime":1518864868060, + *    "createdDate":1518864863801, + *    "deviceSerialNumber":"XXXXXXXXXX", + *    "deviceType":"XXXXXXXXXX", + *    "id":"XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX", + *    "musicAlarmId":null, + *    "musicEntity":null, + *    "notificationIndex":"XXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX", + *    "originalDate":null, + *    "originalTime":"11:54:28.060", + *    "provider":null, + *    "recurringPattern":null, + *    "remainingTime":0, + *    "reminderLabel":null, + *    "sound":{ + *       "displayName":"Clarity", + *       "folder":null, + *       "id":"system_alerts_melodic_05", + *       "providerId":"ECHO", + *       "sampleUrl":"https://s3.amazonaws.com/deeappservice.prod.notificationtones/system_alerts_melodic_05.mp3" + *    }, + *    "status":"OFF", + *    "timeZoneId":null, + *    "timerLabel":null, + *    "triggerTime":0, + *    "type":"Alarm", + *    "version":"2", + *    "alarmIndex":null, + *    "isSaveInFlight":true + * } + * + */ \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java new file mode 100644 index 0000000000000..c5351184d9683 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +/** + * The {@link JsonSmartHomeDevice} encapsulate the GSON-part data of a network query + * + * @author Michael Geramb - Initial contribution + */ +public class JsonSmartHomeDevice { + public String entityId; + public String friendlyName; + public String[] actions; + public String manufacturerName; +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java new file mode 100644 index 0000000000000..5e3dba9a4000c --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +/** + * The {@link JsonStartRoutineRequest} encapsulate the GSON for starting a routine + * + * @author Michael Geramb - Initial contribution + */ +public class JsonStartRoutineRequest { + public String behaviorId; + public String sequenceJson; + public String status = "ENABLED"; +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java index 09a9549b7ef97..e36babaa3522a 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -11,6 +11,7 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; import java.util.ArrayList; +import java.util.Arrays; import java.util.Locale; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -19,7 +20,9 @@ import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider; import org.eclipse.smarthome.core.types.StateDescription; import org.eclipse.smarthome.core.types.StateOption; +import org.openhab.binding.amazonechocontrol.handler.AccountHandler; import org.openhab.binding.amazonechocontrol.handler.EchoHandler; +import org.openhab.binding.amazonechocontrol.handler.FlashBriefingProfileHandler; import org.openhab.binding.amazonechocontrol.internal.Connection; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.PairedDevice; @@ -161,8 +164,34 @@ public AmazonEchoDynamicStateDescriptionProvider() { originalStateDescription.getMaximum(), originalStateDescription.getStep(), originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); return result; + } else if (CHANNEL_TYPE_CHANNEL_PLAY_ON_DEVICE.equals(channel.getChannelTypeUID())) { + FlashBriefingProfileHandler handler = FlashBriefingProfileHandler.find(channel.getUID().getThingUID()); + if (handler == null) { + return originalStateDescription; + } + + AccountHandler accountHandler = handler.findAccountHandler(); + if (accountHandler == null) { + return originalStateDescription; + } + Device[] devices = accountHandler.getLastKnownDevices(); + if (devices.length == 0) { + return originalStateDescription; + } + + ArrayList options = new ArrayList(); + options.add(new StateOption("", "")); + for (Device device : devices) { + if (device.capabilities != null && Arrays.asList(device.capabilities).contains("FLASH_BRIEFING")) { + options.add(new StateOption(device.serialNumber, device.accountName)); + } + } + StateDescription result = new StateDescription(originalStateDescription.getMinimum(), + originalStateDescription.getMaximum(), originalStateDescription.getStep(), + originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); + return result; + } return originalStateDescription; } - } From b3148a826a44f85c8a996eb7e074eb92ac86e094 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Fri, 9 Mar 2018 22:52:15 +0100 Subject: [PATCH 15/56] [amazonechocontrol] Fix warnings Signed-off-by: Michael Geramb (github: mgeramb) --- .../ESH-INF/binding/binding.xml | 21 +- .../ESH-INF/thing/thing-types.xml | 1254 ++++++++++------- .../handler/EchoHandler.java | 2 + .../handler/FlashBriefingProfileHandler.java | 2 +- .../handler/SmartHomeBaseHandler.java | 6 +- .../internal/Connection.java | 2 +- .../internal/StateStorage.java | 4 + ...onEchoDynamicStateDescriptionProvider.java | 2 +- 8 files changed, 758 insertions(+), 535 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml index f2e4d0fcfb43b..e9fe5bbbeace5 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml @@ -1,13 +1,12 @@ - - - Amazon Echo Control Binding - Binding for controlling Amazon Echo (Alexa). This binding enables openhab to control the volume, playing state, bluetooth connection of your amazon echo devices. + + + Amazon Echo Control Binding + + + Binding for controlling Amazon Echo (Alexa). This binding enables openhab to control the volume, playing state, bluetooth connection of your amazon echo devices. - Michael Geramb - - - + + Michael Geramb + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index d58d2ce36fd69..9ae12b9b95a01 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -1,525 +1,743 @@ - - - - - - Amazon Account where your amazon echo is registered. - - - - - - false - - - - - - - - - Select the site where your amazon account is created. - - - - - - Enter the email address of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! - - - - password - - Enter the password of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! - - - 60 - - Refresh state interval in seconds. Lower time causes more network traffic. - Seconds - - - - - - + + + + + Amazon Account where your amazon echo is registered. + + + + + + + + false + + + + + + + + + + + Select the site where your amazon account is created. + + + + + + + + Enter the email address of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! + + + + + + + password + + + + Enter the password of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! + + + + + 60 + + + + Refresh state interval in seconds. Lower time causes more network traffic. + + + Seconds + + + + + - - - - - - Amazon Echo device (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) - + + + + + + Amazon Echo device (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) + - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - - You will find the serial number of your device in the Alexa app - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + + + You will find the serial number of your device in the Alexa app + + + - - - - - - - - Amazon Echo Spot device - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - - - - Amazon Echo Spot device - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - - - - Amazon Multiroom Music - - - - - - - - - - - - - - - - - - serialNumber - - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - - - - Unknown Echo Device. Warning: Maybe not all channels will be supported from the device - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - - - - Store and load a flash briefing configuration - - - - - - - - - - - - - - - - Smart Home Switch - - - - - - entityId - - - - - Use the search feature of openHAB to get the id - - - - - - - - - - - - Smart Home Dimmer - - - - - - - entityId - - - - - Let discover the device to get the id - - - - - - - - Switch - - Save the current flash briefing configuration (Write only) - - - - Switch - - Activate this flash briefing configuration - - - - String - - Plays the briefing on the device (serial number or name, write only) - - - - Switch - - Turns the device on or off - - - - Dimmer - - Dimmer control - - - - String - - Connected bluetooth device - - - - - String - - Id of the radio station - - - - String - - Speak the reminder and send a notification to the Alexa app - - - - Switch - - Starts the flash briefing (Write Only) - - - - Switch - - Starts the weather report (Write Only) - - - - Switch - - Starts the traffic news (Write Only) - - - - String - - Type in what you normally say to Alexa without the preceding "Alexa," (Write Only) - - - - String - - Plays an alarm sound - - - - String - - Id of the amazon music track - - - - Switch - - Amazon Music turned on - - - - String - - Amazon Music play list id (Write only, no current state) - - - - String - - Is of the playlist which was started with openHAB - - - - String - - Name of music provider - - - - - String - - MAC-Address of the bluetooth connected device - - - - String - - Bluetooth connection selection (Currently only in PaperUI) - - - - String - - Url of the album image or radio station logo - - - - - String - - Title - - - - - String - - Subtitle 1 - - - - - String - - Subtitle 2 - - - - - Switch - - Radio turned on - - - - Switch - - Connect to last used device - - - - Switch - - Loop - - - - Switch - - Shuffle play - - + + + + + + + Amazon Echo Spot device + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + + + Amazon Echo Spot device + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + + + Amazon Multiroom Music + + + + + + + + + + + + + + + + + + serialNumber + + + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + + + Unknown Echo Device. Warning: Maybe not all channels will be supported from the device + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + + + Store and load a flash briefing configuration + + + + + + + + + + + + + + Smart Home Switch + + + + + + entityId + + + + + + Use the search feature of openHAB to get the id + + + + + + + + + + + Smart Home Dimmer + + + + + + + entityId + + + + + + Let discover the device to get the id + + + + + + + Switch + + + + Save the current flash briefing configuration (Write only) + + + + + Switch + + + + Activate this flash briefing configuration + + + + + String + + + + Plays the briefing on the device (serial number or name, write only) + + + + + Switch + + + + Turns the device on or off + + + + + Dimmer + + + + Dimmer control + + + + + String + + + + Connected bluetooth device + + + + + + + String + + + + Id of the radio station + + + + + String + + + + Speak the reminder and send a notification to the Alexa app + + + + + Switch + + + + Starts the flash briefing (Write Only) + + + + + Switch + + + + Starts the weather report (Write Only) + + + + + Switch + + + + Starts the traffic news (Write Only) + + + + + String + + + + Type in what you normally say to Alexa without the preceding "Alexa," (Write Only) + + + + + String + + + + Plays an alarm sound + + + + + String + + + + Id of the amazon music track + + + + + Switch + + + + Amazon Music turned on + + + + + String + + + + Amazon Music play list id (Write only, no current state) + + + + + String + + + + Is of the playlist which was started with openHAB + + + + + String + + + + Name of music provider + + + + + + + String + + + + MAC-Address of the bluetooth connected device + + + + + String + + + + Bluetooth connection selection (Currently only in PaperUI) + + + + + String + + + + Url of the album image or radio station logo + + + + + + + String + + + + Title + + + + + + + String + + + + Subtitle 1 + + + + + + + String + + + + Subtitle 2 + + + + + + + Switch + + + + Radio turned on + + + + + Switch + + + + Connect to last used device + + + + + Switch + + + + Loop + + + + + Switch + + + + Shuffle play + + - Player - - Music Player + + Player + + + + Music Player + - - Dimmer - - Volume of the sound - - - + + Dimmer + + + + Volume of the sound + + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 64358a41d1dba..704c47550fb4f 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -277,6 +277,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (channelId.equals(CHANNEL_AMAZON_MUSIC)) { if (command == OnOffType.ON) { + String lastKnownAmazonMusicId = this.lastKnownAmazonMusicId; if (lastKnownAmazonMusicId != null && !lastKnownAmazonMusicId.isEmpty()) { waitForUpdate = 3000; } @@ -300,6 +301,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (channelId.equals(CHANNEL_RADIO)) { if (command == OnOffType.ON) { + String lastKnownRadioStationId = this.lastKnownRadioStationId; if (lastKnownRadioStationId != null && !lastKnownRadioStationId.isEmpty()) { waitForUpdate = 3000; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java index 39be9bc1348ec..a375402423cbf 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java @@ -75,7 +75,7 @@ public static boolean exist(String profileJson) { @Override public void initialize() { updatePlayOnDevice = true; - logger.info(getClass().getSimpleName() + " initialized"); + logger.info("{} initialized", getClass().getSimpleName()); synchronized (instances) { instances.put(this.getThing().getUID(), this); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java index b1a2acb7aa829..46980c3906086 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java @@ -32,7 +32,7 @@ public abstract class SmartHomeBaseHandler extends BaseThingHandler { private static HashMap instances = new HashMap(); - private final Logger logger = LoggerFactory.getLogger(SmartHomeDimmerHandler.class); + private final Logger logger = LoggerFactory.getLogger(SmartHomeBaseHandler.class); private Connection connection; @@ -47,7 +47,7 @@ protected SmartHomeBaseHandler(Thing thing) { @Override public void initialize() { - logger.info(getClass().getSimpleName() + " initialized"); + logger.info("{} initialized", getClass().getSimpleName()); synchronized (instances) { instances.put(this.getThing().getUID(), this); } @@ -88,7 +88,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { try { handleCommand(temp, entityId, channelId, command); } catch (Exception e) { - logger.warn("handle command {} for {} failed: {}", command, channelUID, e); + logger.warn("handle command {} for {} failed", command, channelUID, e); } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index d3c63a94e1dc7..a315d225c7793 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -339,7 +339,7 @@ private HttpsURLConnection makeRequest(String verb, String url, String referer, continue; } } catch (Exception e) { - logger.warn("Request to url '{}' fails with unkown error: {}", url, e); + logger.warn("Request to url '{}' fails with unkown error", url, e); throw e; } if (code != 200) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java index c4f05a7bfd220..cb1c1a39b7d89 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java @@ -42,6 +42,9 @@ public StateStorage(Thing thing) { public void storeState(String key, String value) { synchronized (this) { + if (key == null) { + return; + } initProperties(); if (value == null || value.isEmpty()) { properties.remove(key); @@ -54,6 +57,7 @@ public void storeState(String key, String value) { } } + @SuppressWarnings("null") public String findState(String key) { synchronized (this) { initProperties(); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java index e36babaa3522a..15ab9cceeb266 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -45,7 +45,7 @@ AmazonEchoDynamicStateDescriptionProvider.class }, immediate = true) public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDescriptionProvider { - private final Logger logger = LoggerFactory.getLogger(EchoHandler.class); + private final Logger logger = LoggerFactory.getLogger(AmazonEchoDynamicStateDescriptionProvider.class); public AmazonEchoDynamicStateDescriptionProvider() { From a48e1fafaaa61143fd0888441df627fb9fbd82a2 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Fri, 9 Mar 2018 23:15:15 +0100 Subject: [PATCH 16/56] [amazonechocontrol] Fix wrong formated xml Signed-off-by: Michael Geramb (github: mgeramb) --- .../ESH-INF/binding/binding.xml | 4 ++-- .../ESH-INF/thing/thing-types.xml | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml index e9fe5bbbeace5..55683c3c4aa3b 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml @@ -1,5 +1,5 @@ - + Amazon Echo Control Binding @@ -9,4 +9,4 @@ Michael Geramb - + \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 9ae12b9b95a01..69bc6dbb8d9c2 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -1,12 +1,8 @@ - + - - - Amazon Account where your amazon echo is registered. - + + Amazon Account where your amazon echo is registered. @@ -740,4 +736,4 @@ Volume of the sound - + From 9229be24dd3ab5214f4a103308322dd564da3c4d Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 11 Mar 2018 09:03:59 +0100 Subject: [PATCH 17/56] [amazonechocontrol] State handling changed Signed-off-by: Michael Geramb (github: mgeramb) --- .../handler/AccountHandler.java | 58 +++++++++---------- .../handler/EchoHandler.java | 18 +++++- .../handler/FlashBriefingProfileHandler.java | 10 +++- .../handler/SmartHomeBaseHandler.java | 15 ++++- 4 files changed, 68 insertions(+), 33 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index dfb0aa87d99bb..a5a7252ea38be 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -99,46 +99,44 @@ public void initialize() { start(); } - @Override - public void childHandlerInitialized(@NonNull ThingHandler childHandler, @NonNull Thing childThing) { - super.childHandlerInitialized(childHandler, childThing); - - if (childHandler instanceof EchoHandler) { - EchoHandler echoHandler = (EchoHandler) childHandler; - synchronized (echoHandlers) { + public void addEchoHandler(@NonNull EchoHandler echoHandler) { + synchronized (echoHandlers) { + if (!echoHandlers.contains(echoHandler)) { echoHandlers.add(echoHandler); } - Connection temp = connection; - if (temp != null) { - initializeEchoHandler(echoHandler, temp); - } - } - if (childHandler instanceof FlashBriefingProfileHandler) { - FlashBriefingProfileHandler flashBriefingProfileHandler = (FlashBriefingProfileHandler) childHandler; - synchronized (flashBriefingProfileHandlers) { + Connection temp = connection; + if (temp != null) { + initializeEchoHandler(echoHandler, temp); + } + } + + public void addFlashBriefingProfileHandler(@NonNull FlashBriefingProfileHandler flashBriefingProfileHandler) { + synchronized (flashBriefingProfileHandlers) { + if (!flashBriefingProfileHandlers.contains(flashBriefingProfileHandler)) { flashBriefingProfileHandlers.add(flashBriefingProfileHandler); } - Connection temp = connection; - if (temp != null) { - if (currentFlashBriefingJson.isEmpty()) { - updateFlashBriefingProfiles(temp); - } - - flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson); + } + Connection temp = connection; + if (temp != null) { + if (currentFlashBriefingJson.isEmpty()) { + updateFlashBriefingProfiles(temp); } + + flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson); } - if (childHandler instanceof SmartHomeBaseHandler) { - SmartHomeBaseHandler smartHomeHandler = (SmartHomeBaseHandler) childHandler; - synchronized (smartHomeHandlers) { + } + + public void addSmartHomeHandler(@NonNull SmartHomeBaseHandler smartHomeHandler) { + synchronized (smartHomeHandlers) { + if (!smartHomeHandlers.contains(smartHomeHandler)) { smartHomeHandlers.add(smartHomeHandler); } - Connection temp = connection; - if (temp != null) { - smartHomeHandler.initialize(temp); - } } - + Connection temp = connection; + if (temp != null) { + smartHomeHandler.initialize(temp); + } } private void initializeEchoHandler(@NonNull EchoHandler echoHandler, @NonNull Connection temp) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 704c47550fb4f..81f0a1f522a2d 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -23,6 +23,7 @@ import org.eclipse.smarthome.core.library.types.PlayPauseType; import org.eclipse.smarthome.core.library.types.RewindFastforwardType; import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; @@ -83,15 +84,30 @@ public EchoHandler(Thing thing) { @Override public void initialize() { logger.info("Amazon Echo Control Binding initialized"); + synchronized (instances) { instances.put(this.getThing().getUID(), this); } - updateStatus(ThingStatus.ONLINE); + if (this.connection != null) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.UNKNOWN); + Bridge bridge = this.getBridge(); + if (bridge != null) { + AccountHandler account = (AccountHandler) bridge.getHandler(); + if (account != null) { + account.addEchoHandler(this); + } + } + + } + } public void intialize(Connection connection, @Nullable Device deviceJson) { this.connection = connection; this.device = deviceJson; + updateStatus(ThingStatus.ONLINE); } @Override diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java index a375402423cbf..c13aecbf0df55 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java @@ -17,6 +17,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; @@ -82,7 +83,14 @@ public void initialize() { if (this.currentConfigurationJson != null && !this.currentConfigurationJson.isEmpty()) { updateStatus(ThingStatus.ONLINE); } else { - updateStatus(ThingStatus.OFFLINE); + updateStatus(ThingStatus.UNKNOWN); + Bridge bridge = this.getBridge(); + if (bridge != null) { + AccountHandler account = (AccountHandler) bridge.getHandler(); + if (account != null) { + account.addFlashBriefingProfileHandler(this); + } + } } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java index 46980c3906086..fe744abb011bc 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java @@ -14,6 +14,7 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; @@ -51,11 +52,23 @@ public void initialize() { synchronized (instances) { instances.put(this.getThing().getUID(), this); } - updateStatus(ThingStatus.ONLINE); + if (this.connection != null) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.UNKNOWN); + Bridge bridge = this.getBridge(); + if (bridge != null) { + AccountHandler account = (AccountHandler) bridge.getHandler(); + if (account != null) { + account.addSmartHomeHandler(this); + } + } + } } public void initialize(@NonNull Connection connection) { this.connection = connection; + updateStatus(ThingStatus.ONLINE); } @Override From 0664c0ee83a1cd876cfcda8cf287fe6f32e036ce Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 14 Mar 2018 20:34:02 +0100 Subject: [PATCH 18/56] [amazonechocontrol] Bugfix echoshow device, New login possiblity to handle captcha Signed-off-by: Michael Geramb (github: mgeramb) --- .../i18n/amazonechocontrol_de.properties | 4 +- .../ESH-INF/thing/thing-types.xml | 2 +- .../META-INF/MANIFEST.MF | 4 + .../README.md | 4 +- .../AmazonEchoControlBindingConstants.java | 7 +- .../handler/AccountHandler.java | 40 ++++- .../AmazonEchoControlHandlerFactory.java | 22 ++- .../internal/Connection.java | 125 +++++++++------ .../internal/LoginServlet.java | 145 ++++++++++++++++++ 9 files changed, 289 insertions(+), 64 deletions(-) create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties index 6df809db10e80..ad81af2e905d9 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties @@ -14,7 +14,9 @@ thing-type.config.amazonechocontrol.account.email.label = Amazon Konto E-Mail thing-type.config.amazonechocontrol.account.email.description = E-Mail des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde eingeben. thing-type.config.amazonechocontrol.account.password.label = Amazon Konto Kennwort -thing-type.config.amazonechocontrol.account.password.description = Kennwort des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde eingeben. +thing-type.config.amazonechocontrol.account.password.description = Kennwort des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde eingeben. WICHTIG: Sollte das Account-Thing nicht Online gehen und einen Login-Fehler melden, öffne die URL YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in deinem Browser (Z.B.: http://openhab:8080/amazonechocontrol/account) und versuche dich anzumelden. + e.g. http://openhab:8080/amazonechocontrol/account and try to login. + thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.label = Status-Aktualisierungs-Intervall thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.description = Aktualtisierungs-Intervall für den Status in Sekunden. Kleinere Zeiten verursachen höheren Netzwerkverkehr. diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 69bc6dbb8d9c2..4906fd4f4ce64 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -57,7 +57,7 @@ Amazon account password - Enter the password of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! + Enter the password of the amazon account which is used for the amazon echo devices. IMPORTANT: If the account thing does not go online and reports and login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF index bd7ac2446c8da..e98935ddd8c31 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF +++ b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF @@ -9,6 +9,9 @@ Bundle-Vendor: openHAB Bundle-Version: 2.3.0.qualifier Import-Package: com.google.gson;resolution:=optional, com.google.gson.annotations, + javax.servlet;version="3.1.0", + javax.servlet.http;version="3.1.0", + javax.ws.rs.core;version="2.0.1", org.eclipse.jdt.annotation;resolution:=optional, org.eclipse.smarthome.config.core, org.eclipse.smarthome.config.discovery, @@ -24,6 +27,7 @@ Import-Package: com.google.gson;resolution:=optional, org.openhab.binding.amazonechocontrol.handler, org.osgi.framework, org.osgi.service.component.annotations;resolution:=optional, + org.osgi.service.http;version="1.2.1", org.slf4j Service-Component: OSGI-INF/*.xml Export-Package: diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index dc01d15aa08b2..380ba351939de 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -72,7 +72,7 @@ The binding does not have any configuration. The configuration of your amazon ac ## Thing Configuration -The Amazon Account device need the following configurations: +The Amazon Account thing need the following configurations: | Configuration name | Description | |--------------------------|---------------------------------------------------------------------------| @@ -83,6 +83,8 @@ The Amazon Account device need the following configurations: 2 factor authentication is not supported! +** HINT ** IMPORTANT: If the Account thing does not go online and reports and login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_ACCOUNT_THING (Replace YOUR_OPENHAB and ID_OF_ACCOUNT_THING with your configuration) in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. + ### Amazon devices All Amazon devices (echo, echospot, echoshow, wha, unknown) needs the following configurations: diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java index a22d535d42c9d..02e39baf52900 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java @@ -41,9 +41,10 @@ public class AmazonEchoControlBindingConstants { public static final ThingTypeUID THING_TYPE_SMART_HOME_SWITCH = new ThingTypeUID(BINDING_ID, "smarthomeswitch"); public static final ThingTypeUID THING_TYPE_SMART_HOME_DIMMER = new ThingTypeUID(BINDING_ID, "smarthomedimmer"); - public static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet(Arrays.asList( - THING_TYPE_ACCOUNT, THING_TYPE_ECHO, THING_TYPE_ECHO_SPOT, THING_TYPE_ECHO_WHA, THING_TYPE_UNKNOWN, - THING_TYPE_SMART_HOME_SWITCH, THING_TYPE_SMART_HOME_DIMMER, THING_TYPE_FLASH_BRIEFING_PROFILE)); + public static final Set SUPPORTED_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_UNKNOWN, THING_TYPE_SMART_HOME_SWITCH, THING_TYPE_SMART_HOME_DIMMER, + THING_TYPE_FLASH_BRIEFING_PROFILE)); // List of all Channel ids public static final String CHANNEL_PLAYER = "player"; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index a5a7252ea38be..eb07df4b9911e 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -30,6 +30,7 @@ import org.openhab.binding.amazonechocontrol.internal.AccountConfiguration; import org.openhab.binding.amazonechocontrol.internal.Connection; import org.openhab.binding.amazonechocontrol.internal.ConnectionException; +import org.openhab.binding.amazonechocontrol.internal.LoginServlet; import org.openhab.binding.amazonechocontrol.internal.StateStorage; import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; import org.openhab.binding.amazonechocontrol.internal.discovery.IAmazonEchoDiscovery; @@ -38,6 +39,7 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevice; +import org.osgi.service.http.HttpService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,10 +67,12 @@ public class AccountHandler extends BaseBridgeHandler implements IAmazonEchoDisc private boolean discoverFlashProfiles; private boolean smartHodeDeviceListEnabled; private String currentFlashBriefingJson = ""; + private HttpService httpService; + private LoginServlet loginServlet; - public AccountHandler(@NonNull Bridge bridge) { + public AccountHandler(@NonNull Bridge bridge, @NonNull HttpService httpService) { super(bridge); - + this.httpService = httpService; stateStorage = new StateStorage(bridge); AmazonEchoDiscovery.setHandlerExist(); @@ -199,6 +203,11 @@ public void handleRemoval() { @Override public void dispose() { + LoginServlet loginServlet = this.loginServlet; + if (loginServlet != null) { + loginServlet.dispose(); + } + this.loginServlet = null; AmazonEchoDiscovery.removeDiscoveryHandler(this); cleanup(); super.dispose(); @@ -265,6 +274,10 @@ private void start() { connection = new Connection(config.email, config.password, config.amazonSite); } } + if (this.loginServlet == null) { + this.loginServlet = new LoginServlet(httpService, this.getThing().getUID().getId(), this, config); + } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login"); if (refreshLogin != null) { refreshLogin.cancel(false); @@ -355,16 +368,31 @@ private void checkLogin() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } if (loginIsValid) { - // update the device list - updateDeviceList(false); - updateStatus(ThingStatus.ONLINE); - AmazonEchoDiscovery.addDiscoveryHandler(this); + handleValidLogin(); } } } } + private void handleValidLogin() { + // update the device list + updateDeviceList(false); + updateStatus(ThingStatus.ONLINE); + AmazonEchoDiscovery.addDiscoveryHandler(this); + } + + public void setConnection(Connection connection) { + this.connection = connection; + String serializedStorage = connection.serializeLoginData(); + if (serializedStorage == null) { + serializedStorage = ""; + } + this.stateStorage.storeState("sessionStorage", serializedStorage); + updateSmartHomeDeviceList = true; + handleValidLogin(); + } + private void refreshData() { logger.debug("amazon account bridge refreshing data ..."); try { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 3e6a4f0289c5f..30bf7c99d94c2 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -24,6 +24,8 @@ import org.openhab.binding.amazonechocontrol.handler.SmartHomeDimmerHandler; import org.openhab.binding.amazonechocontrol.handler.SmartHomeSwitchHandler; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.http.HttpService; /** * The {@link AmazonEchoControlHandlerFactory} is responsible for creating things and thing @@ -35,6 +37,9 @@ @NonNullByDefault public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { + @Nullable + HttpService httpService; + @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); @@ -44,8 +49,12 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + HttpService httpService = this.httpService; + if (httpService == null) { + return null; + } if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { - AccountHandler bridgeHandler = new AccountHandler((Bridge) thing); + AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService); return bridgeHandler; } if (thingTypeUID.equals(THING_TYPE_FLASH_BRIEFING_PROFILE)) { @@ -57,9 +66,18 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { if (thingTypeUID.equals(THING_TYPE_SMART_HOME_SWITCH)) { return new SmartHomeSwitchHandler(thing); } - if (SUPPORTED_THING_TYPES_UIDS.contains(THING_TYPE_ECHO)) { + if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { return new EchoHandler(thing); } return null; } + + @Reference + protected void setHttpService(HttpService httpService) { + this.httpService = httpService; + } + + protected void unsetHttpService(HttpService httpService) { + this.httpService = null; + } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index a315d225c7793..ecb4d2b2802e3 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -9,12 +9,15 @@ package org.openhab.binding.amazonechocontrol.internal; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.CookieStore; import java.net.HttpCookie; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; +import java.net.URLConnection; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; @@ -234,7 +237,7 @@ public Date tryGetLoginTime() { } private HttpsURLConnection makeRequest(String verb, String url, String referer, String postData, Boolean json) - throws Exception { + throws IOException, URISyntaxException { String currentUrl = url; for (int i = 0; i < 30; i++) // loop for handling redirect, using automatic redirect is not possible, because // all response headers must be catched @@ -254,7 +257,6 @@ private HttpsURLConnection makeRequest(String verb, String url, String referer, if (referer != null) { connection.setRequestProperty("Referer", referer); } - // add cookies URI uri = connection.getURL().toURI(); @@ -320,12 +322,15 @@ private HttpsURLConnection makeRequest(String verb, String url, String referer, if (code == 302) { logger.debug("Redirected to {}", location); } + location = uri.resolve(location).toString(); + // check for https if (location.toLowerCase().startsWith("http://")) { // always use https location = "https://" + location.substring(7); logger.debug("Redirect corrected to {}", location); } + } } } @@ -338,7 +343,7 @@ private HttpsURLConnection makeRequest(String verb, String url, String referer, currentUrl = location; continue; } - } catch (Exception e) { + } catch (IOException e) { logger.warn("Request to url '{}' fails with unkown error", url, e); throw e; } @@ -353,30 +358,37 @@ public boolean getIsLoggedIn() { return m_loginTime != null; } - public void makeLogin() throws Exception { - try { - // clear session data - m_cookieManager.getCookieStore().removeAll(); - m_sessionId = null; - m_loginTime = null; + public String getLoginPage() throws IOException, URISyntaxException { + // clear session data + m_cookieManager.getCookieStore().removeAll(); + m_sessionId = null; + m_loginTime = null; - logger.debug("Start Login to {}", m_alexaServer); - // get login form - String loginFormHtml = makeRequestAndReturnString(m_alexaServer); + logger.debug("Start Login to {}", m_alexaServer); + // get login form + String loginFormHtml = makeRequestAndReturnString(m_alexaServer); - logger.debug("Received login form {}", loginFormHtml); + logger.debug("Received login form {}", loginFormHtml); - // get session id from cookies - for (HttpCookie cookie : m_cookieManager.getCookieStore().getCookies()) { - if (cookie.getName().equalsIgnoreCase("session-id")) { - m_sessionId = cookie.getValue(); - break; - } - } - if (m_sessionId == null) { - throw new ConnectionException("No session id received"); + // get session id from cookies + for (HttpCookie cookie : m_cookieManager.getCookieStore().getCookies()) { + if (cookie.getName().equalsIgnoreCase("session-id")) { + m_sessionId = cookie.getValue(); + break; } + } + if (m_sessionId == null) { + throw new ConnectionException("No session id received"); + } + m_cookieManager.getCookieStore().add(new URL("https://www." + m_amazonSite).toURI(), + HttpCookie.parse("session-id=" + m_sessionId).get(0)); + return loginFormHtml; + } + public void makeLogin() throws IOException, URISyntaxException { + try { + + String loginFormHtml = getLoginPage(); // read hidden form inputs, the will be used later in the url and for posting Pattern inputPattern = Pattern .compile("[^\"]+)\"\\s+value=\"(?[^\"]*)\""); @@ -406,30 +418,10 @@ public void makeLogin() throws Exception { String postData = postDataBuilder.toString(); - // post login data - - String referer = "https://www." + m_amazonSite + "/ap/signin?" + queryParameters; - m_cookieManager.getCookieStore().add(new URL("https://www." + m_amazonSite).toURI(), - HttpCookie.parse("session-id=" + m_sessionId).get(0)); - String response = makeRequestAndReturnString("POST", "https://www." + m_amazonSite + "/ap/signin", referer, - postData, false); - if (response.contains("Amazon Alexa")) { - logger.debug("Response seems to be alexa app"); - } else { - logger.info("Response maybe not valid"); - } - - logger.debug("Received content after login {}", response); - - // get CSRF - // makeRequest(m_alexaServer + "/api/language", m_alexaServer + "/spa/index.html", null, false); - - // verify login - if (!verifyLogin()) { + if (postLoginData(queryParameters, postData) != null) { throw new ConnectionException("Login fails."); } - m_loginTime = new Date(); - logger.debug("Login succeeded"); + } catch (Exception e) { // clear session data m_cookieManager.getCookieStore().removeAll(); @@ -441,25 +433,58 @@ public void makeLogin() throws Exception { } - private String makeRequestAndReturnString(String url) throws Exception { + public String postLoginData(String optionalQueryParameters, String postData) + throws IOException, URISyntaxException { + // post login data + String queryParameters = optionalQueryParameters; + if (queryParameters == null) { + queryParameters = "session-id=" + URLEncoder.encode(m_sessionId, "UTF-8"); + } + + String referer = "https://www." + m_amazonSite + "/ap/signin?" + queryParameters; + URLConnection request = makeRequest("POST", "https://www." + m_amazonSite + "/ap/signin", referer, postData, + false); + + String response = convertStream(request.getInputStream()); + logger.debug("Received content after login {}", response); + + String host = request.getURL().getHost(); + if (!host.equalsIgnoreCase(new URI(m_alexaServer).getHost())) { + return response; + } + if (response.contains("Amazon Alexa")) { + logger.debug("Response seems to be alexa app"); + } else { + logger.info("Response maybe not valid"); + } + + // verify login + if (!verifyLogin()) { + return response; + } + m_loginTime = new Date(); + logger.debug("Login succeeded"); + return null; + } + + private String makeRequestAndReturnString(String url) throws IOException, URISyntaxException { return makeRequestAndReturnString("GET", url, null, null, false); } private String makeRequestAndReturnString(String verb, String url, String referer, String postData, Boolean json) - throws Exception { + throws IOException, URISyntaxException { HttpsURLConnection connection = makeRequest(verb, url, referer, postData, json); - return getResponse(connection); + return convertStream(connection.getInputStream()); } - public boolean verifyLogin() throws Exception { + public boolean verifyLogin() throws IOException, URISyntaxException { String response = makeRequestAndReturnString(m_alexaServer + "/api/bootstrap?version=0"); Boolean result = response.contains("\"authenticated\":true"); return result; } - private String getResponse(HttpsURLConnection request) throws Exception { - InputStream input = request.getInputStream(); + public String convertStream(InputStream input) throws IOException { Scanner inputScanner = new Scanner(input); Scanner scannerWithoutDelimiter = inputScanner.useDelimiter("\\A"); String result = scannerWithoutDelimiter.hasNext() ? scannerWithoutDelimiter.next() : ""; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java new file mode 100644 index 0000000000000..3749c27380a81 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.openhab.binding.amazonechocontrol.handler.AccountHandler; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Forwards the login dialog from amazon to the user, so the user can enter a captcha + * + * @author Michael Geramb - Initial Contribution + */ +public class LoginServlet extends HttpServlet { + + private static final long serialVersionUID = -1453738923337413163L; + + private final Logger logger = LoggerFactory.getLogger(LoginServlet.class); + + HttpService httpService; + String servletUrlWithoutRoot; + String servletUrl; + Connection connection; + AccountHandler account; + AccountConfiguration configuration; + + public LoginServlet(HttpService httpService, String id, AccountHandler account, + AccountConfiguration configuration) { + this.httpService = httpService; + this.account = account; + this.configuration = configuration; + reCreateConnection(); + servletUrlWithoutRoot = "amazonechocontrol/" + id; + servletUrl = "/" + servletUrlWithoutRoot; + try { + httpService.registerServlet(servletUrl, this, null, httpService.createDefaultHttpContext()); + } catch (ServletException e) { + + logger.warn("Register servlet fails {}", e); + } catch (NamespaceException e) { + + logger.warn("Register servlet fails {}", e); + } + } + + private void reCreateConnection() { + this.connection = new Connection(configuration.email, configuration.password, configuration.amazonSite); + } + + public void dispose() { + httpService.unregister(servletUrl); + + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.addHeader("content-type", "text/html;charset=UTF-8"); + + Map 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 = map.get(name)[0]; + postDataBuilder.append(URLEncoder.encode(value, "UTF-8")); + if (name.equals("email") && !value.equalsIgnoreCase(configuration.email)) { + resp.getWriter().write( + "Email must match the configured email of your thing. Change your configuration or retype your email"); + return; + } + if (name.equals("password") && !value.equals(configuration.password)) { + resp.getWriter().write( + "Password must match the configured password of your thing. Change your configuration or retype your password"); + return; + } + } + String postData = postDataBuilder.toString(); + resp.addHeader("content-type", "text/html;charset=UTF-8"); + String errorHtml = null; + try { + errorHtml = connection.postLoginData(null, postData); + if (errorHtml == null) { + resp.getWriter().write("Login succeeded"); + account.setConnection(this.connection); + reCreateConnection(); + return; + } + errorHtml = replaceLoginUrl(errorHtml); + + } catch (URISyntaxException e) { + logger.error("Post login data failed {}", e); + errorHtml = "Internal error"; + } + resp.addHeader("Content-Location", servletUrl); + resp.getWriter().write(errorHtml); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String uri = req.getRequestURI().substring(servletUrl.length()); + if (!uri.startsWith("/")) { + String newUri = req.getServletPath() + "/" + uri; + resp.sendRedirect(newUri); + return; + } + logger.debug(uri); + try { + String html = this.connection.getLoginPage(); + html = replaceLoginUrl(html); + + resp.addHeader("content-type", "text/html;charset=UTF-8"); + resp.getWriter().write(html); + } catch (URISyntaxException e) { + + } + } + + private String replaceLoginUrl(String html) { + String result = html.replace("https://www." + connection.getAmazonSite() + "/", ""); + return result; + } +} From cbdcf764314917cfc296b3c3d41fe041f923460f Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Thu, 15 Mar 2018 19:35:48 +0100 Subject: [PATCH 19/56] [amazonechocontrol] Fix travis error Signed-off-by: Michael Geramb (github: mgeramb) --- .../binding/amazonechocontrol/internal/LoginServlet.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java index 3749c27380a81..a762613db4a42 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java @@ -110,7 +110,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S errorHtml = replaceLoginUrl(errorHtml); } catch (URISyntaxException e) { - logger.error("Post login data failed {}", e); + logger.error("Post login data failed with uri syntax error{}", e); errorHtml = "Internal error"; } resp.addHeader("Content-Location", servletUrl); @@ -119,14 +119,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - String uri = req.getRequestURI().substring(servletUrl.length()); + logger.debug("doGet {}", uri); if (!uri.startsWith("/")) { String newUri = req.getServletPath() + "/" + uri; resp.sendRedirect(newUri); return; } - logger.debug(uri); try { String html = this.connection.getLoginPage(); html = replaceLoginUrl(html); @@ -134,7 +133,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se resp.addHeader("content-type", "text/html;charset=UTF-8"); resp.getWriter().write(html); } catch (URISyntaxException e) { - + logger.error("get failed with uri syntax error {}", e); } } From 6fbf8eabd6c222c28697e474427c09bb68610e6f Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Mon, 26 Mar 2018 21:38:19 +0200 Subject: [PATCH 20/56] [amazonechocontrol] Improved new login method to handle code verification Typo in readme. Signed-off-by: Michael Geramb --- .../META-INF/MANIFEST.MF | 1 + .../README.md | 2 +- .../internal/Connection.java | 44 +++--- .../internal/LoginServlet.java | 146 ++++++++++++++---- 4 files changed, 138 insertions(+), 55 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF index e98935ddd8c31..f2b2a9e8b8776 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF +++ b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF @@ -12,6 +12,7 @@ Import-Package: com.google.gson;resolution:=optional, javax.servlet;version="3.1.0", javax.servlet.http;version="3.1.0", javax.ws.rs.core;version="2.0.1", + org.apache.commons.lang;version="2.6.0", org.eclipse.jdt.annotation;resolution:=optional, org.eclipse.smarthome.config.core, org.eclipse.smarthome.config.discovery, diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 380ba351939de..273f1d9a81b97 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -115,7 +115,7 @@ The flashbriefingprofile thing has no configuration parameters. It will be confi | bluetoothIdSelection| String | R/W | echo, echoshow, echospot, unknown | Bluetooth device selection. The selection currently only works in PaperUI | bluetooth | Switch | R/W | echo, echoshow, echospot, unknown | 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, unknown | User friendly name of the connected bluetooth device -| radioStationId | String | R/W | echo, echoshow, echospot, wha, unknown | Start playing of a TuneIn radio station by specifying it's id od stops playing if a empty string was provided +| radioStationId | String | R/W | echo, echoshow, echospot, wha, unknown | Start playing of a TuneIn radio station by specifying it's id or stops playing if a empty string was provided | radio | Switch | R/W | echo, echoshow, echospot, wha, unknown | 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, unknown | Start playing of a Amazon Music track by it's id od stops playing if a empty string was provided | amazonMusicPlayListId | String | W | echo, echoshow, echospot, wha, unknown | Write Only! Start playing of a Amazon Music playlist by specifying it's id od stops playing if a empty string was provided. Selection will only work in PaperUI diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index ecb4d2b2802e3..6a3ba434c6a88 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -236,8 +236,8 @@ public Date tryGetLoginTime() { return m_loginTime; } - private HttpsURLConnection makeRequest(String verb, String url, String referer, String postData, Boolean json) - throws IOException, URISyntaxException { + public HttpsURLConnection makeRequest(String verb, String url, String referer, String postData, boolean json, + boolean autoredirect) throws IOException, URISyntaxException { String currentUrl = url; for (int i = 0; i < 30; i++) // loop for handling redirect, using automatic redirect is not possible, because // all response headers must be catched @@ -319,18 +319,13 @@ private HttpsURLConnection makeRequest(String verb, String url, String referer, // get redirect location location = header.getValue().get(0); if (location != null) { - if (code == 302) { - logger.debug("Redirected to {}", location); - } location = uri.resolve(location).toString(); - // check for https if (location.toLowerCase().startsWith("http://")) { // always use https location = "https://" + location.substring(7); logger.debug("Redirect corrected to {}", location); } - } } } @@ -340,15 +335,20 @@ private HttpsURLConnection makeRequest(String verb, String url, String referer, return connection; } if (code == 302 && location != null) { + logger.debug("Redirected to {}", location); + currentUrl = location; - continue; + if (autoredirect) { + continue; + } + return connection; } } catch (IOException e) { logger.warn("Request to url '{}' fails with unkown error", url, e); throw e; } if (code != 200) { - throw new HttpException(code, connection.getResponseMessage()); + throw new HttpException(code, verb + " url '" + url + "' failed: " + connection.getResponseMessage()); } } throw new ConnectionException("To many redirects"); @@ -359,11 +359,11 @@ public boolean getIsLoggedIn() { } public String getLoginPage() throws IOException, URISyntaxException { + // clear session data m_cookieManager.getCookieStore().removeAll(); m_sessionId = null; m_loginTime = null; - logger.debug("Start Login to {}", m_alexaServer); // get login form String loginFormHtml = makeRequestAndReturnString(m_alexaServer); @@ -443,7 +443,7 @@ public String postLoginData(String optionalQueryParameters, String postData) String referer = "https://www." + m_amazonSite + "/ap/signin?" + queryParameters; URLConnection request = makeRequest("POST", "https://www." + m_amazonSite + "/ap/signin", referer, postData, - false); + false, true); String response = convertStream(request.getInputStream()); logger.debug("Received content after login {}", response); @@ -467,13 +467,13 @@ public String postLoginData(String optionalQueryParameters, String postData) return null; } - private String makeRequestAndReturnString(String url) throws IOException, URISyntaxException { + public String makeRequestAndReturnString(String url) throws IOException, URISyntaxException { return makeRequestAndReturnString("GET", url, null, null, false); } private String makeRequestAndReturnString(String verb, String url, String referer, String postData, Boolean json) throws IOException, URISyntaxException { - HttpsURLConnection connection = makeRequest(verb, url, referer, postData, json); + HttpsURLConnection connection = makeRequest(verb, url, referer, postData, json, true); return convertStream(connection.getInputStream()); } @@ -551,7 +551,7 @@ public JsonPlaylists getPlaylists(Device device) throws Exception { public void command(Device device, String command) throws Exception { String url = m_alexaServer + "/api/np/command?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType; - makeRequest("POST", url, null, command, true); + makeRequest("POST", url, null, command, true, true); } @@ -560,11 +560,11 @@ public void bluetooth(Device device, String address) throws Exception { // disconnect makeRequest("POST", m_alexaServer + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" + device.serialNumber, - null, "", true); + null, "", true, true); } else { makeRequest("POST", m_alexaServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber, null, - "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true); + "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true, true); } } @@ -577,7 +577,7 @@ public void playRadio(Device device, String stationId) throws Exception { m_alexaServer + "/api/tunein/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&guideId=" + stationId + "&contentType=station&callSign=&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId, - null, "", true); + null, "", true, true); } } @@ -590,7 +590,7 @@ public void playAmazonMusicTrack(Device device, String trackId) throws Exception m_alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId + "&shuffle=false", - null, command, true); + null, command, true, true); } } @@ -603,7 +603,7 @@ public void playAmazonMusicPlayList(Device device, String playListId) throws Exc m_alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId + "&shuffle=false", - null, command, true); + null, command, true, true); } } @@ -615,7 +615,7 @@ public void executeSequenceCommand(Device device, String command) throws Excepti + "\\\",\\\"deviceSerialNumber\\\":\\\"" + device.serialNumber + "\\\",\\\"customerId\\\":\\\"" + device.deviceOwnerCustomerId + "\\\",\\\"locale\\\":\\\"\\\"}}}\",\n" + " \"status\": \"ENABLED\" }"; - makeRequest("POST", m_alexaServer + "/api/behaviors/preview", null, json, true); + makeRequest("POST", m_alexaServer + "/api/behaviors/preview", null, json, true, true); } public void startRoutine(Device device, String utterance) throws Exception { @@ -669,7 +669,7 @@ public void startRoutine(Device device, String utterance) throws Exception { request.sequenceJson = sequenceJson; String requestJson = gson.toJson(request); - makeRequest("POST", m_alexaServer + "/api/behaviors/preview", null, requestJson, true); + makeRequest("POST", m_alexaServer + "/api/behaviors/preview", null, requestJson, true, true); } else { logger.warn("Routine {} not found", utterance); } @@ -698,7 +698,7 @@ public void setEnabledFlashBriefings(JsonFeed[] enabledFlashBriefing) throws Exc gsonBuilder.serializeNulls(); Gson gson = gsonBuilder.create(); String json = gson.toJson(enabled); - makeRequest("POST", m_alexaServer + "/api/content-skills/enabled-feeds", null, json, true); + makeRequest("POST", m_alexaServer + "/api/content-skills/enabled-feeds", null, json, true, true); } public JsonNotificationSound[] getNotificationSounds(Device device) throws Exception { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java index a762613db4a42..a069b980d237c 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java @@ -8,16 +8,20 @@ */ package org.openhab.binding.amazonechocontrol.internal; +import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; + import java.io.IOException; import java.net.URISyntaxException; import java.net.URLEncoder; import java.util.Map; +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.apache.commons.lang.StringEscapeUtils; import org.openhab.binding.amazonechocontrol.handler.AccountHandler; import org.osgi.service.http.HttpService; import org.osgi.service.http.NamespaceException; @@ -32,6 +36,7 @@ public class LoginServlet extends HttpServlet { private static final long serialVersionUID = -1453738923337413163L; + private static final String FORWARD_URI_PART = "/FORWARD/"; private final Logger logger = LoggerFactory.getLogger(LoginServlet.class); @@ -41,11 +46,13 @@ public class LoginServlet extends HttpServlet { Connection connection; AccountHandler account; AccountConfiguration configuration; + String id; public LoginServlet(HttpService httpService, String id, AccountHandler account, AccountConfiguration configuration) { this.httpService = httpService; this.account = account; + this.id = id; this.configuration = configuration; reCreateConnection(); servletUrlWithoutRoot = "amazonechocontrol/" + id; @@ -86,59 +93,134 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S String value = map.get(name)[0]; postDataBuilder.append(URLEncoder.encode(value, "UTF-8")); if (name.equals("email") && !value.equalsIgnoreCase(configuration.email)) { - resp.getWriter().write( - "Email must match the configured email of your thing. Change your configuration or retype your email"); + returnError(resp, + "Email must match the configured email of your thing. Change your configuration or retype your email."); return; } + if (name.equals("password") && !value.equals(configuration.password)) { - resp.getWriter().write( - "Password must match the configured password of your thing. Change your configuration or retype your password"); + returnError(resp, + "Password must match the configured password of your thing. Change your configuration or retype your password."); return; } } - String postData = postDataBuilder.toString(); - resp.addHeader("content-type", "text/html;charset=UTF-8"); - String errorHtml = null; - try { - errorHtml = connection.postLoginData(null, postData); - if (errorHtml == null) { - resp.getWriter().write("Login succeeded"); - account.setConnection(this.connection); - reCreateConnection(); - return; - } - errorHtml = replaceLoginUrl(errorHtml); - } catch (URISyntaxException e) { - logger.error("Post login data failed with uri syntax error{}", e); - errorHtml = "Internal error"; + String uri = req.getRequestURI(); + if (!uri.startsWith(servletUrl)) { + returnError(resp, "Invalid request uri '" + uri + "'"); + return; + } + String relativeUrl = uri.substring(servletUrl.length()).replace(FORWARD_URI_PART, "/"); + + String postUrl = "https://www." + connection.getAmazonSite() + relativeUrl; + String queryString = req.getQueryString(); + if (queryString != null && queryString.length() > 0) { + postUrl += "?" + queryString; } - resp.addHeader("Content-Location", servletUrl); - resp.getWriter().write(errorHtml); + String referer = "https://www." + connection.getAmazonSite(); + String postData = postDataBuilder.toString(); + HandleProxyRequest(resp, "POST", postUrl, referer, postData); + } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String uri = req.getRequestURI().substring(servletUrl.length()); - logger.debug("doGet {}", uri); - if (!uri.startsWith("/")) { - String newUri = req.getServletPath() + "/" + uri; - resp.sendRedirect(newUri); - return; + String queryString = req.getQueryString(); + if (queryString != null && queryString.length() > 0) { + uri += "?" + queryString; } + logger.debug("doGet {}", uri); try { + if (uri.startsWith(FORWARD_URI_PART)) { + + String getUrl = "https://www." + connection.getAmazonSite() + "/" + + uri.substring(FORWARD_URI_PART.length()); + + this.HandleProxyRequest(resp, "GET", getUrl, null, null); + return; + } + if (!uri.equals("/")) { + String newUri = req.getServletPath() + "/"; + resp.sendRedirect(newUri); + return; + } String html = this.connection.getLoginPage(); - html = replaceLoginUrl(html); + returnHtml(resp, html); - resp.addHeader("content-type", "text/html;charset=UTF-8"); - resp.getWriter().write(html); } catch (URISyntaxException e) { logger.error("get failed with uri syntax error {}", e); } } - private String replaceLoginUrl(String html) { - String result = html.replace("https://www." + connection.getAmazonSite() + "/", ""); - return result; + void HandleProxyRequest(HttpServletResponse resp, String verb, String url, String referer, String postData) + throws IOException { + + HttpsURLConnection urlConnection; + try { + urlConnection = connection.makeRequest(verb, url, referer, postData, false, false); + if (urlConnection.getResponseCode() == 302) { + { + String location = urlConnection.getHeaderField("location"); + if (location.contains("//alexa.")) { + if (connection.verifyLogin()) { + resp.getWriter().write( + "Login succeeded. The account thing sould now be online.
Check Thing in Paper UI"); + account.setConnection(this.connection); + reCreateConnection(); + return; + } + } + String startString = "https://www." + connection.getAmazonSite() + "/"; + String newLocation = null; + 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.addHeader("location", newLocation); + resp.sendError(302); + return; + } + returnError(resp, "Invalid redirect to '" + location + "'"); + return; + } + } + + } catch (URISyntaxException e) { + + returnError(resp, e.getLocalizedMessage()); + return; + } + + String response = connection.convertStream(urlConnection.getInputStream()); + returnHtml(resp, response); + } + + private void returnHtml(HttpServletResponse resp, String html) { + String resultHtml = html.replace("https://www." + connection.getAmazonSite() + "/", servletUrl + "/"); + resp.addHeader("content-type", "text/html;charset=UTF-8"); + try { + resp.getWriter().write(resultHtml); + } catch (IOException e) { + logger.error("return html failed with uri syntax error {}", e); + } + } + + void returnError(HttpServletResponse resp, String errorMessage) { + try { + + resp.getWriter().write("" + StringEscapeUtils.escapeHtml(errorMessage) + "
Try again"); + } catch (IOException e) { + logger.info("Returning error message failed {}", e); + } } } From 2ca24c81636a36c3454b5a5a4b70a79900743e7a Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sat, 31 Mar 2018 15:15:35 +0200 Subject: [PATCH 21/56] [amazonechocontrol] Code cleanup and comments added Signed-off-by: Michael Geramb (github: mgeramb) --- .../handler/AccountHandler.java | 164 ++++++++++-------- .../handler/EchoHandler.java | 20 +-- .../handler/FlashBriefingProfileHandler.java | 15 +- .../handler/SmartHomeBaseHandler.java | 6 +- .../handler/SmartHomeDimmerHandler.java | 4 +- .../handler/SmartHomeSwitchHandler.java | 6 +- .../internal/AccountConfiguration.java | 5 + .../internal/Connection.java | 134 +++++++------- .../internal/LoginServlet.java | 6 +- 9 files changed, 197 insertions(+), 163 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index eb07df4b9911e..b792963452a71 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -8,6 +8,8 @@ */ package org.openhab.binding.amazonechocontrol.handler; +import java.io.IOException; +import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Date; @@ -78,14 +80,9 @@ public AccountHandler(@NonNull Bridge bridge, @NonNull HttpService httpService) } - public Device[] getLastKnownDevices() { - Map temp = jsonSerialNumberDeviceMapping; - if (temp == null) { - return new Device[0]; - } - Device[] devices = new Device[temp.size()]; - temp.values().toArray(devices); - return devices; + @Override + public void initialize() { + start(); } @Override @@ -97,10 +94,14 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - @Override - public void initialize() { - - start(); + public Device[] getLastKnownDevices() { + Map temp = jsonSerialNumberDeviceMapping; + if (temp == null) { + return new Device[0]; + } + Device[] devices = new Device[temp.size()]; + temp.values().toArray(devices); + return devices; } public void addEchoHandler(@NonNull EchoHandler echoHandler) { @@ -150,22 +151,34 @@ private void initializeEchoHandler(@NonNull EchoHandler echoHandler, @NonNull Co BluetoothState state = null; JsonBluetoothStates states = null; - try { - if (temp.getIsLoggedIn()) { - states = temp.getBluetoothConnectionStates(); - } - } catch (Exception e) { - - logger.info("getBluetoothConnectionStates failed: {}", e); + if (temp.getIsLoggedIn()) { + states = temp.getBluetoothConnectionStates(); } + if (states != null) { state = states.findStateByDevice(device); } echoHandler.updateState(device, state); } + private void intializeChildDevice(@NonNull Connection connection, @NonNull EchoHandler child) { + Device deviceJson = this.findDeviceJson(child); + if (deviceJson != null) { + child.intialize(connection, deviceJson); + } + } + + @Override + public void handleRemoval() { + + cleanup(); + super.handleRemoval(); + } + @Override public void childHandlerDisposed(@NonNull ThingHandler childHandler, @NonNull Thing childThing) { + + // echo handler? if (childHandler instanceof EchoHandler) { synchronized (echoHandlers) { echoHandlers.remove(childHandler); @@ -176,11 +189,15 @@ public void childHandlerDisposed(@NonNull ThingHandler childHandler, @NonNull Th instance.removeExistingEchoHandler(childThing.getUID()); } } + + // flash briefing profile handler? if (childHandler instanceof FlashBriefingProfileHandler) { synchronized (flashBriefingProfileHandlers) { flashBriefingProfileHandlers.remove(childHandler); } } + + // smart home handler? if (childHandler instanceof SmartHomeBaseHandler) { synchronized (smartHomeHandlers) { smartHomeHandlers.remove(childHandler); @@ -194,13 +211,6 @@ public void childHandlerDisposed(@NonNull ThingHandler childHandler, @NonNull Th super.childHandlerDisposed(childHandler, childThing); } - @Override - public void handleRemoval() { - - cleanup(); - super.handleRemoval(); - } - @Override public void dispose() { LoginServlet loginServlet = this.loginServlet; @@ -271,7 +281,8 @@ private void start() { if (connection == null || !connection.getEmail().equals(config.email) || !connection.getPassword().equals(config.password) || !connection.getAmazonSite().equals(config.amazonSite)) { - connection = new Connection(config.email, config.password, config.amazonSite); + connection = new Connection(config.email, config.password, config.amazonSite, + this.getThing().getUID().getId()); } } if (this.loginServlet == null) { @@ -312,7 +323,7 @@ private void checkLogin() { if (!temp.verifyLogin()) { temp.logout(); } - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { logger.info("logout failed: {}", e.getMessage()); temp.logout(); } @@ -322,7 +333,8 @@ private void checkLogin() { { // Recreate session this.stateStorage.storeState("sessionStorage", ""); - temp = new Connection(temp.getEmail(), temp.getPassword(), temp.getAmazonSite()); + temp = new Connection(temp.getEmail(), temp.getPassword(), temp.getAmazonSite(), + this.getThing().getUID().getId()); } boolean loginIsValid = true; if (!temp.getIsLoggedIn()) { @@ -347,7 +359,12 @@ private void checkLogin() { throw e; } // give amazon some time - Thread.sleep(2000); + try { + Thread.sleep(2000); + } catch (InterruptedException exception) { + // throw the original exception + throw e; + } } } // store session data in property @@ -363,25 +380,29 @@ private void checkLogin() { loginIsValid = false; updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unknown host name '" + e.getMessage() + "'. Maybe your internet connection is offline"); - } catch (Exception e) { + } catch (IOException e) { + loginIsValid = false; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); + } catch (URISyntaxException e) { loginIsValid = false; updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } if (loginIsValid) { handleValidLogin(); } - } } } private void handleValidLogin() { - // update the device list + updateDeviceList(false); updateStatus(ThingStatus.ONLINE); + AmazonEchoDiscovery.addDiscoveryHandler(this); } + // used to set a valid connection from the web proxy login public void setConnection(Connection connection) { this.connection = connection; String serializedStorage = connection.serializeLoginData(); @@ -395,46 +416,42 @@ public void setConnection(Connection connection) { private void refreshData() { logger.debug("amazon account bridge refreshing data ..."); - try { - Connection temp = null; - synchronized (synchronizeConnection) { - temp = connection; - if (temp != null) { - if (!temp.getIsLoggedIn()) { - return; - } - } - } - if (temp == null) { - return; - } - - updateDeviceList(false); - JsonBluetoothStates states = null; - if (temp.getIsLoggedIn()) { - try { - states = temp.getBluetoothConnectionStates(); - } catch (Exception e) { - logger.info("getBluetoothConnectionStates failed: {}", e); + // check if logged in + Connection temp = null; + synchronized (synchronizeConnection) { + temp = connection; + if (temp != null) { + if (!temp.getIsLoggedIn()) { + return; } } + } + if (temp == null) { + return; + } - for (EchoHandler child : echoHandlers) { - Device device = findDeviceJson(child); - BluetoothState state = null; - if (states != null) { - state = states.findStateByDevice(device); - } - child.updateState(device, state); - } + // get all devices registered in the account + updateDeviceList(false); - updateStatus(ThingStatus.ONLINE); + // update bluetooth states + JsonBluetoothStates states = null; + if (temp.getIsLoggedIn()) { + states = temp.getBluetoothConnectionStates(); + } - } catch (Exception e) { - logger.warn("Update states of amazon account failed: {}", e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); + // forward device information to echo handler + for (EchoHandler child : echoHandlers) { + Device device = findDeviceJson(child); + BluetoothState state = null; + if (states != null) { + state = states.findStateByDevice(device); + } + child.updateState(device, state); } + + // update account state + updateStatus(ThingStatus.ONLINE); } public Device findDeviceJson(EchoHandler echoHandler) { @@ -490,7 +507,7 @@ public void updateDeviceList(boolean manualScan) { if (temp.getIsLoggedIn()) { devices = temp.getDeviceList(); } - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } if (devices != null) { @@ -525,7 +542,7 @@ public void updateDeviceList(boolean manualScan) { List smartHomeDevices = null; try { smartHomeDevices = temp.getSmartHomeDevices(); - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { logger.warn("Update smart home list failed {}", e); } if (smartHomeDevices != null) { @@ -542,7 +559,7 @@ public void setEnabledFlashBriefingsJson(String flashBriefingJson) { if (temp != null) { try { temp.setEnabledFlashBriefings(feeds); - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { logger.warn("Set flashbriefing profile failed {}", e); } } @@ -609,17 +626,10 @@ private void updateFlashBriefingProfiles(Connection temp) { Gson gson = new Gson(); this.currentFlashBriefingJson = gson.toJson(forSerializer); - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { logger.warn("get flash briefing profiles fails {}", e); } } - private void intializeChildDevice(@NonNull Connection connection, @NonNull EchoHandler child) { - Device deviceJson = this.findDeviceJson(child); - if (deviceJson != null) { - child.intialize(connection, deviceJson); - } - } - } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 81f0a1f522a2d..19270197955a7 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -10,6 +10,8 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; +import java.io.IOException; +import java.net.URISyntaxException; import java.util.HashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -409,12 +411,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { BluetoothState state = null; if (bluetoothRefresh) { JsonBluetoothStates states; - try { - states = temp.getBluetoothConnectionStates(); - state = states.findStateByDevice(device); - } catch (Exception e) { - logger.info("getBluetoothConnectionStates fails: {}", e); - } + states = temp.getBluetoothConnectionStates(); + state = states.findStateByDevice(device); } this.disableUpdate = false; @@ -429,7 +427,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { this.updateStateJob = scheduler.schedule(doRefresh, waitForUpdate, TimeUnit.MILLISECONDS); } - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { logger.info("handleCommand fails: {}", e); } } @@ -447,7 +445,7 @@ private void stopCurrentNotification() { if (tempConnection != null) { try { tempConnection.stopNotification(tempCurrentNotification); - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { logger.warn("Stop notification failed: {}", e); } } @@ -467,7 +465,7 @@ private void updateNotificationTimerState() { } } } - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { logger.warn("update notification state fails: {}", e); } if (stopCurrentNotifcation) { @@ -525,7 +523,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } else { logger.info("getPlayer fails: {}", e); } - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { logger.info("getPlayer fails: {}", e); } JsonMediaState mediaState = null; @@ -540,7 +538,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } else { logger.info("getMediaState fails: {}", e); } - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { logger.info("getMediaState fails: {}", e); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java index c13aecbf0df55..228025eedd130 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java @@ -10,6 +10,8 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; +import java.io.IOException; +import java.net.URISyntaxException; import java.util.HashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -171,7 +173,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } } - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { logger.warn("Handle command failed {}", e); } if (waitForUpdate >= 0) { @@ -214,13 +216,10 @@ public void initialize(AccountHandler handler, String currentConfigurationJson) private String saveCurrentProfile(AccountHandler connection) { String configurationJson = ""; - try { - configurationJson = connection.getEnabledFlashBriefingsJson(); - removeFromDiscovery(); - this.currentConfigurationJson = configurationJson; - } catch (Exception e) { - logger.warn("get flash briefing configuration failed {}", e); - } + configurationJson = connection.getEnabledFlashBriefingsJson(); + removeFromDiscovery(); + this.currentConfigurationJson = configurationJson; + if (!configurationJson.isEmpty()) { this.stateStorage.storeState("configurationJson", configurationJson); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java index fe744abb011bc..ac7c6464b72ab 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java @@ -10,6 +10,8 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.DEVICE_PROPERTY_ENTITY_ID; +import java.io.IOException; +import java.net.URISyntaxException; import java.util.HashMap; import org.eclipse.jdt.annotation.NonNull; @@ -100,13 +102,13 @@ public void handleCommand(ChannelUID channelUID, Command command) { String channelId = channelUID.getId(); try { handleCommand(temp, entityId, channelId, command); - } catch (Exception e) { + } catch (IOException | URISyntaxException e) { logger.warn("handle command {} for {} failed", command, channelUID, e); } } protected abstract void handleCommand(Connection connection, String entityId, String channelId, Command command) - throws Exception; + throws IOException, URISyntaxException; public static @Nullable SmartHomeBaseHandler find(ThingUID uid) { synchronized (instances) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java index 14d9fbdedff2e..14d438c519387 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java @@ -10,6 +10,8 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; +import java.io.IOException; +import java.net.URISyntaxException; import java.util.Locale; import org.eclipse.smarthome.core.library.types.OnOffType; @@ -42,7 +44,7 @@ public void initialize() { @Override public void handleCommand(Connection connection, String entityId, String channelId, Command command) - throws Exception { + throws IOException, URISyntaxException { if (channelId.equals(CHANNEL_SWITCH) || channelId.equals(CHANNEL_DIMMER)) { if (command == OnOffType.ON) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java index cac93b8542f68..6d03a324c42d8 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java @@ -10,6 +10,9 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.CHANNEL_SWITCH; +import java.io.IOException; +import java.net.URISyntaxException; + import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.types.Command; @@ -24,12 +27,11 @@ public class SmartHomeSwitchHandler extends SmartHomeBaseHandler { public SmartHomeSwitchHandler(Thing thing) { super(thing); - } @Override public void handleCommand(Connection connection, String entityId, String channelId, Command command) - throws Exception { + throws IOException, URISyntaxException { if (channelId.equals(CHANNEL_SWITCH)) { if (command == OnOffType.ON) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java index 8af39325b12d6..55f10d8025ccc 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java @@ -19,5 +19,10 @@ public class AccountConfiguration { public String password; public String amazonSite; public Integer pollingIntervalInSeconds; + + // The smarthome devices feature is currently not available in the configuration for public use, + // because there seems to be a problem in detecting and controlling devices, + // there seems to be different smarthome skill versions public Boolean discoverSmartHomeDevices; + } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 6a3ba434c6a88..6a72b8fe42051 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -71,8 +71,10 @@ public class Connection { private String m_sessionId; private Date m_loginTime; private String m_alexaServer; + private String m_accountThingId; - public Connection(String email, String password, String amazonSite) { + public Connection(String email, String password, String amazonSite, String accountThingId) { + m_accountThingId = accountThingId; m_email = email; m_password = password; @@ -93,6 +95,10 @@ public Connection(String email, String password, String amazonSite) { } + public Date tryGetLoginTime() { + return m_loginTime; + } + public String getEmail() { return m_email; } @@ -159,6 +165,8 @@ private String readValue(Scanner scanner) { } public Boolean tryRestoreLogin(String data) { + + // verify store data if (data == null || data.isEmpty()) { return false; } @@ -170,6 +178,7 @@ public Boolean tryRestoreLogin(String data) { return false; } + // check if email or password was changed in the mean time String email = scanner.nextLine(); if (!email.equals(this.m_email)) { scanner.close(); @@ -181,6 +190,8 @@ public Boolean tryRestoreLogin(String data) { scanner.close(); return false; } + + // Recreate session and cookies m_sessionId = scanner.nextLine(); Date loginTime = new Date(Long.parseLong(scanner.nextLine())); @@ -195,24 +206,16 @@ public Boolean tryRestoreLogin(String data) { String value = readValue(scanner); HttpCookie clientCookie = new HttpCookie(name, value); - clientCookie.setComment(readValue(scanner)); - clientCookie.setCommentURL(readValue(scanner)); - clientCookie.setDomain(readValue(scanner)); - clientCookie.setMaxAge(Long.parseLong(readValue(scanner))); - clientCookie.setPath(readValue(scanner)); - clientCookie.setPortlist(readValue(scanner)); - clientCookie.setVersion(Integer.parseInt(readValue(scanner))); - clientCookie.setSecure(Boolean.parseBoolean(readValue(scanner))); - clientCookie.setDiscard(Boolean.parseBoolean(readValue(scanner))); + cookieStore.add(null, clientCookie); } @@ -221,10 +224,13 @@ public Boolean tryRestoreLogin(String data) { if (verifyLogin()) { m_loginTime = loginTime; return true; - } - } catch (Exception e) { + } catch (IOException e) { + logger.info("verify login fails with io exception: {}", e); + } catch (URISyntaxException e) { + logger.error("verify login fails with uri syntax exception: {}", e); } + // anything goes wrong, remove session data cookieStore.removeAll(); m_sessionId = null; m_loginTime = null; @@ -232,8 +238,24 @@ public Boolean tryRestoreLogin(String data) { } - public Date tryGetLoginTime() { - return m_loginTime; + public String convertStream(InputStream input) throws IOException { + Scanner inputScanner = new Scanner(input); + Scanner scannerWithoutDelimiter = inputScanner.useDelimiter("\\A"); + String result = scannerWithoutDelimiter.hasNext() ? scannerWithoutDelimiter.next() : ""; + inputScanner.close(); + scannerWithoutDelimiter.close(); + input.close(); + return result; + } + + public String makeRequestAndReturnString(String url) throws IOException, URISyntaxException { + return makeRequestAndReturnString("GET", url, null, null, false); + } + + private String makeRequestAndReturnString(String verb, String url, String referer, String postData, Boolean json) + throws IOException, URISyntaxException { + HttpsURLConnection connection = makeRequest(verb, url, referer, postData, json, true); + return convertStream(connection.getInputStream()); } public HttpsURLConnection makeRequest(String verb, String url, String referer, String postData, boolean json, @@ -419,7 +441,9 @@ public void makeLogin() throws IOException, URISyntaxException { String postData = postDataBuilder.toString(); if (postLoginData(queryParameters, postData) != null) { - throw new ConnectionException("Login fails."); + throw new ConnectionException( + "Login fails. Check your credentials and try to login with your webbrowser to http(s):///amazonechocontrol/" + + m_accountThingId); } } catch (Exception e) { @@ -428,20 +452,24 @@ public void makeLogin() throws IOException, URISyntaxException { m_sessionId = null; m_loginTime = null; logger.info("Login failed:{} ", e); + // rethrow throw e; } - } public String postLoginData(String optionalQueryParameters, String postData) throws IOException, URISyntaxException { - // post login data + + // build query parameters String queryParameters = optionalQueryParameters; if (queryParameters == null) { queryParameters = "session-id=" + URLEncoder.encode(m_sessionId, "UTF-8"); } + // build referer link String referer = "https://www." + m_amazonSite + "/ap/signin?" + queryParameters; + + // make the request URLConnection request = makeRequest("POST", "https://www." + m_amazonSite + "/ap/signin", referer, postData, false, true); @@ -467,16 +495,6 @@ public String postLoginData(String optionalQueryParameters, String postData) return null; } - public String makeRequestAndReturnString(String url) throws IOException, URISyntaxException { - return makeRequestAndReturnString("GET", url, null, null, false); - } - - private String makeRequestAndReturnString(String verb, String url, String referer, String postData, Boolean json) - throws IOException, URISyntaxException { - HttpsURLConnection connection = makeRequest(verb, url, referer, postData, json, true); - return convertStream(connection.getInputStream()); - } - public boolean verifyLogin() throws IOException, URISyntaxException { String response = makeRequestAndReturnString(m_alexaServer + "/api/bootstrap?version=0"); Boolean result = response.contains("\"authenticated\":true"); @@ -484,16 +502,6 @@ public boolean verifyLogin() throws IOException, URISyntaxException { return result; } - public String convertStream(InputStream input) throws IOException { - Scanner inputScanner = new Scanner(input); - Scanner scannerWithoutDelimiter = inputScanner.useDelimiter("\\A"); - String result = scannerWithoutDelimiter.hasNext() ? scannerWithoutDelimiter.next() : ""; - inputScanner.close(); - scannerWithoutDelimiter.close(); - input.close(); - return result; - } - public void logout() { m_cookieManager.getCookieStore().removeAll(); m_sessionId = null; @@ -514,33 +522,39 @@ private T parseJson(String json, Class type) { // commands and states - public Device[] getDeviceList() throws Exception { + public Device[] getDeviceList() throws IOException, URISyntaxException { String json = makeRequestAndReturnString(m_alexaServer + "/api/devices-v2/device?cached=false"); JsonDevices devices = parseJson(json, JsonDevices.class); return devices.devices; } - public JsonPlayerState getPlayer(Device device) throws Exception { + public JsonPlayerState getPlayer(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString(m_alexaServer + "/api/np/player?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&screenWidth=1440"); JsonPlayerState playerState = parseJson(json, JsonPlayerState.class); return playerState; } - public JsonMediaState getMediaState(Device device) throws Exception { + public JsonMediaState getMediaState(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString(m_alexaServer + "/api/media/state?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType); JsonMediaState mediaState = parseJson(json, JsonMediaState.class); return mediaState; } - public JsonBluetoothStates getBluetoothConnectionStates() throws Exception { - String json = makeRequestAndReturnString(m_alexaServer + "/api/bluetooth?cached=true"); + public JsonBluetoothStates getBluetoothConnectionStates() { + String json; + try { + json = makeRequestAndReturnString(m_alexaServer + "/api/bluetooth?cached=true"); + } catch (IOException | URISyntaxException e) { + logger.debug(e.getMessage()); + return new JsonBluetoothStates(); + } JsonBluetoothStates bluetoothStates = parseJson(json, JsonBluetoothStates.class); return bluetoothStates; } - public JsonPlaylists getPlaylists(Device device) throws Exception { + public JsonPlaylists getPlaylists(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString( m_alexaServer + "/api/cloudplayer/playlists?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId); @@ -548,14 +562,14 @@ public JsonPlaylists getPlaylists(Device device) throws Exception { return playlists; } - public void command(Device device, String command) throws Exception { + public void command(Device device, String command) throws IOException, URISyntaxException { String url = m_alexaServer + "/api/np/command?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType; makeRequest("POST", url, null, command, true, true); } - public void bluetooth(Device device, String address) throws Exception { + public void bluetooth(Device device, String address) throws IOException, URISyntaxException { if (address == null || address.isEmpty()) { // disconnect makeRequest("POST", @@ -569,7 +583,7 @@ public void bluetooth(Device device, String address) throws Exception { } } - public void playRadio(Device device, String stationId) throws Exception { + public void playRadio(Device device, String stationId) throws IOException, URISyntaxException { if (stationId == null || stationId.isEmpty()) { command(device, "{\"type\":\"PauseCommand\"}"); } else { @@ -581,7 +595,7 @@ public void playRadio(Device device, String stationId) throws Exception { } } - public void playAmazonMusicTrack(Device device, String trackId) throws Exception { + public void playAmazonMusicTrack(Device device, String trackId) throws IOException, URISyntaxException { if (trackId == null || trackId.isEmpty()) { command(device, "{\"type\":\"PauseCommand\"}"); } else { @@ -594,7 +608,7 @@ public void playAmazonMusicTrack(Device device, String trackId) throws Exception } } - public void playAmazonMusicPlayList(Device device, String playListId) throws Exception { + public void playAmazonMusicPlayList(Device device, String playListId) throws IOException, URISyntaxException { if (playListId == null || playListId.isEmpty()) { command(device, "{\"type\":\"PauseCommand\"}"); } else { @@ -608,7 +622,7 @@ public void playAmazonMusicPlayList(Device device, String playListId) throws Exc } // commands: Alexa.Weather.Play, Alexa.Traffic.Play, Alexa.FlashBriefing.Play - public void executeSequenceCommand(Device device, String command) throws Exception { + public void executeSequenceCommand(Device device, String command) throws IOException, URISyntaxException { String json = "{ \"behaviorId\": \"amzn1.alexa.automation.00000000-0000-0000-0000-000000000000\", " + " \"sequenceJson\": \"{\\\"@type\\\":\\\"com.amazon.alexa.behaviors.model.Sequence\\\",\\\"startNode\\\":{\\\"@type\\\":\\\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\\\",\\\"type\\\":\\\"" + command + "\\\",\\\"operationPayload\\\":{\\\"deviceType\\\":\\\"" + device.deviceType @@ -618,7 +632,7 @@ public void executeSequenceCommand(Device device, String command) throws Excepti makeRequest("POST", m_alexaServer + "/api/behaviors/preview", null, json, true, true); } - public void startRoutine(Device device, String utterance) throws Exception { + public void startRoutine(Device device, String utterance) throws IOException, URISyntaxException { JsonAutomation found = null; String deviceLocale = null; for (JsonAutomation routine : getRoutines()) { @@ -675,13 +689,13 @@ public void startRoutine(Device device, String utterance) throws Exception { } } - public JsonAutomation[] getRoutines() throws Exception { + public JsonAutomation[] getRoutines() throws IOException, URISyntaxException { String json = makeRequestAndReturnString("GET", m_alexaServer + "/api/behaviors/automations", null, null, true); JsonAutomation[] result = parseJson(json, JsonAutomation[].class); return result; } - public JsonFeed[] getEnabledFlashBriefings() throws Exception { + public JsonFeed[] getEnabledFlashBriefings() throws IOException, URISyntaxException { String json = makeRequestAndReturnString("GET", m_alexaServer + "/api/content-skills/enabled-feeds", null, null, true); JsonEnabledFeeds result = parseJson(json, JsonEnabledFeeds.class); @@ -691,7 +705,7 @@ public JsonFeed[] getEnabledFlashBriefings() throws Exception { return new JsonFeed[0]; } - public void setEnabledFlashBriefings(JsonFeed[] enabledFlashBriefing) throws Exception { + public void setEnabledFlashBriefings(JsonFeed[] enabledFlashBriefing) throws IOException, URISyntaxException { JsonEnabledFeeds enabled = new JsonEnabledFeeds(); enabled.enabledFeeds = enabledFlashBriefing; GsonBuilder gsonBuilder = new GsonBuilder(); @@ -701,7 +715,7 @@ public void setEnabledFlashBriefings(JsonFeed[] enabledFlashBriefing) throws Exc makeRequest("POST", m_alexaServer + "/api/content-skills/enabled-feeds", null, json, true, true); } - public JsonNotificationSound[] getNotificationSounds(Device device) throws Exception { + public JsonNotificationSound[] getNotificationSounds(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString( "GET", m_alexaServer + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&softwareVersion=" + device.softwareVersion, @@ -714,7 +728,7 @@ public JsonNotificationSound[] getNotificationSounds(Device device) throws Excep } public JsonNotificationResponse notification(Device device, String type, String label, JsonNotificationSound sound) - throws Exception { + throws IOException, URISyntaxException { Date date = new Date(new Date().getTime()); long createdDate = date.getTime(); @@ -747,18 +761,19 @@ public JsonNotificationResponse notification(Device device, String type, String } - public void stopNotification(JsonNotificationResponse notification) throws Exception { + public void stopNotification(JsonNotificationResponse notification) throws IOException, URISyntaxException { makeRequestAndReturnString("DELETE", m_alexaServer + "/api/notifications/" + notification.id, null, null, true); } - public JsonNotificationResponse getNotificationState(JsonNotificationResponse notification) throws Exception { + public JsonNotificationResponse getNotificationState(JsonNotificationResponse notification) + throws IOException, URISyntaxException { String response = makeRequestAndReturnString("GET", m_alexaServer + "/api/notifications/" + notification.id, null, null, true); JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); return result; } - public List getSmartHomeDevices() throws Exception { + public List getSmartHomeDevices() throws IOException, URISyntaxException { try { String json = makeRequestAndReturnString("GET", m_alexaServer + "/api/phoenix", null, null, true); logger.debug("getSmartHomeDevices result: {}", json); @@ -795,7 +810,7 @@ private void searchSmartHomeDevicesRecursive(Gson gson, Object jsonNode, List Date: Sat, 31 Mar 2018 19:28:16 +0200 Subject: [PATCH 22/56] [amazonechocontrol] Travis error and warnings fixed Signed-off-by: Michael Geramb (github: mgeramb) --- .../META-INF/MANIFEST.MF | 10 +++++----- .../binding/amazonechocontrol/internal/Connection.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF index f2b2a9e8b8776..bb6c935559107 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF +++ b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF @@ -9,10 +9,10 @@ Bundle-Vendor: openHAB Bundle-Version: 2.3.0.qualifier Import-Package: com.google.gson;resolution:=optional, com.google.gson.annotations, - javax.servlet;version="3.1.0", - javax.servlet.http;version="3.1.0", - javax.ws.rs.core;version="2.0.1", - org.apache.commons.lang;version="2.6.0", + javax.servlet, + javax.servlet.http, + javax.ws.rs.core, + org.apache.commons.lang, org.eclipse.jdt.annotation;resolution:=optional, org.eclipse.smarthome.config.core, org.eclipse.smarthome.config.discovery, @@ -28,7 +28,7 @@ Import-Package: com.google.gson;resolution:=optional, org.openhab.binding.amazonechocontrol.handler, org.osgi.framework, org.osgi.service.component.annotations;resolution:=optional, - org.osgi.service.http;version="1.2.1", + org.osgi.service.http, org.slf4j Service-Component: OSGI-INF/*.xml Export-Package: diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 6a72b8fe42051..8aa39d3177fae 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -547,7 +547,7 @@ public JsonBluetoothStates getBluetoothConnectionStates() { try { json = makeRequestAndReturnString(m_alexaServer + "/api/bluetooth?cached=true"); } catch (IOException | URISyntaxException e) { - logger.debug(e.getMessage()); + logger.debug("failed to get bluetooth state: {}", e.getMessage()); return new JsonBluetoothStates(); } JsonBluetoothStates bluetoothStates = parseJson(json, JsonBluetoothStates.class); From 87bfddb12d81c4d4cf572702724331cfab796c33 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 4 Apr 2018 19:47:25 +0200 Subject: [PATCH 23/56] [amazonechocontrol] Fixed bug in proxy login Handle for login tries if connection is already valid Diagnostic page for devices added Signed-off-by: Michael Geramb (github: mgeramb) --- .../handler/AccountHandler.java | 4 ++-- .../internal/Connection.java | 14 +++++++---- .../internal/LoginServlet.java | 23 ++++++++++++++++++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index b792963452a71..db74e2edbbd34 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -352,9 +352,9 @@ private void checkLogin() { temp.makeLogin(); break; } catch (ConnectionException e) { - // Up to 3 retries for login + // Up to 2 retries for login retry++; - if (retry >= 3) { + if (retry >= 2) { temp.logout(); throw e; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 8aa39d3177fae..202d30b0dd29e 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -451,7 +451,7 @@ public void makeLogin() throws IOException, URISyntaxException { m_cookieManager.getCookieStore().removeAll(); m_sessionId = null; m_loginTime = null; - logger.info("Login failed:{} ", e); + logger.info("Login failed: {}", e.getLocalizedMessage()); // rethrow throw e; } @@ -490,7 +490,6 @@ public String postLoginData(String optionalQueryParameters, String postData) if (!verifyLogin()) { return response; } - m_loginTime = new Date(); logger.debug("Login succeeded"); return null; } @@ -498,7 +497,9 @@ public String postLoginData(String optionalQueryParameters, String postData) public boolean verifyLogin() throws IOException, URISyntaxException { String response = makeRequestAndReturnString(m_alexaServer + "/api/bootstrap?version=0"); Boolean result = response.contains("\"authenticated\":true"); - + if (result && m_loginTime == null) { + m_loginTime = new Date(); + } return result; } @@ -523,11 +524,16 @@ private T parseJson(String json, Class type) { // commands and states public Device[] getDeviceList() throws IOException, URISyntaxException { - String json = makeRequestAndReturnString(m_alexaServer + "/api/devices-v2/device?cached=false"); + String json = getDeviceListJson(); JsonDevices devices = parseJson(json, JsonDevices.class); return devices.devices; } + public String getDeviceListJson() throws IOException, URISyntaxException { + String json = makeRequestAndReturnString(m_alexaServer + "/api/devices-v2/device?cached=false"); + return json; + } + public JsonPlayerState getPlayer(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString(m_alexaServer + "/api/np/player?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&screenWidth=1440"); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java index 56e32a7bccf62..943774ffbf296 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java @@ -142,11 +142,32 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se this.HandleProxyRequest(resp, "GET", getUrl, null, null); return; } + + Connection connection = this.account.findConnection(); + if (connection != null && connection.verifyLogin()) { + + // handle diagnostic commands + if (uri.equals("/devices") || uri.equals("/devices/")) { + returnHtml(resp, + "" + StringEscapeUtils.escapeHtml(connection.getDeviceListJson()) + ""); + return; + } + + // return hint that everything is ok + resp.getWriter().write( + "The Account is already logged in. The account thing should be online.
Check Thing in Paper UI"); + + return; + } + if (!uri.equals("/")) { String newUri = req.getServletPath() + "/"; resp.sendRedirect(newUri); return; } + String html = this.connection.getLoginPage(); returnHtml(resp, html); @@ -167,7 +188,7 @@ void HandleProxyRequest(HttpServletResponse resp, String verb, String url, Strin if (location.contains("//alexa.")) { if (connection.verifyLogin()) { resp.getWriter().write( - "Login succeeded. The account thing sould now be online.
Check Thing in Paper UI"); account.setConnection(this.connection); From b0d7cc5421fd76e050dfe617dff36831caa74b05 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Fri, 6 Apr 2018 22:19:20 +0200 Subject: [PATCH 24/56] [amazonechocontrol] Code cleanup for review (e.g. NullableAsDefault) Signed-off-by: Michael Geramb (github: mgeramb) --- .../ESH-INF/binding/binding.xml | 14 +- .../ESH-INF/thing/thing-types.xml | 1176 ++++++----------- .../README.md | 2 +- .../handler/AccountHandler.java | 156 ++- .../handler/EchoHandler.java | 95 +- .../handler/FlashBriefingProfileHandler.java | 57 +- .../handler/SmartHomeBaseHandler.java | 11 +- .../handler/SmartHomeDimmerHandler.java | 3 +- .../handler/SmartHomeSwitchHandler.java | 2 + .../internal/AccountConfiguration.java | 9 + .../internal/Connection.java | 272 ++-- .../internal/ConnectionException.java | 3 + .../internal/HttpException.java | 3 + .../internal/LoginServlet.java | 36 +- .../internal/StateStorage.java | 49 +- .../discovery/AmazonEchoDiscovery.java | 55 +- .../internal/jsons/JsonAutomation.java | 28 +- .../internal/jsons/JsonBluetoothStates.java | 32 +- .../internal/jsons/JsonDevices.java | 22 +- .../internal/jsons/JsonEnabledFeeds.java | 6 +- .../internal/jsons/JsonFeed.java | 12 +- .../internal/jsons/JsonMediaState.java | 78 +- .../internal/jsons/JsonNetworkDetails.java | 6 +- .../jsons/JsonNotificationRequest.java | 28 +- .../jsons/JsonNotificationResponse.java | 14 +- .../internal/jsons/JsonNotificationSound.java | 14 +- .../jsons/JsonNotificationSounds.java | 6 +- .../internal/jsons/JsonPlayerState.java | 28 +- .../internal/jsons/JsonPlaylists.java | 10 +- .../internal/jsons/JsonSmartHomeDevice.java | 12 +- .../jsons/JsonStartRoutineRequest.java | 10 +- ...onEchoDynamicStateDescriptionProvider.java | 26 +- 32 files changed, 1059 insertions(+), 1216 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml index 55683c3c4aa3b..28f384fcdf1f8 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml @@ -1,12 +1,6 @@ - - - Amazon Echo Control Binding - - - Binding for controlling Amazon Echo (Alexa). This binding enables openhab to control the volume, playing state, bluetooth connection of your amazon echo devices. - - - Michael Geramb - + + Amazon Echo Control Binding + Binding to control Amazon Echo devices (Alexa). This binding enables openhab to control the volume, playing state, bluetooth connection of your amazon echo devices. + Michael Geramb \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 4906fd4f4ce64..57e301c6c99ef 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -1,739 +1,439 @@ - - - - Amazon Account where your amazon echo is registered. - - - - - - - false - - - - - - - - - - - Select the site where your amazon account is created. - - - - - - - - Enter the email address of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! - - - - - - - password - - - - Enter the password of the amazon account which is used for the amazon echo devices. IMPORTANT: If the account thing does not go online and reports and login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. - - - - - 60 - - - - Refresh state interval in seconds. Lower time causes more network traffic. - - - Seconds - - - - - - - - - - - - Amazon Echo device (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - - - Amazon Echo Spot device - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - - - Amazon Echo Spot device - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - - - Amazon Multiroom Music - - - - - - - - - - - - - - - - - - serialNumber - - - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - - - Unknown Echo Device. Warning: Maybe not all channels will be supported from the device - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - - - Store and load a flash briefing configuration - - - - - - - - - - - - - - Smart Home Switch - - - - - - entityId - - - - - - Use the search feature of openHAB to get the id - - - - - - - - - - - Smart Home Dimmer - - - - - - - entityId - - - - - - Let discover the device to get the id - - - - - - - Switch - - - - Save the current flash briefing configuration (Write only) - - - - - Switch - - - - Activate this flash briefing configuration - - - - - String - - - - Plays the briefing on the device (serial number or name, write only) - - - - - Switch - - - - Turns the device on or off - - - - - Dimmer - - - - Dimmer control - - - - - String - - - - Connected bluetooth device - - - - - - - String - - - - Id of the radio station - - - - - String - - - - Speak the reminder and send a notification to the Alexa app - - - - - Switch - - - - Starts the flash briefing (Write Only) - - - - - Switch - - - - Starts the weather report (Write Only) - - - - - Switch - - - - Starts the traffic news (Write Only) - - - - - String - - - - Type in what you normally say to Alexa without the preceding "Alexa," (Write Only) - - - - - String - - - - Plays an alarm sound - - - - - String - - - - Id of the amazon music track - - - - - Switch - - - - Amazon Music turned on - - - - - String - - - - Amazon Music play list id (Write only, no current state) - - - - - String - - - - Is of the playlist which was started with openHAB - - - - - String - - - - Name of music provider - - - - - - - String - - - - MAC-Address of the bluetooth connected device - - - - - String - - - - Bluetooth connection selection (Currently only in PaperUI) - - - - - String - - - - Url of the album image or radio station logo - - - - - - - String - - - - Title - - - - - - - String - - - - Subtitle 1 - - - - - - - String - - - - Subtitle 2 - - - - - - - Switch - - - - Radio turned on - - - - - Switch - - - - Connect to last used device - - - - - Switch - - - - Loop - - - - - Switch - - - - Shuffle play - - - - - Player - - - - Music Player - - - - - Dimmer - - - - Volume of the sound - - - + + + + Amazon Account where your amazon echo is registered. + + + + + false + + + + + + + + + Select the site where your amazon account is created. + + + + + Enter the email address of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! + + + + password + + Enter the password of the amazon account which is used for the amazon echo devices. IMPORTANT: If the account thing does not go online and reports and login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. + + + 60 + + Refresh state interval in seconds. Lower time causes more network traffic. + Seconds + + + + + + + + + + Amazon Echo device (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + Amazon Echo Spot device + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + Amazon Echo Spot device + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + Amazon Multiroom Music + + + + + + + + + + + + + + + + serialNumber + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + Unknown Echo Device. Warning: Maybe not all channels will be supported from the device + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + Store and load a flash briefing configuration + + + + + + + + + + + + Smart Home Switch + + + + entityId + + + + Use the search feature of openHAB to get the id + + + + + + + + + Smart Home Dimmer + + + + + entityId + + + + Let discover the device to get the id + + + + + Switch + + Save the current flash briefing configuration (Write only) + + + Switch + + Activate this flash briefing configuration + + + String + + Plays the briefing on the device (serial number or name, write only) + + + Switch + + Turns the device on or off + + + Dimmer + + Dimmer control + + + String + + Connected bluetooth device + + + + String + + Id of the radio station + + + String + + Speak the reminder and send a notification to the Alexa app + + + Switch + + Starts the flash briefing (Write Only) + + + Switch + + Starts the weather report (Write Only) + + + Switch + + Starts the traffic news (Write Only) + + + String + + Type in what you normally say to Alexa without the preceding "Alexa," (Write Only) + + + String + + Plays an alarm sound + + + String + + Id of the amazon music track + + + Switch + + Amazon Music turned on + + + String + + Amazon Music play list id (Write only, no current state) + + + String + + Is of the playlist which was started with openHAB + + + String + + Name of music provider + + + + String + + MAC-Address of the bluetooth connected device + + + String + + Bluetooth connection selection (Currently only in PaperUI) + + + String + + Url of the album image or radio station logo + + + + String + + Title + + + + String + + Subtitle 1 + + + + String + + Subtitle 2 + + + + Switch + + Radio turned on + + + Switch + + Connect to last used device + + + Switch + + Loop + + + Switch + + Shuffle play + + + Player + + Music Player + + + Dimmer + + Volume of the sound + + \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 273f1d9a81b97..6f2b37743ea0a 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -1,6 +1,6 @@ # Amazon Echo Control Binding -This binding let control openHAB Amazon Echo devices (Alexa). +This binding can control Amazon Echo devices (Alexa) from openhab. The idea for writing this binding came from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German). Thank you Alex! diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index db74e2edbbd34..deb23934acbe4 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -19,7 +19,10 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; @@ -52,27 +55,27 @@ * * @author Michael Geramb - Initial Contribution */ +@NonNullByDefault public class AccountHandler extends BaseBridgeHandler implements IAmazonEchoDiscovery { private final Logger logger = LoggerFactory.getLogger(AccountHandler.class); private StateStorage stateStorage; - private AccountConfiguration config; - private Connection connection; - private List echoHandlers = new ArrayList<>(); - private List smartHomeHandlers = new ArrayList<>(); - private List flashBriefingProfileHandlers = new ArrayList<>(); - private Object synchronizeConnection = new Object(); + private @Nullable Connection connection; + private final List echoHandlers = new ArrayList<>(); + private final List smartHomeHandlers = new ArrayList<>(); + private final List flashBriefingProfileHandlers = new ArrayList<>(); + private final Object synchronizeConnection = new Object(); private Map jsonSerialNumberDeviceMapping = new HashMap<>(); - private ScheduledFuture refreshJob; - private ScheduledFuture refreshLogin; + private @Nullable ScheduledFuture refreshJob; + private @Nullable ScheduledFuture refreshLogin; private boolean updateSmartHomeDeviceList; private boolean discoverFlashProfiles; private boolean smartHodeDeviceListEnabled; private String currentFlashBriefingJson = ""; - private HttpService httpService; - private LoginServlet loginServlet; + private final HttpService httpService; + private @Nullable LoginServlet loginServlet; - public AccountHandler(@NonNull Bridge bridge, @NonNull HttpService httpService) { + public AccountHandler(Bridge bridge, HttpService httpService) { super(bridge); this.httpService = httpService; stateStorage = new StateStorage(bridge); @@ -95,28 +98,24 @@ public void handleCommand(ChannelUID channelUID, Command command) { } public Device[] getLastKnownDevices() { - Map temp = jsonSerialNumberDeviceMapping; - if (temp == null) { - return new Device[0]; - } - Device[] devices = new Device[temp.size()]; - temp.values().toArray(devices); + Device[] devices = new Device[jsonSerialNumberDeviceMapping.size()]; + jsonSerialNumberDeviceMapping.values().toArray(devices); return devices; } - public void addEchoHandler(@NonNull EchoHandler echoHandler) { + public void addEchoHandler(EchoHandler echoHandler) { synchronized (echoHandlers) { if (!echoHandlers.contains(echoHandler)) { echoHandlers.add(echoHandler); } } - Connection temp = connection; - if (temp != null) { - initializeEchoHandler(echoHandler, temp); + Connection connection = this.connection; + if (connection != null) { + initializeEchoHandler(echoHandler, connection); } } - public void addFlashBriefingProfileHandler(@NonNull FlashBriefingProfileHandler flashBriefingProfileHandler) { + public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) { synchronized (flashBriefingProfileHandlers) { if (!flashBriefingProfileHandlers.contains(flashBriefingProfileHandler)) { flashBriefingProfileHandlers.add(flashBriefingProfileHandler); @@ -132,27 +131,28 @@ public void addFlashBriefingProfileHandler(@NonNull FlashBriefingProfileHandler } } - public void addSmartHomeHandler(@NonNull SmartHomeBaseHandler smartHomeHandler) { + public void addSmartHomeHandler(SmartHomeBaseHandler smartHomeHandler) { synchronized (smartHomeHandlers) { if (!smartHomeHandlers.contains(smartHomeHandler)) { smartHomeHandlers.add(smartHomeHandler); } } - Connection temp = connection; - if (temp != null) { - smartHomeHandler.initialize(temp); + Connection connection = this.connection; + if (connection != null) { + smartHomeHandler.initialize(connection); } } - private void initializeEchoHandler(@NonNull EchoHandler echoHandler, @NonNull Connection temp) { - intializeChildDevice(temp, echoHandler); + private void initializeEchoHandler(EchoHandler echoHandler, Connection connection) { + intializeChildDevice(connection, echoHandler); + @Nullable Device device = findDeviceJson(echoHandler); BluetoothState state = null; JsonBluetoothStates states = null; - if (temp.getIsLoggedIn()) { - states = temp.getBluetoothConnectionStates(); + if (connection.getIsLoggedIn()) { + states = connection.getBluetoothConnectionStates(); } if (states != null) { @@ -161,7 +161,8 @@ private void initializeEchoHandler(@NonNull EchoHandler echoHandler, @NonNull Co echoHandler.updateState(device, state); } - private void intializeChildDevice(@NonNull Connection connection, @NonNull EchoHandler child) { + private void intializeChildDevice(Connection connection, EchoHandler child) { + Device deviceJson = this.findDeviceJson(child); if (deviceJson != null) { child.intialize(connection, deviceJson); @@ -241,23 +242,27 @@ private void cleanup() { private void start() { logger.debug("amazon account bridge starting handler ..."); - config = getConfigAs(AccountConfiguration.class); - if (config.amazonSite == null || config.amazonSite.isEmpty()) { + AccountConfiguration config = getConfigAs(AccountConfiguration.class); + + if (StringUtils.isEmpty(config.amazonSite)) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Amazon site not configured"); cleanup(); return; } - if (config.email == null || config.email.isEmpty()) { + String email = config.email; + if (StringUtils.isEmpty(email)) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Account email not configured"); cleanup(); return; } - if (config.password == null || config.password.isEmpty()) { + String password = config.password; + if (StringUtils.isEmpty(password)) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Account password not configured"); cleanup(); return; } - if (config.pollingIntervalInSeconds == null) { + Integer pollingIntervalInSeconds = config.pollingIntervalInSeconds; + if (pollingIntervalInSeconds == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Polling interval not configured"); cleanup(); return; @@ -271,17 +276,18 @@ private void start() { smartHodeDeviceListEnabled = false; } - if (config.pollingIntervalInSeconds < 10) { + if (pollingIntervalInSeconds < 10) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Polling interval less than 10 seconds not allowed"); cleanup(); return; } synchronized (synchronizeConnection) { + Connection connection = this.connection; if (connection == null || !connection.getEmail().equals(config.email) || !connection.getPassword().equals(config.password) || !connection.getAmazonSite().equals(config.amazonSite)) { - connection = new Connection(config.email, config.password, config.amazonSite, + this.connection = new Connection(config.email, config.password, config.amazonSite, this.getThing().getUID().getId()); } } @@ -290,6 +296,7 @@ private void start() { } updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login"); + if (refreshLogin != null) { refreshLogin.cancel(false); } @@ -302,7 +309,7 @@ private void start() { } refreshJob = scheduler.scheduleWithFixedDelay(() -> { refreshData(); - }, 4, config.pollingIntervalInSeconds, TimeUnit.SECONDS); + }, 4, pollingIntervalInSeconds, TimeUnit.SECONDS); logger.debug("amazon account bridge handler started."); } @@ -310,52 +317,52 @@ private void start() { private void checkLogin() { synchronized (synchronizeConnection) { - Connection temp = connection; - if (temp == null) { + Connection currentConnection = this.connection; + if (currentConnection == null) { return; } - Date loginTime = temp.tryGetLoginTime(); + Date loginTime = currentConnection.tryGetLoginTime(); Date currentDate = new Date(); long currentTime = currentDate.getTime(); if (loginTime != null && currentTime - loginTime.getTime() > 3600000) // One hour { try { - if (!temp.verifyLogin()) { - temp.logout(); + if (!currentConnection.verifyLogin()) { + currentConnection.logout(); } } catch (IOException | URISyntaxException e) { logger.info("logout failed: {}", e.getMessage()); - temp.logout(); + currentConnection.logout(); } } - loginTime = temp.tryGetLoginTime(); + loginTime = currentConnection.tryGetLoginTime(); if (loginTime != null && currentTime - loginTime.getTime() > 86400000 * 5) // 5 days { // Recreate session this.stateStorage.storeState("sessionStorage", ""); - temp = new Connection(temp.getEmail(), temp.getPassword(), temp.getAmazonSite(), - this.getThing().getUID().getId()); + currentConnection = new Connection(currentConnection.getEmail(), currentConnection.getPassword(), + currentConnection.getAmazonSite(), this.getThing().getUID().getId()); } boolean loginIsValid = true; - if (!temp.getIsLoggedIn()) { + if (!currentConnection.getIsLoggedIn()) { try { // read session data from property String sessionStore = this.stateStorage.findState("sessionStorage"); // try use the session data - if (!temp.tryRestoreLogin(sessionStore)) { + if (!currentConnection.tryRestoreLogin(sessionStore)) { // session data not valid -> login int retry = 0; while (true) { try { - temp.makeLogin(); + currentConnection.makeLogin(); break; } catch (ConnectionException e) { // Up to 2 retries for login retry++; if (retry >= 2) { - temp.logout(); + currentConnection.logout(); throw e; } // give amazon some time @@ -368,14 +375,11 @@ private void checkLogin() { } } // store session data in property - String serializedStorage = temp.serializeLoginData(); - if (serializedStorage == null) { - serializedStorage = ""; - } + String serializedStorage = currentConnection.serializeLoginData(); this.stateStorage.storeState("sessionStorage", serializedStorage); } updateSmartHomeDeviceList = true; - connection = temp; + this.connection = currentConnection; } catch (UnknownHostException e) { loginIsValid = false; updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, @@ -406,9 +410,6 @@ private void handleValidLogin() { public void setConnection(Connection connection) { this.connection = connection; String serializedStorage = connection.serializeLoginData(); - if (serializedStorage == null) { - serializedStorage = ""; - } this.stateStorage.storeState("sessionStorage", serializedStorage); updateSmartHomeDeviceList = true; handleValidLogin(); @@ -454,25 +455,22 @@ private void refreshData() { updateStatus(ThingStatus.ONLINE); } - public Device findDeviceJson(EchoHandler echoHandler) { + public @Nullable Device findDeviceJson(EchoHandler echoHandler) { String serialNumber = echoHandler.findSerialNumber(); return findDeviceJson(serialNumber); } - public Device findDeviceJson(String serialNumber) { + public @Nullable Device findDeviceJson(String serialNumber) { Device result = null; - if (!serialNumber.isEmpty()) { - Map temp = jsonSerialNumberDeviceMapping; - if (temp != null) { - result = temp.get(serialNumber); - } - return result; + if (StringUtils.isNotEmpty(serialNumber)) { + Map jsonSerialNumberDeviceMapping = this.jsonSerialNumberDeviceMapping; + result = jsonSerialNumberDeviceMapping.get(serialNumber); } return result; } - public Device findDeviceJsonBySerialOrName(String serialOrName) { - if (!serialOrName.isEmpty()) { + public @Nullable Device findDeviceJsonBySerialOrName(String serialOrName) { + if (StringUtils.isNotEmpty(serialOrName)) { String serialOrNameLowerCase = serialOrName.toLowerCase(); Map temp = jsonSerialNumberDeviceMapping; for (Device device : temp.values()) { @@ -513,7 +511,9 @@ public void updateDeviceList(boolean manualScan) { if (devices != null) { Map newJsonSerialDeviceMapping = new HashMap<>(); for (Device device : devices) { - newJsonSerialDeviceMapping.put(device.serialNumber, device); + if (device.serialNumber != null) { + newJsonSerialDeviceMapping.put(device.serialNumber, device); + } } jsonSerialNumberDeviceMapping = newJsonSerialDeviceMapping; @@ -523,16 +523,12 @@ public void updateDeviceList(boolean manualScan) { } synchronized (echoHandlers) { for (EchoHandler child : echoHandlers) { - if (child != null) { - initializeEchoHandler(child, temp); - } + initializeEchoHandler(child, temp); } } synchronized (smartHomeHandlers) { for (SmartHomeBaseHandler child : smartHomeHandlers) { - if (child != null) { - child.initialize(temp); - } + child.initialize(temp); } } updateFlashBriefingHandlers(temp); @@ -580,9 +576,7 @@ private void updateFlashBriefingHandlers(Connection temp) { } for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) { - if (child != null) { - child.initialize(this, currentFlashBriefingJson); - } + child.initialize(this, currentFlashBriefingJson); } if (flashBriefingProfileHandlers.isEmpty()) { discoverFlashProfiles = true; // discover at least one device @@ -597,7 +591,7 @@ private void updateFlashBriefingHandlers(Connection temp) { } } - public Connection findConnection() { + public @Nullable Connection findConnection() { return this.connection; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 19270197955a7..61539b8290a26 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -16,6 +16,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType; @@ -63,7 +64,7 @@ public class EchoHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(EchoHandler.class); - private static HashMap instances = new HashMap(); + private final static HashMap instances = new HashMap(); private @Nullable Device device; private @Nullable Connection connection; private @Nullable ScheduledFuture updateStateJob; @@ -166,8 +167,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { updateStateJob.cancel(false); } - Connection temp = connection; - if (temp == null) { + Connection connection = this.connection; + if (connection == null) { return; } Device device = this.device; @@ -179,17 +180,17 @@ public void handleCommand(ChannelUID channelUID, Command command) { String channelId = channelUID.getId(); if (channelId.equals(CHANNEL_PLAYER)) { if (command == PlayPauseType.PAUSE || command == OnOffType.OFF) { - temp.command(device, "{\"type\":\"PauseCommand\"}"); + connection.command(device, "{\"type\":\"PauseCommand\"}"); } else if (command == PlayPauseType.PLAY || command == OnOffType.ON) { - temp.command(device, "{\"type\":\"PlayCommand\"}"); + connection.command(device, "{\"type\":\"PlayCommand\"}"); } else if (command == NextPreviousType.NEXT) { - temp.command(device, "{\"type\":\"NextCommand\"}"); + connection.command(device, "{\"type\":\"NextCommand\"}"); } else if (command == NextPreviousType.PREVIOUS) { - temp.command(device, "{\"type\":\"PreviousCommand\"}"); + connection.command(device, "{\"type\":\"PreviousCommand\"}"); } else if (command == RewindFastforwardType.FASTFORWARD) { - temp.command(device, "{\"type\":\"ForwardCommand\"}"); + connection.command(device, "{\"type\":\"ForwardCommand\"}"); } else if (command == RewindFastforwardType.REWIND) { - temp.command(device, "{\"type\":\"RewindCommand\"}"); + connection.command(device, "{\"type\":\"RewindCommand\"}"); } } // Volume commands @@ -197,26 +198,26 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof PercentType) { PercentType value = (PercentType) command; int volume = value.intValue(); - temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume + connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume + ",\"contentFocusClientId\":\"Default\"}"); } else if (command == OnOffType.OFF) { - temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + 0 + connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + 0 + ",\"contentFocusClientId\":\"Default\"}"); } else if (command == OnOffType.ON) { - temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + ",\"contentFocusClientId\":\"Default\"}"); } else if (command == IncreaseDecreaseType.INCREASE) { if (lastKnownVolume < 100) { lastKnownVolume++; updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume)); - temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + ",\"contentFocusClientId\":\"Default\"}"); } } else if (command == IncreaseDecreaseType.DECREASE) { if (lastKnownVolume > 0) { lastKnownVolume--; updateState(CHANNEL_VOLUME, new PercentType(lastKnownVolume)); - temp.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + lastKnownVolume + ",\"contentFocusClientId\":\"Default\"}"); } } @@ -226,7 +227,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof OnOffType) { OnOffType value = (OnOffType) command; - temp.command(device, "{\"type\":\"ShuffleCommand\",\"shuffle\":\"" + connection.command(device, "{\"type\":\"ShuffleCommand\",\"shuffle\":\"" + (value == OnOffType.ON ? "true" : "false") + "\"}"); } } @@ -239,7 +240,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (!address.isEmpty()) { waitForUpdate = 4000; } - temp.bluetooth(device, address); + connection.bluetooth(device, address); } } if (channelId.equals(CHANNEL_BLUETOOTH)) { @@ -251,7 +252,10 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (state != null && (bluetoothId == null || bluetoothId.isEmpty())) { if (state.pairedDeviceList != null) { for (PairedDevice paired : state.pairedDeviceList) { - if (paired.address != null && !paired.address.isEmpty()) { + if (paired == null) { + continue; + } + if (StringUtils.isNotEmpty(paired.address)) { lastKnownBluetoothId = paired.address; break; } @@ -259,10 +263,10 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } if (lastKnownBluetoothId != null && !lastKnownBluetoothId.isEmpty()) { - temp.bluetooth(device, lastKnownBluetoothId); + connection.bluetooth(device, lastKnownBluetoothId); } } else if (command == OnOffType.OFF) { - temp.bluetooth(device, null); + connection.bluetooth(device, null); } } if (channelId.equals(CHANNEL_BLUETOOTH_DEVICE_NAME)) { @@ -276,7 +280,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (trackId != null && !trackId.isEmpty()) { waitForUpdate = 3000; } - temp.playAmazonMusicTrack(device, trackId); + connection.playAmazonMusicTrack(device, trackId); } } @@ -288,7 +292,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { waitForUpdate = 3000; updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID_LAST_USED, new StringType(playListId)); } - temp.playAmazonMusicPlayList(device, playListId); + connection.playAmazonMusicPlayList(device, playListId); } } @@ -299,9 +303,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (lastKnownAmazonMusicId != null && !lastKnownAmazonMusicId.isEmpty()) { waitForUpdate = 3000; } - temp.playAmazonMusicTrack(device, lastKnownAmazonMusicId); + connection.playAmazonMusicTrack(device, lastKnownAmazonMusicId); } else if (command == OnOffType.OFF) { - temp.playAmazonMusicTrack(device, ""); + connection.playAmazonMusicTrack(device, ""); } } @@ -313,7 +317,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (stationId != null && !stationId.isEmpty()) { waitForUpdate = 3000; } - temp.playRadio(device, stationId); + connection.playRadio(device, stationId); } } if (channelId.equals(CHANNEL_RADIO)) { @@ -323,9 +327,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (lastKnownRadioStationId != null && !lastKnownRadioStationId.isEmpty()) { waitForUpdate = 3000; } - temp.playRadio(device, lastKnownRadioStationId); + connection.playRadio(device, lastKnownRadioStationId); } else if (command == OnOffType.OFF) { - temp.playRadio(device, ""); + connection.playRadio(device, ""); } } // notification @@ -337,7 +341,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (reminder != null && !reminder.isEmpty()) { waitForUpdate = 3000; updateRemind = true; - currentNotification = temp.notification(device, "Reminder", reminder, null); + currentNotification = connection.notification(device, "Reminder", reminder, null); currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> { updateNotificationTimerState(); }, 1, 1, TimeUnit.SECONDS); @@ -361,7 +365,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { sound.providerId = "ECHO"; sound.id = alarmSound; } - currentNotification = temp.notification(device, "Alarm", null, sound); + currentNotification = connection.notification(device, "Alarm", null, sound); currentNotifcationUpdateTimer = scheduler.scheduleWithFixedDelay(() -> { updateNotificationTimerState(); }, 1, 1, TimeUnit.SECONDS); @@ -375,21 +379,21 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command == OnOffType.ON) { waitForUpdate = 1000; - temp.executeSequenceCommand(device, "Alexa.FlashBriefing.Play"); + connection.executeSequenceCommand(device, "Alexa.FlashBriefing.Play"); } } if (channelId.equals(CHANNEL_PLAY_TRAFFIC_NEWS)) { if (command == OnOffType.ON) { waitForUpdate = 1000; - temp.executeSequenceCommand(device, "Alexa.Traffic.Play"); + connection.executeSequenceCommand(device, "Alexa.Traffic.Play"); } } if (channelId.equals(CHANNEL_PLAY_WEATER_REPORT)) { if (command == OnOffType.ON) { waitForUpdate = 1000; - temp.executeSequenceCommand(device, "Alexa.Weather.Play"); + connection.executeSequenceCommand(device, "Alexa.Weather.Play"); } } @@ -399,7 +403,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (utterance != null && !utterance.isEmpty()) { waitForUpdate = 1000; updateRoutine = true; - temp.startRoutine(device, utterance); + connection.startRoutine(device, utterance); } } } @@ -411,7 +415,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { BluetoothState state = null; if (bluetoothRefresh) { JsonBluetoothStates states; - states = temp.getBluetoothConnectionStates(); + states = connection.getBluetoothConnectionStates(); state = states.findStateByDevice(device); } @@ -469,14 +473,18 @@ private void updateNotificationTimerState() { logger.warn("update notification state fails: {}", e); } if (stopCurrentNotifcation) { - if (tempCurrentNotification != null && tempCurrentNotification.type != null) { - if (tempCurrentNotification.type.equals("Reminder")) { - updateState(CHANNEL_REMIND, new StringType("")); - updateRemind = false; - } - if (tempCurrentNotification.type.equals("Alarm")) { - updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType("")); - updateAlarm = false; + + if (tempCurrentNotification != null) { + String type = tempCurrentNotification.type; + if (type != null) { + if (type.equals("Reminder")) { + updateState(CHANNEL_REMIND, new StringType("")); + updateRemind = false; + } + if (type.equals("Alarm")) { + updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType("")); + updateAlarm = false; + } } } stopCurrentNotification(); @@ -567,6 +575,9 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto this.bluetoothState = bluetoothState; if (bluetoothState.pairedDeviceList != null) { for (PairedDevice paired : bluetoothState.pairedDeviceList) { + if (paired == null) { + continue; + } if (paired.connected && paired.address != null) { bluetoothIsConnected = true; bluetoothId = paired.address; @@ -586,7 +597,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto boolean isRadio = false; if (mediaState != null && mediaState.radioStationId != null && !mediaState.radioStationId.isEmpty()) { lastKnownRadioStationId = mediaState.radioStationId; - if (provider != null && provider.providerName.equalsIgnoreCase("TuneIn Live-Radio")) { + if (provider != null && StringUtils.equalsIgnoreCase(provider.providerName, "TuneIn Live-Radio")) { isRadio = true; } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java index 228025eedd130..388e4d7adb093 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java @@ -16,6 +16,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.StringType; @@ -27,6 +28,7 @@ import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; +import org.openhab.binding.amazonechocontrol.internal.Connection; import org.openhab.binding.amazonechocontrol.internal.StateStorage; import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; @@ -38,12 +40,14 @@ * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class FlashBriefingProfileHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(FlashBriefingProfileHandler.class); private static HashMap instances = new HashMap(); - AccountHandler handler; + @Nullable + AccountHandler accountHandler; StateStorage stateStorage; boolean updatePlayOnDevice = true; String currentConfigurationJson = ""; @@ -54,8 +58,8 @@ public FlashBriefingProfileHandler(Thing thing) { stateStorage = new StateStorage(thing); } - public AccountHandler findAccountHandler() { - return this.handler; + public @Nullable AccountHandler findAccountHandler() { + return this.accountHandler; } public static @Nullable FlashBriefingProfileHandler find(ThingUID uid) { @@ -82,7 +86,7 @@ public void initialize() { synchronized (instances) { instances.put(this.getThing().getUID(), this); } - if (this.currentConfigurationJson != null && !this.currentConfigurationJson.isEmpty()) { + if (!this.currentConfigurationJson.isEmpty()) { updateStatus(ThingStatus.ONLINE); } else { updateStatus(ThingStatus.UNKNOWN); @@ -112,8 +116,8 @@ public void dispose() { @Override public void handleCommand(ChannelUID channelUID, Command command) { - AccountHandler temp = this.handler; - if (temp == null) { + AccountHandler accountHandler = this.accountHandler; + if (accountHandler == null) { return; } int waitForUpdate = -1; @@ -132,15 +136,15 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (channelId.equals(CHANNEL_SAVE)) { if (command.equals(OnOffType.ON)) { - saveCurrentProfile(temp); + saveCurrentProfile(accountHandler); waitForUpdate = 500; } } if (channelId.equals(CHANNEL_ACTIVE)) { if (command.equals(OnOffType.ON)) { String currentConfigurationJson = this.currentConfigurationJson; - if (currentConfigurationJson != null && !currentConfigurationJson.isEmpty()) { - temp.setEnabledFlashBriefingsJson(currentConfigurationJson); + if (!currentConfigurationJson.isEmpty()) { + accountHandler.setEnabledFlashBriefingsJson(currentConfigurationJson); updateState(CHANNEL_ACTIVE, OnOffType.ON); waitForUpdate = 500; @@ -151,21 +155,28 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof StringType) { String deviceSerialOrName = ((StringType) command).toFullString(); String currentConfigurationJson = this.currentConfigurationJson; - if (currentConfigurationJson != null && !currentConfigurationJson.isEmpty()) { + if (!currentConfigurationJson.isEmpty()) { - String old = temp.getEnabledFlashBriefingsJson(); - temp.setEnabledFlashBriefingsJson(currentConfigurationJson); + String old = accountHandler.getEnabledFlashBriefingsJson(); + accountHandler.setEnabledFlashBriefingsJson(currentConfigurationJson); - Device device = temp.findDeviceJsonBySerialOrName(deviceSerialOrName); + Device device = accountHandler.findDeviceJsonBySerialOrName(deviceSerialOrName); if (device == null) { logger.warn("Device '{}' not found", deviceSerialOrName); } else { - temp.findConnection().executeSequenceCommand(device, "Alexa.FlashBriefing.Play"); - - scheduler.schedule(() -> temp.setEnabledFlashBriefingsJson(old), 1000, - TimeUnit.MILLISECONDS); - - updateState(CHANNEL_ACTIVE, OnOffType.ON); + @Nullable + Connection connection = accountHandler.findConnection(); + if (connection == null) { + logger.warn("Connection for '{}' not found", + accountHandler.getThing().getUID().getId()); + } else { + connection.executeSequenceCommand(device, "Alexa.FlashBriefing.Play"); + + scheduler.schedule(() -> accountHandler.setEnabledFlashBriefingsJson(old), 1000, + TimeUnit.MILLISECONDS); + + updateState(CHANNEL_ACTIVE, OnOffType.ON); + } } updatePlayOnDevice = true; waitForUpdate = 1000; @@ -177,7 +188,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { logger.warn("Handle command failed {}", e); } if (waitForUpdate >= 0) { - this.updateStateJob = scheduler.schedule(() -> temp.updateFlashBriefingHandlers(), waitForUpdate, + this.updateStateJob = scheduler.schedule(() -> accountHandler.updateFlashBriefingHandlers(), waitForUpdate, TimeUnit.MILLISECONDS); } } @@ -188,9 +199,9 @@ public void initialize(AccountHandler handler, String currentConfigurationJson) if (updatePlayOnDevice) { updateState(CHANNEL_PLAY_ON_DEVICE, new StringType("")); } - if (this.handler != handler) { + if (this.accountHandler != handler) { - this.handler = handler; + this.accountHandler = handler; String configurationJson = this.stateStorage.findState("configurationJson"); if (configurationJson == null || configurationJson.isEmpty()) { this.currentConfigurationJson = saveCurrentProfile(handler); @@ -199,7 +210,7 @@ public void initialize(AccountHandler handler, String currentConfigurationJson) removeFromDiscovery(); this.currentConfigurationJson = configurationJson; } - if (this.currentConfigurationJson != null && !this.currentConfigurationJson.isEmpty()) { + if (!this.currentConfigurationJson.isEmpty()) { updateStatus(ThingStatus.ONLINE); } else { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java index ac7c6464b72ab..ab6defa08b001 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java @@ -14,7 +14,7 @@ import java.net.URISyntaxException; import java.util.HashMap; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ChannelUID; @@ -32,14 +32,15 @@ * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public abstract class SmartHomeBaseHandler extends BaseThingHandler { - private static HashMap instances = new HashMap(); + private final static HashMap instances = new HashMap(); private final Logger logger = LoggerFactory.getLogger(SmartHomeBaseHandler.class); - private Connection connection; + private @Nullable Connection connection; - protected Connection findConnection() { + protected @Nullable Connection findConnection() { return this.connection; } @@ -68,7 +69,7 @@ public void initialize() { } } - public void initialize(@NonNull Connection connection) { + public void initialize(Connection connection) { this.connection = connection; updateStatus(ThingStatus.ONLINE); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java index 14d438c519387..eed8bd7a2e291 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java @@ -14,6 +14,7 @@ import java.net.URISyntaxException; import java.util.Locale; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.PercentType; import org.eclipse.smarthome.core.thing.Thing; @@ -27,13 +28,13 @@ * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class SmartHomeDimmerHandler extends SmartHomeBaseHandler { private final Logger logger = LoggerFactory.getLogger(SmartHomeDimmerHandler.class); public SmartHomeDimmerHandler(Thing thing) { super(thing); - } @Override diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java index 6d03a324c42d8..fc2ef0236bb47 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.net.URISyntaxException; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.types.Command; @@ -23,6 +24,7 @@ * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class SmartHomeSwitchHandler extends SmartHomeBaseHandler { public SmartHomeSwitchHandler(Thing thing) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java index 55f10d8025ccc..c3cd328b06b9f 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java @@ -9,20 +9,29 @@ package org.openhab.binding.amazonechocontrol.internal; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * Account Thing configuration * * @author Michael Geramb - Initial Contribution */ +@NonNullByDefault public class AccountConfiguration { + @Nullable public String email; + @Nullable public String password; + @Nullable public String amazonSite; + @Nullable public Integer pollingIntervalInSeconds; // The smarthome devices feature is currently not available in the configuration for public use, // because there seems to be a problem in detecting and controlling devices, // there seems to be different smarthome skill versions + @Nullable public Boolean discoverSmartHomeDevices; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 202d30b0dd29e..fce4b82e455a0 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.CookieManager; import java.net.CookieStore; import java.net.HttpCookie; import java.net.URI; @@ -31,7 +32,11 @@ import javax.net.ssl.HttpsURLConnection; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAutomation; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAutomation.Payload; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; @@ -61,71 +66,77 @@ * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class Connection { private final Logger logger = LoggerFactory.getLogger(Connection.class); - private java.net.CookieManager m_cookieManager = new java.net.CookieManager(); - private String m_email; - private String m_password; - private String m_amazonSite; - private String m_sessionId; - private Date m_loginTime; - private String m_alexaServer; - private String m_accountThingId; + private final CookieManager cookieManager = new CookieManager(); + private final String email; + private final String password; + private final String amazonSite; + private final String alexaServer; + private final String accountThingId; - public Connection(String email, String password, String amazonSite, String accountThingId) { - m_accountThingId = accountThingId; - m_email = email; - m_password = password; + private @Nullable String sessionId; + private @Nullable Date loginTime; - m_amazonSite = amazonSite; - if (m_amazonSite.toLowerCase().startsWith("http://")) { - m_amazonSite = m_amazonSite.substring(7); + public Connection(@Nullable String email, @Nullable String password, @Nullable String amazonSite, + @Nullable String accountThingId) { + + this.accountThingId = accountThingId != null ? accountThingId : ""; + this.email = email != null ? email : ""; + this.password = password != null ? password : ""; + + String correctedAmazonSite = amazonSite != null ? amazonSite : ""; + if (correctedAmazonSite.toLowerCase().startsWith("http://")) { + correctedAmazonSite = correctedAmazonSite.substring(7); } - if (m_amazonSite.toLowerCase().startsWith("https://")) { - m_amazonSite = m_amazonSite.substring(8); + if (correctedAmazonSite.toLowerCase().startsWith("https://")) { + correctedAmazonSite = correctedAmazonSite.substring(8); } - if (m_amazonSite.toLowerCase().startsWith("www.")) { - m_amazonSite = m_amazonSite.substring(4); + if (correctedAmazonSite.toLowerCase().startsWith("www.")) { + correctedAmazonSite = correctedAmazonSite.substring(4); } - if (m_amazonSite.toLowerCase().startsWith("alexa.")) { - m_amazonSite = m_amazonSite.substring(6); + if (correctedAmazonSite.toLowerCase().startsWith("alexa.")) { + correctedAmazonSite = correctedAmazonSite.substring(6); } - m_alexaServer = "https://alexa." + amazonSite; + this.amazonSite = correctedAmazonSite; + alexaServer = "https://alexa." + this.amazonSite; } - public Date tryGetLoginTime() { - return m_loginTime; + public @Nullable Date tryGetLoginTime() { + return loginTime; } public String getEmail() { - return m_email; + return email; } public String getPassword() { - return m_password; + return password; } public String getAmazonSite() { - return m_amazonSite; + return amazonSite; } public String serializeLoginData() { - if (m_sessionId == null || m_loginTime == null) { + Date loginTime = this.loginTime; + if (sessionId == null || loginTime == null) { return ""; } StringBuilder builder = new StringBuilder(); builder.append("4\n"); // version - builder.append(m_email); + builder.append(email); builder.append("\n"); - builder.append(m_password.hashCode()); + builder.append(password.hashCode()); builder.append("\n"); - builder.append(m_sessionId); + builder.append(sessionId); builder.append("\n"); - builder.append(m_loginTime.getTime()); + builder.append(loginTime.getTime()); builder.append("\n"); - List cookies = m_cookieManager.getCookieStore().getCookies(); + List cookies = cookieManager.getCookieStore().getCookies(); builder.append(cookies.size()); builder.append("\n"); for (HttpCookie cookie : cookies) { @@ -146,7 +157,7 @@ public String serializeLoginData() { return builder.toString(); } - private void writeValue(StringBuilder builder, Object value) { + private void writeValue(StringBuilder builder, @Nullable Object value) { if (value == null) { builder.append('0'); } else { @@ -161,13 +172,13 @@ private String readValue(Scanner scanner) { if (scanner.nextLine().equals("1")) { return scanner.nextLine(); } - return null; + return ""; } - public Boolean tryRestoreLogin(String data) { + public boolean tryRestoreLogin(@Nullable String data) { // verify store data - if (data == null || data.isEmpty()) { + if (StringUtils.isEmpty(data)) { return false; } @@ -180,29 +191,28 @@ public Boolean tryRestoreLogin(String data) { // check if email or password was changed in the mean time String email = scanner.nextLine(); - if (!email.equals(this.m_email)) { + if (!email.equals(this.email)) { scanner.close(); return false; } int passwordHash = Integer.parseInt(scanner.nextLine()); - if (passwordHash != this.m_password.hashCode()) { + if (passwordHash != this.password.hashCode()) { scanner.close(); return false; } // Recreate session and cookies - m_sessionId = scanner.nextLine(); + sessionId = scanner.nextLine(); Date loginTime = new Date(Long.parseLong(scanner.nextLine())); - CookieStore cookieStore = m_cookieManager.getCookieStore(); + CookieStore cookieStore = cookieManager.getCookieStore(); cookieStore.removeAll(); Integer numberOfCookies = Integer.parseInt(scanner.nextLine()); for (Integer i = 0; i < numberOfCookies; i++) { String name = readValue(scanner); - String value = readValue(scanner); HttpCookie clientCookie = new HttpCookie(name, value); @@ -222,7 +232,7 @@ public Boolean tryRestoreLogin(String data) { scanner.close(); try { if (verifyLogin()) { - m_loginTime = loginTime; + this.loginTime = loginTime; return true; } } catch (IOException e) { @@ -232,8 +242,8 @@ public Boolean tryRestoreLogin(String data) { } // anything goes wrong, remove session data cookieStore.removeAll(); - m_sessionId = null; - m_loginTime = null; + this.sessionId = null; + this.loginTime = null; return false; } @@ -252,14 +262,14 @@ public String makeRequestAndReturnString(String url) throws IOException, URISynt return makeRequestAndReturnString("GET", url, null, null, false); } - private String makeRequestAndReturnString(String verb, String url, String referer, String postData, Boolean json) - throws IOException, URISyntaxException { + private String makeRequestAndReturnString(String verb, String url, @Nullable String referer, + @Nullable String postData, boolean json) throws IOException, URISyntaxException { HttpsURLConnection connection = makeRequest(verb, url, referer, postData, json, true); return convertStream(connection.getInputStream()); } - public HttpsURLConnection makeRequest(String verb, String url, String referer, String postData, boolean json, - boolean autoredirect) throws IOException, URISyntaxException { + public HttpsURLConnection makeRequest(String verb, String url, @Nullable String referer, @Nullable String postData, + boolean json, boolean autoredirect) throws IOException, URISyntaxException { String currentUrl = url; for (int i = 0; i < 30; i++) // loop for handling redirect, using automatic redirect is not possible, because // all response headers must be catched @@ -283,7 +293,7 @@ public HttpsURLConnection makeRequest(String verb, String url, String referer, S URI uri = connection.getURL().toURI(); StringBuilder cookieHeaderBuilder = new StringBuilder(); - for (HttpCookie cookie : m_cookieManager.getCookieStore().get(uri)) { + for (HttpCookie cookie : cookieManager.getCookieStore().get(uri)) { if (cookieHeaderBuilder.length() > 0) { cookieHeaderBuilder.insert(0, "; "); } @@ -327,20 +337,20 @@ public HttpsURLConnection makeRequest(String verb, String url, String referer, S Map> headerFields = connection.getHeaderFields(); for (Map.Entry> header : headerFields.entrySet()) { String key = header.getKey(); - if (key != null) { + if (StringUtils.isNotEmpty(key)) { if (key.equalsIgnoreCase("Set-Cookie")) { // store cookie for (String cookieHeader : header.getValue()) { List cookies = HttpCookie.parse(cookieHeader); for (HttpCookie cookie : cookies) { - m_cookieManager.getCookieStore().add(uri, cookie); + cookieManager.getCookieStore().add(uri, cookie); } } } if (key.equalsIgnoreCase("Location")) { // get redirect location location = header.getValue().get(0); - if (location != null) { + if (StringUtils.isNotEmpty(location)) { location = uri.resolve(location).toString(); // check for https if (location.toLowerCase().startsWith("http://")) { @@ -377,33 +387,33 @@ public HttpsURLConnection makeRequest(String verb, String url, String referer, S } public boolean getIsLoggedIn() { - return m_loginTime != null; + return loginTime != null; } public String getLoginPage() throws IOException, URISyntaxException { // clear session data - m_cookieManager.getCookieStore().removeAll(); - m_sessionId = null; - m_loginTime = null; - logger.debug("Start Login to {}", m_alexaServer); + cookieManager.getCookieStore().removeAll(); + sessionId = null; + loginTime = null; + logger.debug("Start Login to {}", alexaServer); // get login form - String loginFormHtml = makeRequestAndReturnString(m_alexaServer); + String loginFormHtml = makeRequestAndReturnString(alexaServer); logger.debug("Received login form {}", loginFormHtml); // get session id from cookies - for (HttpCookie cookie : m_cookieManager.getCookieStore().getCookies()) { + for (HttpCookie cookie : cookieManager.getCookieStore().getCookies()) { if (cookie.getName().equalsIgnoreCase("session-id")) { - m_sessionId = cookie.getValue(); + sessionId = cookie.getValue(); break; } } - if (m_sessionId == null) { + if (sessionId == null) { throw new ConnectionException("No session id received"); } - m_cookieManager.getCookieStore().add(new URL("https://www." + m_amazonSite).toURI(), - HttpCookie.parse("session-id=" + m_sessionId).get(0)); + cookieManager.getCookieStore().add(new URL("https://www." + amazonSite).toURI(), + HttpCookie.parse("session-id=" + sessionId).get(0)); return loginFormHtml; } @@ -425,59 +435,59 @@ public void makeLogin() throws IOException, URISyntaxException { postDataBuilder.append('&'); } - String queryParameters = postDataBuilder.toString() + "session-id=" - + URLEncoder.encode(m_sessionId, "UTF-8"); + String queryParameters = postDataBuilder.toString() + "session-id=" + URLEncoder.encode(sessionId, "UTF-8"); logger.debug("Login query String: {}", queryParameters); postDataBuilder.append("email"); postDataBuilder.append('='); - postDataBuilder.append(URLEncoder.encode(m_email, "UTF-8")); + postDataBuilder.append(URLEncoder.encode(email, "UTF-8")); postDataBuilder.append('&'); postDataBuilder.append("password"); postDataBuilder.append('='); - postDataBuilder.append(URLEncoder.encode(m_password, "UTF-8")); + postDataBuilder.append(URLEncoder.encode(password, "UTF-8")); String postData = postDataBuilder.toString(); if (postLoginData(queryParameters, postData) != null) { throw new ConnectionException( - "Login fails. Check your credentials and try to login with your webbrowser to http(s):///amazonechocontrol/" - + m_accountThingId); + "Login fails. Check your credentials and try to login with your webbrowser to http(s):///amazonechocontrol/" + + accountThingId); } } catch (Exception e) { // clear session data - m_cookieManager.getCookieStore().removeAll(); - m_sessionId = null; - m_loginTime = null; + cookieManager.getCookieStore().removeAll(); + sessionId = null; + loginTime = null; logger.info("Login failed: {}", e.getLocalizedMessage()); // rethrow throw e; } } - public String postLoginData(String optionalQueryParameters, String postData) + public @Nullable String postLoginData(@Nullable String optionalQueryParameters, String postData) throws IOException, URISyntaxException { // build query parameters + @Nullable String queryParameters = optionalQueryParameters; if (queryParameters == null) { - queryParameters = "session-id=" + URLEncoder.encode(m_sessionId, "UTF-8"); + queryParameters = "session-id=" + URLEncoder.encode(sessionId, "UTF-8"); } // build referer link - String referer = "https://www." + m_amazonSite + "/ap/signin?" + queryParameters; + String referer = "https://www." + amazonSite + "/ap/signin?" + queryParameters; // make the request - URLConnection request = makeRequest("POST", "https://www." + m_amazonSite + "/ap/signin", referer, postData, + URLConnection request = makeRequest("POST", "https://www." + amazonSite + "/ap/signin", referer, postData, false, true); String response = convertStream(request.getInputStream()); logger.debug("Received content after login {}", response); String host = request.getURL().getHost(); - if (!host.equalsIgnoreCase(new URI(m_alexaServer).getHost())) { + if (!host.equalsIgnoreCase(new URI(alexaServer).getHost())) { return response; } if (response.contains("Amazon Alexa")) { @@ -495,18 +505,18 @@ public String postLoginData(String optionalQueryParameters, String postData) } public boolean verifyLogin() throws IOException, URISyntaxException { - String response = makeRequestAndReturnString(m_alexaServer + "/api/bootstrap?version=0"); + String response = makeRequestAndReturnString(alexaServer + "/api/bootstrap?version=0"); Boolean result = response.contains("\"authenticated\":true"); - if (result && m_loginTime == null) { - m_loginTime = new Date(); + if (result && loginTime == null) { + loginTime = new Date(); } return result; } public void logout() { - m_cookieManager.getCookieStore().removeAll(); - m_sessionId = null; - m_loginTime = null; + cookieManager.getCookieStore().removeAll(); + sessionId = null; + loginTime = null; } // parser @@ -526,23 +536,27 @@ private T parseJson(String json, Class type) { public Device[] getDeviceList() throws IOException, URISyntaxException { String json = getDeviceListJson(); JsonDevices devices = parseJson(json, JsonDevices.class); - return devices.devices; + Device[] result = devices.devices; + if (result == null) { + result = new Device[0]; + } + return result; } public String getDeviceListJson() throws IOException, URISyntaxException { - String json = makeRequestAndReturnString(m_alexaServer + "/api/devices-v2/device?cached=false"); + String json = makeRequestAndReturnString(alexaServer + "/api/devices-v2/device?cached=false"); return json; } public JsonPlayerState getPlayer(Device device) throws IOException, URISyntaxException { - String json = makeRequestAndReturnString(m_alexaServer + "/api/np/player?deviceSerialNumber=" + String json = makeRequestAndReturnString(alexaServer + "/api/np/player?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&screenWidth=1440"); JsonPlayerState playerState = parseJson(json, JsonPlayerState.class); return playerState; } public JsonMediaState getMediaState(Device device) throws IOException, URISyntaxException { - String json = makeRequestAndReturnString(m_alexaServer + "/api/media/state?deviceSerialNumber=" + String json = makeRequestAndReturnString(alexaServer + "/api/media/state?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType); JsonMediaState mediaState = parseJson(json, JsonMediaState.class); return mediaState; @@ -551,7 +565,7 @@ public JsonMediaState getMediaState(Device device) throws IOException, URISyntax public JsonBluetoothStates getBluetoothConnectionStates() { String json; try { - json = makeRequestAndReturnString(m_alexaServer + "/api/bluetooth?cached=true"); + json = makeRequestAndReturnString(alexaServer + "/api/bluetooth?cached=true"); } catch (IOException | URISyntaxException e) { logger.debug("failed to get bluetooth state: {}", e.getMessage()); return new JsonBluetoothStates(); @@ -562,65 +576,66 @@ public JsonBluetoothStates getBluetoothConnectionStates() { public JsonPlaylists getPlaylists(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString( - m_alexaServer + "/api/cloudplayer/playlists?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + alexaServer + "/api/cloudplayer/playlists?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId); JsonPlaylists playlists = parseJson(json, JsonPlaylists.class); return playlists; } public void command(Device device, String command) throws IOException, URISyntaxException { - String url = m_alexaServer + "/api/np/command?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + String url = alexaServer + "/api/np/command?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType; makeRequest("POST", url, null, command, true, true); } - public void bluetooth(Device device, String address) throws IOException, URISyntaxException { - if (address == null || address.isEmpty()) { + public void bluetooth(Device device, @Nullable String address) throws IOException, URISyntaxException { + if (StringUtils.isEmpty(address)) { // disconnect makeRequest("POST", - m_alexaServer + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" + device.serialNumber, + alexaServer + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" + device.serialNumber, null, "", true, true); } else { makeRequest("POST", - m_alexaServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber, null, + alexaServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber, null, "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true, true); } } - public void playRadio(Device device, String stationId) throws IOException, URISyntaxException { - if (stationId == null || stationId.isEmpty()) { + public void playRadio(Device device, @Nullable String stationId) throws IOException, URISyntaxException { + if (StringUtils.isEmpty(stationId)) { command(device, "{\"type\":\"PauseCommand\"}"); } else { makeRequest("POST", - m_alexaServer + "/api/tunein/queue-and-play?deviceSerialNumber=" + device.serialNumber + alexaServer + "/api/tunein/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&guideId=" + stationId + "&contentType=station&callSign=&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId, null, "", true, true); } } - public void playAmazonMusicTrack(Device device, String trackId) throws IOException, URISyntaxException { - if (trackId == null || trackId.isEmpty()) { + public void playAmazonMusicTrack(Device device, @Nullable String trackId) throws IOException, URISyntaxException { + if (StringUtils.isEmpty(trackId)) { command(device, "{\"type\":\"PauseCommand\"}"); } else { String command = "{\"trackId\":\"" + trackId + "\",\"playQueuePrime\":true}"; makeRequest("POST", - m_alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId + "&shuffle=false", null, command, true, true); } } - public void playAmazonMusicPlayList(Device device, String playListId) throws IOException, URISyntaxException { - if (playListId == null || playListId.isEmpty()) { + public void playAmazonMusicPlayList(Device device, @Nullable String playListId) + throws IOException, URISyntaxException { + if (StringUtils.isEmpty(playListId)) { command(device, "{\"type\":\"PauseCommand\"}"); } else { String command = "{\"playlistId\":\"" + playListId + "\",\"playQueuePrime\":true}"; makeRequest("POST", - m_alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId + "&shuffle=false", null, command, true, true); @@ -635,19 +650,25 @@ public void executeSequenceCommand(Device device, String command) throws IOExcep + "\\\",\\\"deviceSerialNumber\\\":\\\"" + device.serialNumber + "\\\",\\\"customerId\\\":\\\"" + device.deviceOwnerCustomerId + "\\\",\\\"locale\\\":\\\"\\\"}}}\",\n" + " \"status\": \"ENABLED\" }"; - makeRequest("POST", m_alexaServer + "/api/behaviors/preview", null, json, true, true); + makeRequest("POST", alexaServer + "/api/behaviors/preview", null, json, true, true); } public void startRoutine(Device device, String utterance) throws IOException, URISyntaxException { JsonAutomation found = null; - String deviceLocale = null; + String deviceLocale = ""; for (JsonAutomation routine : getRoutines()) { if (routine.triggers != null && routine.sequence != null) { for (JsonAutomation.Trigger trigger : routine.triggers) { - if (trigger.payload != null && trigger.payload.utterance != null - && trigger.payload.utterance.equalsIgnoreCase(utterance)) { + if (trigger == null) { + continue; + } + Payload payload = trigger.payload; + if (payload == null) { + continue; + } + if (payload.utterance != null && payload.utterance.equalsIgnoreCase(utterance)) { found = routine; - deviceLocale = trigger.payload.locale; + deviceLocale = payload.locale; break; } } @@ -682,27 +703,28 @@ public void startRoutine(Device device, String utterance) throws IOException, UR // "locale": "ALEXA_CURRENT_LOCALE" String locale = "\"locale\":\"ALEXA_CURRENT_LOCALE\""; - String newlocale = deviceLocale != null ? "\"locale\":\"" + deviceLocale + "\"" : "\"locale\":null"; + String newlocale = StringUtils.isNotEmpty(deviceLocale) ? "\"locale\":\"" + deviceLocale + "\"" + : "\"locale\":null"; sequenceJson = sequenceJson.replace(locale.subSequence(0, locale.length()), newlocale.subSequence(0, newlocale.length())); request.sequenceJson = sequenceJson; String requestJson = gson.toJson(request); - makeRequest("POST", m_alexaServer + "/api/behaviors/preview", null, requestJson, true, true); + makeRequest("POST", alexaServer + "/api/behaviors/preview", null, requestJson, true, true); } else { logger.warn("Routine {} not found", utterance); } } public JsonAutomation[] getRoutines() throws IOException, URISyntaxException { - String json = makeRequestAndReturnString("GET", m_alexaServer + "/api/behaviors/automations", null, null, true); + String json = makeRequestAndReturnString("GET", alexaServer + "/api/behaviors/automations", null, null, true); JsonAutomation[] result = parseJson(json, JsonAutomation[].class); return result; } public JsonFeed[] getEnabledFlashBriefings() throws IOException, URISyntaxException { - String json = makeRequestAndReturnString("GET", m_alexaServer + "/api/content-skills/enabled-feeds", null, null, + String json = makeRequestAndReturnString("GET", alexaServer + "/api/content-skills/enabled-feeds", null, null, true); JsonEnabledFeeds result = parseJson(json, JsonEnabledFeeds.class); if (result.enabledFeeds != null) { @@ -718,12 +740,12 @@ public void setEnabledFlashBriefings(JsonFeed[] enabledFlashBriefing) throws IOE gsonBuilder.serializeNulls(); Gson gson = gsonBuilder.create(); String json = gson.toJson(enabled); - makeRequest("POST", m_alexaServer + "/api/content-skills/enabled-feeds", null, json, true, true); + makeRequest("POST", alexaServer + "/api/content-skills/enabled-feeds", null, json, true, true); } public JsonNotificationSound[] getNotificationSounds(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString( - "GET", m_alexaServer + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber + "GET", alexaServer + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&softwareVersion=" + device.softwareVersion, null, null, true); JsonNotificationSounds result = parseJson(json, JsonNotificationSounds.class); @@ -733,8 +755,8 @@ public JsonNotificationSound[] getNotificationSounds(Device device) throws IOExc return new JsonNotificationSound[0]; } - public JsonNotificationResponse notification(Device device, String type, String label, JsonNotificationSound sound) - throws IOException, URISyntaxException { + public JsonNotificationResponse notification(Device device, String type, @Nullable String label, + @Nullable JsonNotificationSound sound) throws IOException, URISyntaxException { Date date = new Date(new Date().getTime()); long createdDate = date.getTime(); @@ -760,7 +782,7 @@ public JsonNotificationResponse notification(Device device, String type, String Gson gson = gsonBuilder.create(); String data = gson.toJson(request); - String response = makeRequestAndReturnString("PUT", m_alexaServer + "/api/notifications/createReminder", null, + String response = makeRequestAndReturnString("PUT", alexaServer + "/api/notifications/createReminder", null, data, true); JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); return result; @@ -768,20 +790,20 @@ public JsonNotificationResponse notification(Device device, String type, String } public void stopNotification(JsonNotificationResponse notification) throws IOException, URISyntaxException { - makeRequestAndReturnString("DELETE", m_alexaServer + "/api/notifications/" + notification.id, null, null, true); + makeRequestAndReturnString("DELETE", alexaServer + "/api/notifications/" + notification.id, null, null, true); } public JsonNotificationResponse getNotificationState(JsonNotificationResponse notification) throws IOException, URISyntaxException { - String response = makeRequestAndReturnString("GET", m_alexaServer + "/api/notifications/" + notification.id, - null, null, true); + String response = makeRequestAndReturnString("GET", alexaServer + "/api/notifications/" + notification.id, null, + null, true); JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); return result; } public List getSmartHomeDevices() throws IOException, URISyntaxException { try { - String json = makeRequestAndReturnString("GET", m_alexaServer + "/api/phoenix", null, null, true); + String json = makeRequestAndReturnString("GET", alexaServer + "/api/phoenix", null, null, true); logger.debug("getSmartHomeDevices result: {}", json); JsonNetworkDetails networkDetails = parseJson(json, JsonNetworkDetails.class); @@ -815,15 +837,15 @@ private void searchSmartHomeDevicesRecursive(Gson gson, Object jsonNode, List map = req.getParameterMap(); @@ -126,7 +135,14 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S } @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) + throws ServletException, IOException { + if (req == null) { + return; + } + if (resp == null) { + return; + } String uri = req.getRequestURI().substring(servletUrl.length()); String queryString = req.getQueryString(); if (queryString != null && queryString.length() > 0) { @@ -176,8 +192,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se } } - void HandleProxyRequest(HttpServletResponse resp, String verb, String url, String referer, String postData) - throws IOException { + void HandleProxyRequest(HttpServletResponse resp, String verb, String url, @Nullable String referer, + @Nullable String postData) throws IOException { HttpsURLConnection urlConnection; try { @@ -192,7 +208,7 @@ void HandleProxyRequest(HttpServletResponse resp, String verb, String url, Strin + BINDING_ID + ":" + THING_TYPE_ACCOUNT.getId() + ":" + id + "'>Check Thing in Paper UI"); account.setConnection(this.connection); - reCreateConnection(); + this.connection = reCreateConnection(); return; } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java index cb1c1a39b7d89..c6fef5c4ea7e3 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java @@ -14,6 +14,9 @@ import java.io.IOException; import java.util.Properties; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.config.core.ConfigConstants; import org.eclipse.smarthome.core.thing.Thing; import org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants; @@ -25,12 +28,14 @@ * * @author Michael Geramb - Initial Contribution */ +@NonNullByDefault public class StateStorage { private final Logger logger = LoggerFactory.getLogger(StateStorage.class); File propertyFile; Thing thing; + @Nullable Properties properties; public StateStorage(Thing thing) { @@ -40,13 +45,13 @@ public StateStorage(Thing thing) { + File.separator + thing.getUID().getAsString().replace(':', '_') + ".properties"); } - public void storeState(String key, String value) { + public void storeState(@Nullable String key, @Nullable String value) { synchronized (this) { if (key == null) { return; } - initProperties(); - if (value == null || value.isEmpty()) { + Properties properties = initProperties(); + if (StringUtils.isEmpty(value)) { properties.remove(key); } else { properties.setProperty(key, value); @@ -57,15 +62,14 @@ public void storeState(String key, String value) { } } - @SuppressWarnings("null") - public String findState(String key) { + public @Nullable String findState(String key) { synchronized (this) { - initProperties(); + Properties properties = initProperties(); Object value = properties.get(key); if (value == null) { // upgrade from BETA 9 configuration String oldValue = thing.getProperties().get(key); - if (oldValue != null && !oldValue.isEmpty()) { + if (StringUtils.isNotEmpty(oldValue)) { value = oldValue; storeState(key, oldValue); } @@ -80,6 +84,7 @@ public String findState(String key) { private void saveProperties() { try { + Properties properties = initProperties(); logger.debug("Create file {}.", propertyFile); String directoryName = propertyFile.getParent(); File directory = new File(directoryName); @@ -96,20 +101,24 @@ private void saveProperties() { } - private void initProperties() { - if (properties == null) { - Properties p = new Properties(); - - if (propertyFile.exists()) { - try { - FileReader fileReader = new FileReader(propertyFile); - p.load(fileReader); - fileReader.close(); - } catch (IOException e) { - logger.error("Error occured on writing the property file.", e); - } + private Properties initProperties() { + if (properties != null) { + return properties; + } + + Properties p = new Properties(); + + if (propertyFile.exists()) { + try { + FileReader fileReader = new FileReader(propertyFile); + p.load(fileReader); + fileReader.close(); + } catch (IOException e) { + logger.error("Error occured on writing the property file.", e); } - properties = p; } + properties = p; + return p; + } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 65854ce91ed17..822e58e83d102 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -21,7 +21,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; @@ -44,16 +45,22 @@ * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.amazonechocontrol") public class AmazonEchoDiscovery extends AbstractDiscoveryService { - public static AmazonEchoDiscovery instance; - private final @NonNull static List discoveryServices = new ArrayList<>(); + static boolean discoverAccount = true; + + public @Nullable static AmazonEchoDiscovery instance; + private final static List discoveryServices = new ArrayList<>(); - private final @NonNull Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); - private final @NonNull Map lastDeviceInformations = new HashMap<>(); - private final @NonNull Map lastSmartHomeDeviceInformations = new HashMap<>(); - private final @NonNull HashSet discoverdFlashBriefings = new HashSet(); + private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); + private final Map lastDeviceInformations = new HashMap<>(); + private final Map lastSmartHomeDeviceInformations = new HashMap<>(); + private final HashSet discoverdFlashBriefings = new HashSet(); + + @Nullable + ScheduledFuture startScanStateJob; public static void addDiscoveryHandler(IAmazonEchoDiscovery discoveryService) { synchronized (discoveryServices) { @@ -79,8 +86,6 @@ public void deactivate() { super.deactivate(); } - static boolean discoverAccount = true; - public static void setHandlerExist() { discoverAccount = false; } @@ -122,8 +127,6 @@ protected void startScan(boolean manual) { } - ScheduledFuture startScanStateJob; - @Override protected void startBackgroundDiscovery() { AmazonEchoDiscovery.instance = this; @@ -151,7 +154,7 @@ protected void stopBackgroundDiscovery() { @Override @Activate - public void activate(Map config) { + public void activate(@Nullable Map config) { super.activate(config); if (config != null) { modified(config); @@ -169,12 +172,13 @@ public synchronized void setSmartHomeDevices(ThingUID brigdeThingUID, String entityId = deviceInformation.entityId; if (entityId != null) { boolean alreadyfound = toRemove.remove(entityId); - if (!alreadyfound && deviceInformation.actions != null) { - List actions = Arrays.asList(deviceInformation.actions); - if (actions.contains("turnOn") && actions.contains("turnOff")) { + String[] actions = deviceInformation.actions; + if (!alreadyfound && actions != null) { + List actionList = Arrays.asList(actions); + if (actionList.contains("turnOn") && actionList.contains("turnOff")) { ThingTypeUID thingTypeId; - if (actions.contains("setPercentage")) { + if (actionList.contains("setPercentage")) { thingTypeId = THING_TYPE_SMART_HOME_DIMMER; } else { thingTypeId = THING_TYPE_SMART_HOME_SWITCH; @@ -212,15 +216,16 @@ public synchronized void setDevices(ThingUID brigdeThingUID, Device[] deviceInfo if (serialNumber != null) { boolean alreadyfound = toRemove.remove(serialNumber); // new - if (!alreadyfound && deviceInformation.deviceFamily != null) { + String deviceFamily = deviceInformation.deviceFamily; + if (!alreadyfound && deviceFamily != null) { ThingTypeUID thingTypeId; - if (deviceInformation.deviceFamily.equals("ECHO")) { + if (deviceFamily.equals("ECHO")) { thingTypeId = THING_TYPE_ECHO; - } else if (deviceInformation.deviceFamily.equals("ROOK")) { + } else if (deviceFamily.equals("ROOK")) { thingTypeId = THING_TYPE_ECHO_SPOT; - } else if (deviceInformation.deviceFamily.equals("KNIGHT")) { + } else if (deviceFamily.equals("KNIGHT")) { thingTypeId = THING_TYPE_ECHO_SHOW; - } else if (deviceInformation.deviceFamily.equals("WHA")) { + } else if (deviceFamily.equals("WHA")) { thingTypeId = THING_TYPE_ECHO_WHA; } else { thingTypeId = THING_TYPE_UNKNOWN; @@ -234,7 +239,7 @@ public synchronized void setDevices(ThingUID brigdeThingUID, Device[] deviceInfo DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) .withLabel(deviceInformation.accountName) .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) - .withProperty(DEVICE_PROPERTY_FAMILY, deviceInformation.deviceFamily) + .withProperty(DEVICE_PROPERTY_FAMILY, deviceFamily) .withRepresentationProperty(DEVICE_PROPERTY_SERIAL_NUMBER).withBridge(brigdeThingUID) .build(); @@ -273,7 +278,7 @@ public synchronized void discoverFlashBriefingProfiles(ThingUID brigdeThingUID, } } - public synchronized void removeExistingEchoHandler(@NonNull ThingUID uid) { + public synchronized void removeExistingEchoHandler(ThingUID uid) { for (String id : lastDeviceInformations.keySet()) { if (lastDeviceInformations.get(id).equals(uid)) { lastDeviceInformations.remove(id); @@ -281,7 +286,7 @@ public synchronized void removeExistingEchoHandler(@NonNull ThingUID uid) { } } - public synchronized void removeExistingSmartHomeHandler(@NonNull ThingUID uid) { + public synchronized void removeExistingSmartHomeHandler(ThingUID uid) { for (String id : lastSmartHomeDeviceInformations.keySet()) { if (lastSmartHomeDeviceInformations.get(id).equals(uid)) { lastSmartHomeDeviceInformations.remove(id); @@ -289,7 +294,7 @@ public synchronized void removeExistingSmartHomeHandler(@NonNull ThingUID uid) { } } - public synchronized void removeExistingFlashBriefingProfile(String currentFlashBriefingJson) { + public synchronized void removeExistingFlashBriefingProfile(@Nullable String currentFlashBriefingJson) { if (currentFlashBriefingJson != null) { discoverdFlashBriefings.remove(currentFlashBriefingJson); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAutomation.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAutomation.java index 2918115355caa..ab7103bd96a7c 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAutomation.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonAutomation.java @@ -10,30 +10,34 @@ import java.util.TreeMap; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonAutomation} encapsulate the GSON data of automation query * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonAutomation { - public String automationId; - public String name; - public Trigger[] triggers; - public TreeMap sequence; - public String status; + public @Nullable String automationId; + public @Nullable String name; + public @Nullable Trigger @Nullable [] triggers; + public @Nullable TreeMap sequence; + public @Nullable String status; public long creationTimeEpochMillis; public long lastUpdatedTimeEpochMillis; public class Trigger { - public Payload payload; - public String id; - public String type; + public @Nullable Payload payload; + public @Nullable String id; + public @Nullable String type; } public class Payload { - public String customerId; - public String utterance; - public String locale; - public String marketplaceId; + public @Nullable String customerId; + public @Nullable String utterance; + public @Nullable String locale; + public @Nullable String marketplaceId; } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java index 120ac1dfb656a..60431c4553754 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java @@ -8,6 +8,8 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; /** @@ -15,43 +17,45 @@ * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonBluetoothStates { - public BluetoothState findStateByDevice(Device device) { + public @Nullable BluetoothState findStateByDevice(@Nullable Device device) { if (device == null) { return null; } + @Nullable + BluetoothState @Nullable [] bluetoothStates = this.bluetoothStates; if (bluetoothStates == null) { return null; } for (BluetoothState state : bluetoothStates) { - if (state.deviceSerialNumber != null && state.deviceSerialNumber.equals(device.serialNumber)) { + if (state != null && state.deviceSerialNumber != null + && state.deviceSerialNumber.equals(device.serialNumber)) { return state; } } return null; } - public BluetoothState[] bluetoothStates; + public @Nullable BluetoothState @Nullable [] bluetoothStates; public class PairedDevice { - public String address; + public @Nullable String address; public boolean connected; - public String deviceClass; - public String friendlyName; - // "profiles":[ - // "AVRCP", - // "A2DP-SINK" - // ] + public @Nullable String deviceClass; + public @Nullable String friendlyName; + public @Nullable String @Nullable [] profiles; + } public class BluetoothState { - public String deviceSerialNumber; - public String deviceType; - public String friendlyName; + public @Nullable String deviceSerialNumber; + public @Nullable String deviceType; + public @Nullable String friendlyName; public boolean gadgetPaired; public boolean online; - public PairedDevice[] pairedDeviceList; + public @Nullable PairedDevice @Nullable [] pairedDeviceList; } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java index 84f99152bae2b..82a6032746eb4 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonDevices.java @@ -8,25 +8,29 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonDevices} encapsulate the GSON data of device list * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonDevices { public class Device { - public String accountName; - public String serialNumber; - public String deviceOwnerCustomerId; - public String deviceAccountId; - public String deviceFamily; - public String deviceType; - public String softwareVersion; + public @Nullable String accountName; + public @Nullable String serialNumber; + public @Nullable String deviceOwnerCustomerId; + public @Nullable String deviceAccountId; + public @Nullable String deviceFamily; + public @Nullable String deviceType; + public @Nullable String softwareVersion; public boolean online; - public String[] capabilities; + public @Nullable String @Nullable [] capabilities; } - public Device[] devices; + public @Nullable Device @Nullable [] devices; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEnabledFeeds.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEnabledFeeds.java index 6429abd6cdc08..c0b1112b7b0c9 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEnabledFeeds.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonEnabledFeeds.java @@ -8,12 +8,16 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonEnabledFeeds} encapsulate the GSON data of the enabled feeds list * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonEnabledFeeds { - public JsonFeed[] enabledFeeds; + public @Nullable JsonFeed @Nullable [] enabledFeeds; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java index a912e9baf4ad6..4b75cd40c739d 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java @@ -8,14 +8,18 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonFeed} encapsulate the GSON data of feed * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonFeed { - public Object feedId; - public String name; - public String skillId; - public String imageUrl; + public @Nullable Object feedId; + public @Nullable String name; + public @Nullable String skillId; + public @Nullable String imageUrl; } \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java index 980226016d781..2be8c9229a4dc 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMediaState.java @@ -8,69 +8,73 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonMediaState} encapsulate the GSON data of the current media state * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonMediaState { - public String clientId; - public String contentId; - public String contentType; - public String currentState; - public String imageURL; + public @Nullable String clientId; + public @Nullable String contentId; + public @Nullable String contentType; + public @Nullable String currentState; + public @Nullable String imageURL; public boolean isDisliked; public boolean isLiked; public boolean looping; - public String mediaOwnerCustomerId; + public @Nullable String mediaOwnerCustomerId; public boolean muted; - public String programId; + public @Nullable String programId; public int progressSeconds; - public String providerId; - public QueueEntry[] queue; - public String queueId; - public Integer queueSize; - public String radioStationId; + public @Nullable String providerId; + public @Nullable QueueEntry @Nullable [] queue; + public @Nullable String queueId; + public @Nullable Integer queueSize; + public @Nullable String radioStationId; public int radioVariety; - public String referenceId; - public String service; + public @Nullable String referenceId; + public @Nullable String service; public boolean shuffling; // public long timeLastShuffled; parsing fails with some values, so do not use it public int volume; public class QueueEntry { - public String album; - public String albumAsin; - public String artist; - public String asin; - public String cardImageURL; - public String contentId; - public String contentType; + public @Nullable String album; + public @Nullable String albumAsin; + public @Nullable String artist; + public @Nullable String asin; + public @Nullable String cardImageURL; + public @Nullable String contentId; + public @Nullable String contentType; public int durationSeconds; public boolean feedbackDisabled; - public String historicalId; - public String imageURL; + public @Nullable String historicalId; + public @Nullable String imageURL; public int index; public boolean isAd; public boolean isDisliked; public boolean isFreeWithPrime; public boolean isLiked; - public String programId; - public String programName; - public String providerId; - public String queueId; - public String radioStationCallSign; - public String radioStationId; - public String radioStationLocation; - public String radioStationSlogan; - public String referenceId; - public String service; - public String startTime; - public String title; - public String trackId; - public String trackStatus; + public @Nullable String programId; + public @Nullable String programName; + public @Nullable String providerId; + public @Nullable String queueId; + public @Nullable String radioStationCallSign; + public @Nullable String radioStationId; + public @Nullable String radioStationLocation; + public @Nullable String radioStationSlogan; + public @Nullable String referenceId; + public @Nullable String service; + public @Nullable String startTime; + public @Nullable String title; + public @Nullable String trackId; + public @Nullable String trackStatus; } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java index 5938d36f5de34..1d68a036c13df 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java @@ -8,12 +8,16 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonNetworkDetails} encapsulate the GSON data of a network query * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonNetworkDetails { - public String networkDetail; + public @Nullable String networkDetail; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java index 08e3c11f4185d..32223cd1f46a1 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java @@ -8,26 +8,30 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonNotificationRequest} encapsulate the GSON data for a notification request * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonNotificationRequest { - public String type = "Reminder"; // "Reminder", "Alarm" - public String status = "ON"; + public @Nullable String type = "Reminder"; // "Reminder", "Alarm" + public @Nullable String status = "ON"; public long alarmTime; - public String originalTime; - public String originalDate; - public String timeZoneId; - public String reminderIndex; - public JsonNotificationSound sound; - public String deviceSerialNumber; - public String deviceType; - public String recurringPattern; - public String reminderLabel; + public @Nullable String originalTime; + public @Nullable String originalDate; + public @Nullable String timeZoneId; + public @Nullable String reminderIndex; + public @Nullable JsonNotificationSound sound; + public @Nullable String deviceSerialNumber; + public @Nullable String deviceType; + public @Nullable String recurringPattern; + public @Nullable String reminderLabel; public boolean isSaveInFlight = true; - public String id = "createReminder"; + public @Nullable String id = "createReminder"; public boolean isRecurring = false; public long createdDate; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java index 5362f7ef19fe2..51b44f8206114 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java @@ -8,20 +8,24 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonNotificationResponse} encapsulate the GSON data for the result of a notification request * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonNotificationResponse { // This is only a partial definition, see the example JSON below public long alarmTime; public long createdDate; - public String deviceSerialNumber; - public String deviceType; - public String id; - public String status; - public String type; + public @Nullable String deviceSerialNumber; + public @Nullable String deviceType; + public @Nullable String id; + public @Nullable String status; + public @Nullable String type; } /* diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java index 03f6c6c7eef82..fca8478bb2312 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java @@ -8,15 +8,19 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonNotificationSound} encapsulate the GSON data for a notification sound * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonNotificationSound { - public String displayName; - public String folder; - public String id = "system_alerts_melodic_01"; - public String providerId = "ECHO"; - public String sampleUrl; + public @Nullable String displayName; + public @Nullable String folder; + public @Nullable String id = "system_alerts_melodic_01"; + public @Nullable String providerId = "ECHO"; + public @Nullable String sampleUrl; } \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSounds.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSounds.java index ed459cdcc305d..9070bad204cec 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSounds.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSounds.java @@ -8,11 +8,15 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonNotificationSounds} encapsulate the GSON data for a notification sounds * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonNotificationSounds { - public JsonNotificationSound[] notificationSounds; + public @Nullable JsonNotificationSound @Nullable [] notificationSounds; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java index 3f4a605053da7..ab1f3b476879e 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java @@ -9,6 +9,7 @@ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; /** @@ -16,31 +17,32 @@ * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonPlayerState { - public PlayerInfo playerInfo; + public @Nullable PlayerInfo playerInfo; public class PlayerInfo { - public String state; + public @Nullable String state; public @Nullable InfoText infoText; public @Nullable InfoText miniInfoText; public @Nullable Provider provider; public @Nullable Volume volume; public @Nullable MainArt mainArt; - public String queueId; - public String mediaId; + public @Nullable String queueId; + public @Nullable String mediaId; public class InfoText { public boolean multiLineMode; - public String subText1; - public String subText2; - public String title; + public @Nullable String subText1; + public @Nullable String subText2; + public @Nullable String title; } public class Provider { - public String providerDisplayName; - public String providerName; + public @Nullable String providerDisplayName; + public @Nullable String providerName; } public class Volume { @@ -49,10 +51,10 @@ public class Volume { } public class MainArt { - public String altText; - public String artType; - public String contentType; - public String url; + public @Nullable String altText; + public @Nullable String artType; + public @Nullable String contentType; + public @Nullable String url; } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaylists.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaylists.java index a522bc97abef0..c9d8e76d0003d 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaylists.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaylists.java @@ -10,18 +10,22 @@ import java.util.Map; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonPlayerState} encapsulate the GSON data of playlist query * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonPlaylists { - public Map playlists; + public @Nullable Map playlists; public class PlayList { - public String playlistId; - public String title; + public @Nullable String playlistId; + public @Nullable String title; public int trackCount; public int version; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java index c5351184d9683..f036780f86d98 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java @@ -8,14 +8,18 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonSmartHomeDevice} encapsulate the GSON-part data of a network query * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonSmartHomeDevice { - public String entityId; - public String friendlyName; - public String[] actions; - public String manufacturerName; + public @Nullable String entityId; + public @Nullable String friendlyName; + public @Nullable String @Nullable [] actions; + public @Nullable String manufacturerName; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java index 5e3dba9a4000c..47349e495d4c4 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java @@ -8,13 +8,17 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * The {@link JsonStartRoutineRequest} encapsulate the GSON for starting a routine * * @author Michael Geramb - Initial contribution */ +@NonNullByDefault public class JsonStartRoutineRequest { - public String behaviorId; - public String sequenceJson; - public String status = "ENABLED"; + public @Nullable String behaviorId; + public @Nullable String sequenceJson; + public @Nullable String status = "ENABLED"; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java index 15ab9cceeb266..6dd8179da1def 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -70,13 +70,17 @@ public AmazonEchoDynamicStateDescriptionProvider() { return originalStateDescription; } - if (bluetoothState.pairedDeviceList == null) { + PairedDevice[] pairedDeviceList = bluetoothState.pairedDeviceList; + if (pairedDeviceList == null) { return originalStateDescription; } ArrayList options = new ArrayList(); options.add(new StateOption("", "")); - for (PairedDevice device : bluetoothState.pairedDeviceList) { + for (PairedDevice device : pairedDeviceList) { + if (device == null) { + continue; + } if (device.address != null && device.friendlyName != null) { options.add(new StateOption(device.address, device.friendlyName)); } @@ -111,7 +115,7 @@ public AmazonEchoDynamicStateDescriptionProvider() { options.add(new StateOption("", "")); if (playLists.playlists != null) { for (PlayList[] innerLists : playLists.playlists.values()) { - if (innerLists.length > 0) { + if (innerLists != null && innerLists.length > 0) { PlayList playList = innerLists[0]; if (playList.playlistId != null && playList.title != null) { options.add(new StateOption(playList.playlistId, @@ -148,18 +152,18 @@ public AmazonEchoDynamicStateDescriptionProvider() { } ArrayList options = new ArrayList(); options.add(new StateOption("", "")); - if (notificationSounds != null) { - 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, - String.format("%s [%s]", notificationSound.displayName, providerSoundId))); + 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, + String.format("%s [%s]", notificationSound.displayName, providerSoundId))); - } } } + StateDescription result = new StateDescription(originalStateDescription.getMinimum(), originalStateDescription.getMaximum(), originalStateDescription.getStep(), originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); From 52ca33e35a787ba440556e6bbbb7f89f8bd71fa0 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Fri, 6 Apr 2018 22:33:14 +0200 Subject: [PATCH 25/56] [amazonechocontrol] All temp variables renamed to a better name Signed-off-by: Michael Geramb (github: mgeramb) --- .../handler/AccountHandler.java | 68 +++++++++---------- .../handler/EchoHandler.java | 36 +++++----- .../handler/SmartHomeBaseHandler.java | 6 +- 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index deb23934acbe4..ce8152a47ba79 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -121,10 +121,10 @@ public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBrie flashBriefingProfileHandlers.add(flashBriefingProfileHandler); } } - Connection temp = connection; - if (temp != null) { + Connection connection = this.connection; + if (connection != null) { if (currentFlashBriefingJson.isEmpty()) { - updateFlashBriefingProfiles(temp); + updateFlashBriefingProfiles(connection); } flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson); @@ -419,16 +419,16 @@ private void refreshData() { logger.debug("amazon account bridge refreshing data ..."); // check if logged in - Connection temp = null; + Connection currentConnection = null; synchronized (synchronizeConnection) { - temp = connection; - if (temp != null) { - if (!temp.getIsLoggedIn()) { + currentConnection = connection; + if (currentConnection != null) { + if (!currentConnection.getIsLoggedIn()) { return; } } } - if (temp == null) { + if (currentConnection == null) { return; } @@ -437,8 +437,8 @@ private void refreshData() { // update bluetooth states JsonBluetoothStates states = null; - if (temp.getIsLoggedIn()) { - states = temp.getBluetoothConnectionStates(); + if (currentConnection.getIsLoggedIn()) { + states = currentConnection.getBluetoothConnectionStates(); } // forward device information to echo handler @@ -472,13 +472,13 @@ private void refreshData() { public @Nullable Device findDeviceJsonBySerialOrName(String serialOrName) { if (StringUtils.isNotEmpty(serialOrName)) { String serialOrNameLowerCase = serialOrName.toLowerCase(); - Map temp = jsonSerialNumberDeviceMapping; - for (Device device : temp.values()) { + Map currentJsonSerialNumberDeviceMapping = this.jsonSerialNumberDeviceMapping; + for (Device device : currentJsonSerialNumberDeviceMapping.values()) { if (device.serialNumber != null && device.serialNumber.toLowerCase().equals(serialOrNameLowerCase)) { return device; } } - for (Device device : temp.values()) { + for (Device device : currentJsonSerialNumberDeviceMapping.values()) { if (device.accountName != null && device.accountName.toLowerCase().equals(serialOrNameLowerCase)) { return device; } @@ -494,16 +494,16 @@ public void updateDeviceList(boolean manualScan) { discoverFlashProfiles = true; } - Connection temp = connection; - if (temp == null) { + Connection currentConnection = connection; + if (currentConnection == null) { return; } AmazonEchoDiscovery discoveryService = AmazonEchoDiscovery.instance; Device[] devices = null; try { - if (temp.getIsLoggedIn()) { - devices = temp.getDeviceList(); + if (currentConnection.getIsLoggedIn()) { + devices = currentConnection.getDeviceList(); } } catch (IOException | URISyntaxException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); @@ -523,21 +523,21 @@ public void updateDeviceList(boolean manualScan) { } synchronized (echoHandlers) { for (EchoHandler child : echoHandlers) { - initializeEchoHandler(child, temp); + initializeEchoHandler(child, currentConnection); } } synchronized (smartHomeHandlers) { for (SmartHomeBaseHandler child : smartHomeHandlers) { - child.initialize(temp); + child.initialize(currentConnection); } } - updateFlashBriefingHandlers(temp); + updateFlashBriefingHandlers(currentConnection); if (discoveryService != null && updateSmartHomeDeviceList && smartHodeDeviceListEnabled) { updateSmartHomeDeviceList = false; List smartHomeDevices = null; try { - smartHomeDevices = temp.getSmartHomeDevices(); + smartHomeDevices = currentConnection.getSmartHomeDevices(); } catch (IOException | URISyntaxException e) { logger.warn("Update smart home list failed {}", e); } @@ -549,12 +549,12 @@ public void updateDeviceList(boolean manualScan) { } public void setEnabledFlashBriefingsJson(String flashBriefingJson) { - Connection temp = connection; + Connection currentConnection = connection; Gson gson = new Gson(); JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class); - if (temp != null) { + if (currentConnection != null) { try { - temp.setEnabledFlashBriefings(feeds); + currentConnection.setEnabledFlashBriefings(feeds); } catch (IOException | URISyntaxException e) { logger.warn("Set flashbriefing profile failed {}", e); } @@ -563,16 +563,16 @@ public void setEnabledFlashBriefingsJson(String flashBriefingJson) { } public void updateFlashBriefingHandlers() { - Connection temp = connection; - if (temp != null) { - updateFlashBriefingHandlers(temp); + Connection currentConnection = connection; + if (currentConnection != null) { + updateFlashBriefingHandlers(currentConnection); } } - private void updateFlashBriefingHandlers(Connection temp) { + private void updateFlashBriefingHandlers(Connection currentConnection) { synchronized (smartHomeHandlers) { if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) { - updateFlashBriefingProfiles(temp); + updateFlashBriefingProfiles(currentConnection); } for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) { @@ -596,17 +596,17 @@ private void updateFlashBriefingHandlers(Connection temp) { } public String getEnabledFlashBriefingsJson() { - Connection temp = this.connection; - if (temp == null) { + Connection currentConnection = this.connection; + if (currentConnection == null) { return ""; } - updateFlashBriefingProfiles(temp); + updateFlashBriefingProfiles(currentConnection); return this.currentFlashBriefingJson; } - private void updateFlashBriefingProfiles(Connection temp) { + private void updateFlashBriefingProfiles(Connection currentConnection) { try { - JsonFeed[] feeds = temp.getEnabledFlashBriefings(); + JsonFeed[] feeds = currentConnection.getEnabledFlashBriefings(); // Make a copy and remove changeable parts JsonFeed[] forSerializer = new JsonFeed[feeds.length]; for (int i = 0; i < feeds.length; i++) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 61539b8290a26..a019510faffba 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -437,18 +437,18 @@ public void handleCommand(ChannelUID channelUID, Command command) { } private void stopCurrentNotification() { - ScheduledFuture tempCurrentNotifcationUpdateTimer = currentNotifcationUpdateTimer; - if (tempCurrentNotifcationUpdateTimer != null) { - currentNotifcationUpdateTimer = null; - tempCurrentNotifcationUpdateTimer.cancel(true); - } - JsonNotificationResponse tempCurrentNotification = currentNotification; - if (tempCurrentNotification != null) { - currentNotification = null; - Connection tempConnection = this.connection; - if (tempConnection != null) { + ScheduledFuture currentNotifcationUpdateTimer = this.currentNotifcationUpdateTimer; + if (currentNotifcationUpdateTimer != null) { + this.currentNotifcationUpdateTimer = null; + currentNotifcationUpdateTimer.cancel(true); + } + JsonNotificationResponse currentNotification = this.currentNotification; + if (currentNotification != null) { + this.currentNotification = null; + Connection currentConnection = this.connection; + if (currentConnection != null) { try { - tempConnection.stopNotification(tempCurrentNotification); + currentConnection.stopNotification(currentNotification); } catch (IOException | URISyntaxException e) { logger.warn("Stop notification failed: {}", e); } @@ -458,12 +458,12 @@ private void stopCurrentNotification() { private void updateNotificationTimerState() { boolean stopCurrentNotifcation = true; - JsonNotificationResponse tempCurrentNotification = currentNotification; + JsonNotificationResponse currentNotification = this.currentNotification; try { - if (tempCurrentNotification != null) { - Connection tempConnection = connection; - if (tempConnection != null) { - JsonNotificationResponse newState = tempConnection.getNotificationState(tempCurrentNotification); + if (currentNotification != null) { + Connection currentConnection = connection; + if (currentConnection != null) { + JsonNotificationResponse newState = currentConnection.getNotificationState(currentNotification); if (newState.status != null && newState.status.equals("ON")) { stopCurrentNotifcation = false; } @@ -474,8 +474,8 @@ private void updateNotificationTimerState() { } if (stopCurrentNotifcation) { - if (tempCurrentNotification != null) { - String type = tempCurrentNotification.type; + if (currentNotification != null) { + String type = currentNotification.type; if (type != null) { if (type.equals("Reminder")) { updateState(CHANNEL_REMIND, new StringType("")); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java index ab6defa08b001..ddbfc08c3c2d3 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java @@ -92,8 +92,8 @@ private String findEntityId() { @Override public void handleCommand(ChannelUID channelUID, Command command) { - Connection temp = findConnection(); - if (temp == null) { + Connection connection = findConnection(); + if (connection == null) { return; } String entityId = findEntityId(); @@ -102,7 +102,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } String channelId = channelUID.getId(); try { - handleCommand(temp, entityId, channelId, command); + handleCommand(connection, entityId, channelId, command); } catch (IOException | URISyntaxException e) { logger.warn("handle command {} for {} failed", command, channelUID, e); } From 0752d681157fd3188f2e03318ec19a26346d65fe Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sat, 7 Apr 2018 20:01:45 +0200 Subject: [PATCH 26/56] [amazonechocontrol] Code review fixes, Nullable checks Signed-off-by: Michael Geramb (github: mgeramb) --- .../handler/AccountHandler.java | 54 ++++++++------- .../handler/EchoHandler.java | 66 +++++++++---------- .../handler/SmartHomeBaseHandler.java | 4 ++ .../internal/Connection.java | 29 ++++---- .../internal/LoginServlet.java | 4 +- .../internal/StateStorage.java | 30 ++++----- .../discovery/AmazonEchoDiscovery.java | 11 ++-- .../internal/jsons/JsonBluetoothStates.java | 4 +- ...onEchoDynamicStateDescriptionProvider.java | 8 ++- 9 files changed, 105 insertions(+), 105 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index ce8152a47ba79..07660197a5b0e 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -11,16 +11,16 @@ import java.io.IOException; import java.net.URISyntaxException; import java.net.UnknownHostException; -import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.StringUtils; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Bridge; @@ -61,9 +61,9 @@ public class AccountHandler extends BaseBridgeHandler implements IAmazonEchoDisc private final Logger logger = LoggerFactory.getLogger(AccountHandler.class); private StateStorage stateStorage; private @Nullable Connection connection; - private final List echoHandlers = new ArrayList<>(); - private final List smartHomeHandlers = new ArrayList<>(); - private final List flashBriefingProfileHandlers = new ArrayList<>(); + private final Set echoHandlers = new HashSet<>(); + private final Set smartHomeHandlers = new HashSet<>(); + private final Set flashBriefingProfileHandlers = new HashSet<>(); private final Object synchronizeConnection = new Object(); private Map jsonSerialNumberDeviceMapping = new HashMap<>(); private @Nullable ScheduledFuture refreshJob; @@ -105,9 +105,7 @@ public Device[] getLastKnownDevices() { public void addEchoHandler(EchoHandler echoHandler) { synchronized (echoHandlers) { - if (!echoHandlers.contains(echoHandler)) { - echoHandlers.add(echoHandler); - } + echoHandlers.add(echoHandler); } Connection connection = this.connection; if (connection != null) { @@ -117,9 +115,7 @@ public void addEchoHandler(EchoHandler echoHandler) { public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) { synchronized (flashBriefingProfileHandlers) { - if (!flashBriefingProfileHandlers.contains(flashBriefingProfileHandler)) { - flashBriefingProfileHandlers.add(flashBriefingProfileHandler); - } + flashBriefingProfileHandlers.add(flashBriefingProfileHandler); } Connection connection = this.connection; if (connection != null) { @@ -133,9 +129,7 @@ public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBrie public void addSmartHomeHandler(SmartHomeBaseHandler smartHomeHandler) { synchronized (smartHomeHandlers) { - if (!smartHomeHandlers.contains(smartHomeHandler)) { - smartHomeHandlers.add(smartHomeHandler); - } + smartHomeHandlers.add(smartHomeHandler); } Connection connection = this.connection; if (connection != null) { @@ -162,7 +156,6 @@ private void initializeEchoHandler(EchoHandler echoHandler, Connection connectio } private void intializeChildDevice(Connection connection, EchoHandler child) { - Device deviceJson = this.findDeviceJson(child); if (deviceJson != null) { child.intialize(connection, deviceJson); @@ -171,13 +164,12 @@ private void intializeChildDevice(Connection connection, EchoHandler child) { @Override public void handleRemoval() { - cleanup(); super.handleRemoval(); } @Override - public void childHandlerDisposed(@NonNull ThingHandler childHandler, @NonNull Thing childThing) { + public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) { // echo handler? if (childHandler instanceof EchoHandler) { @@ -225,17 +217,22 @@ public void dispose() { } private void cleanup() { + @Nullable + ScheduledFuture refreshJob = this.refreshJob; if (refreshJob != null) { refreshJob.cancel(true); - refreshJob = null; + this.refreshJob = null; } + @Nullable + ScheduledFuture refreshLogin = this.refreshLogin; if (refreshLogin != null) { refreshLogin.cancel(true); - refreshLogin = null; + this.refreshLogin = null; } + Connection connection = this.connection; if (connection != null) { connection.logout(); - connection = null; + this.connection = null; } } @@ -267,7 +264,8 @@ private void start() { cleanup(); return; } - if (config.discoverSmartHomeDevices != null && config.discoverSmartHomeDevices) { + Boolean discoverSmartHomeDevices = config.discoverSmartHomeDevices; + if (discoverSmartHomeDevices != null && discoverSmartHomeDevices) { if (!smartHodeDeviceListEnabled) { updateSmartHomeDeviceList = true; } @@ -460,7 +458,7 @@ private void refreshData() { return findDeviceJson(serialNumber); } - public @Nullable Device findDeviceJson(String serialNumber) { + public @Nullable Device findDeviceJson(@Nullable String serialNumber) { Device result = null; if (StringUtils.isNotEmpty(serialNumber)) { Map jsonSerialNumberDeviceMapping = this.jsonSerialNumberDeviceMapping; @@ -469,17 +467,16 @@ private void refreshData() { return result; } - public @Nullable Device findDeviceJsonBySerialOrName(String serialOrName) { + public @Nullable Device findDeviceJsonBySerialOrName(@Nullable String serialOrName) { if (StringUtils.isNotEmpty(serialOrName)) { - String serialOrNameLowerCase = serialOrName.toLowerCase(); Map currentJsonSerialNumberDeviceMapping = this.jsonSerialNumberDeviceMapping; for (Device device : currentJsonSerialNumberDeviceMapping.values()) { - if (device.serialNumber != null && device.serialNumber.toLowerCase().equals(serialOrNameLowerCase)) { + if (StringUtils.equalsIgnoreCase(device.serialNumber, serialOrName)) { return device; } } for (Device device : currentJsonSerialNumberDeviceMapping.values()) { - if (device.accountName != null && device.accountName.toLowerCase().equals(serialOrNameLowerCase)) { + if (StringUtils.equalsIgnoreCase(device.accountName, serialOrName)) { return device; } } @@ -511,8 +508,9 @@ public void updateDeviceList(boolean manualScan) { if (devices != null) { Map newJsonSerialDeviceMapping = new HashMap<>(); for (Device device : devices) { - if (device.serialNumber != null) { - newJsonSerialDeviceMapping.put(device.serialNumber, device); + String serialNumber = device.serialNumber; + if (serialNumber != null) { + newJsonSerialDeviceMapping.put(serialNumber, device); } } jsonSerialNumberDeviceMapping = newJsonSerialDeviceMapping; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index a019510faffba..2040b466b041f 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -249,9 +249,10 @@ public void handleCommand(ChannelUID channelUID, Command command) { waitForUpdate = 4000; String bluetoothId = lastKnownBluetoothId; BluetoothState state = bluetoothState; - if (state != null && (bluetoothId == null || bluetoothId.isEmpty())) { - if (state.pairedDeviceList != null) { - for (PairedDevice paired : state.pairedDeviceList) { + if (state != null && (StringUtils.isEmpty(bluetoothId))) { + PairedDevice[] pairedDeviceList = state.pairedDeviceList; + if (pairedDeviceList != null) { + for (PairedDevice paired : pairedDeviceList) { if (paired == null) { continue; } @@ -277,7 +278,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof StringType) { String trackId = ((StringType) command).toFullString(); - if (trackId != null && !trackId.isEmpty()) { + if (StringUtils.isNotEmpty(trackId)) { waitForUpdate = 3000; } connection.playAmazonMusicTrack(device, trackId); @@ -288,7 +289,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof StringType) { String playListId = ((StringType) command).toFullString(); - if (playListId != null && !playListId.isEmpty()) { + if (StringUtils.isNotEmpty(playListId)) { waitForUpdate = 3000; updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID_LAST_USED, new StringType(playListId)); } @@ -300,31 +301,29 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command == OnOffType.ON) { String lastKnownAmazonMusicId = this.lastKnownAmazonMusicId; - if (lastKnownAmazonMusicId != null && !lastKnownAmazonMusicId.isEmpty()) { + if (StringUtils.isNotEmpty(lastKnownAmazonMusicId)) { waitForUpdate = 3000; } connection.playAmazonMusicTrack(device, lastKnownAmazonMusicId); } else if (command == OnOffType.OFF) { connection.playAmazonMusicTrack(device, ""); } - } // radio commands if (channelId.equals(CHANNEL_RADIO_STATION_ID)) { if (command instanceof StringType) { String stationId = ((StringType) command).toFullString(); - if (stationId != null && !stationId.isEmpty()) { + if (StringUtils.isNotEmpty(stationId)) { waitForUpdate = 3000; } connection.playRadio(device, stationId); } } if (channelId.equals(CHANNEL_RADIO)) { - if (command == OnOffType.ON) { String lastKnownRadioStationId = this.lastKnownRadioStationId; - if (lastKnownRadioStationId != null && !lastKnownRadioStationId.isEmpty()) { + if (StringUtils.isNotEmpty(lastKnownRadioStationId)) { waitForUpdate = 3000; } connection.playRadio(device, lastKnownRadioStationId); @@ -335,10 +334,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { // notification if (channelId.equals(CHANNEL_REMIND)) { if (command instanceof StringType) { - stopCurrentNotification(); String reminder = ((StringType) command).toFullString(); - if (reminder != null && !reminder.isEmpty()) { + if (StringUtils.isNotEmpty(reminder)) { waitForUpdate = 3000; updateRemind = true; currentNotification = connection.notification(device, "Reminder", reminder, null); @@ -350,10 +348,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { } if (channelId.equals(CHANNEL_PLAY_ALARM_SOUND)) { if (command instanceof StringType) { - stopCurrentNotification(); String alarmSound = ((StringType) command).toFullString(); - if (alarmSound != null && !alarmSound.isEmpty()) { + if (StringUtils.isNotEmpty(alarmSound)) { waitForUpdate = 3000; updateAlarm = true; String[] parts = alarmSound.split(":", 2); @@ -400,7 +397,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (channelId.equals(CHANNEL_START_ROUTINE)) { if (command instanceof StringType) { String utterance = ((StringType) command).toFullString(); - if (utterance != null && !utterance.isEmpty()) { + if (StringUtils.isNotEmpty(utterance)) { waitForUpdate = 1000; updateRoutine = true; connection.startRoutine(device, utterance); @@ -464,7 +461,7 @@ private void updateNotificationTimerState() { Connection currentConnection = connection; if (currentConnection != null) { JsonNotificationResponse newState = currentConnection.getNotificationState(currentNotification); - if (newState.status != null && newState.status.equals("ON")) { + if (StringUtils.equals(newState.status, "ON")) { stopCurrentNotifcation = false; } } @@ -551,20 +548,18 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } // check playing - boolean playing = playerInfo != null && playerInfo.state != null && playerInfo.state.equals("PLAYING"); + boolean playing = playerInfo != null && StringUtils.equals(playerInfo.state, "PLAYING"); // handle amazon music String amazonMusicTrackId = ""; String amazonMusicPlayListId = ""; boolean amazonMusic = false; - if (mediaState != null && mediaState.currentState != null && mediaState.currentState.equals("PLAYING") - && mediaState.providerId != null && mediaState.providerId.equals("CLOUD_PLAYER") - && mediaState.contentId != null && !mediaState.contentId.isEmpty()) { - + if (mediaState != null && StringUtils.equals(mediaState.currentState, "PLAYING") + && StringUtils.equals(mediaState.providerId, "CLOUD_PLAYER") + && StringUtils.isNotEmpty(mediaState.contentId)) { amazonMusicTrackId = mediaState.contentId; lastKnownAmazonMusicId = amazonMusicTrackId; amazonMusic = true; - } // handle bluetooth @@ -573,8 +568,9 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto boolean bluetoothIsConnected = false; if (bluetoothState != null) { this.bluetoothState = bluetoothState; - if (bluetoothState.pairedDeviceList != null) { - for (PairedDevice paired : bluetoothState.pairedDeviceList) { + PairedDevice[] pairedDeviceList = bluetoothState.pairedDeviceList; + if (pairedDeviceList != null) { + for (PairedDevice paired : pairedDeviceList) { if (paired == null) { continue; } @@ -582,7 +578,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto bluetoothIsConnected = true; bluetoothId = paired.address; bluetoothDeviceName = paired.friendlyName; - if (bluetoothDeviceName == null || bluetoothDeviceName.isEmpty()) { + if (StringUtils.isEmpty(bluetoothDeviceName)) { bluetoothDeviceName = paired.address; } break; @@ -590,20 +586,20 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } } } - if (bluetoothId != null && !bluetoothId.isEmpty()) { + if (StringUtils.isNotEmpty(bluetoothId)) { lastKnownBluetoothId = bluetoothId; } // handle radio boolean isRadio = false; - if (mediaState != null && mediaState.radioStationId != null && !mediaState.radioStationId.isEmpty()) { + if (mediaState != null && StringUtils.isNotEmpty(mediaState.radioStationId)) { lastKnownRadioStationId = mediaState.radioStationId; if (provider != null && StringUtils.equalsIgnoreCase(provider.providerName, "TuneIn Live-Radio")) { isRadio = true; } } String radioStationId = ""; - if (isRadio && mediaState != null && mediaState.currentState != null - && mediaState.currentState.equals("PLAYING") && mediaState.radioStationId != null) { + if (isRadio && mediaState != null && StringUtils.equals(mediaState.currentState, "PLAYING") + && mediaState.radioStationId != null) { radioStationId = mediaState.radioStationId; } // handle title, subtitle, imageUrl @@ -635,13 +631,13 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto if (entry != null) { if (isRadio) { - if (imageUrl.isEmpty() && entry.imageURL != null) { + if (StringUtils.isEmpty(imageUrl) && entry.imageURL != null) { imageUrl = entry.imageURL; } - if (subTitle1.isEmpty() && entry.radioStationSlogan != null) { + if (StringUtils.isEmpty(subTitle1) && entry.radioStationSlogan != null) { subTitle1 = entry.radioStationSlogan; } - if (subTitle2.isEmpty() && entry.radioStationLocation != null) { + if (StringUtils.isEmpty(subTitle2) && entry.radioStationLocation != null) { subTitle2 = entry.radioStationLocation; } } @@ -654,10 +650,8 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto if (provider.providerDisplayName != null) { providerDisplayName = provider.providerDisplayName; } - if (provider.providerName != null) { - if (providerDisplayName.isEmpty()) { - providerDisplayName = provider.providerName; - } + if (StringUtils.isNotEmpty(provider.providerName) && StringUtils.isEmpty(providerDisplayName)) { + providerDisplayName = provider.providerName; } } // handle volume diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java index ddbfc08c3c2d3..c1d6798ac155f 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java @@ -14,6 +14,7 @@ import java.net.URISyntaxException; import java.util.HashMap; +import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Bridge; @@ -101,6 +102,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { return; } String channelId = channelUID.getId(); + if (StringUtils.isEmpty(channelId)) { + return; + } try { handleCommand(connection, entityId, channelId, command); } catch (IOException | URISyntaxException e) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index fce4b82e455a0..abbee64d36c9a 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -37,6 +37,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAutomation; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAutomation.Payload; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAutomation.Trigger; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; @@ -238,7 +239,7 @@ public boolean tryRestoreLogin(@Nullable String data) { } catch (IOException e) { logger.info("verify login fails with io exception: {}", e); } catch (URISyntaxException e) { - logger.error("verify login fails with uri syntax exception: {}", e); + logger.warn("verify login fails with uri syntax exception: {}", e); } // anything goes wrong, remove session data cookieStore.removeAll(); @@ -251,10 +252,13 @@ public boolean tryRestoreLogin(@Nullable String data) { public String convertStream(InputStream input) throws IOException { Scanner inputScanner = new Scanner(input); Scanner scannerWithoutDelimiter = inputScanner.useDelimiter("\\A"); - String result = scannerWithoutDelimiter.hasNext() ? scannerWithoutDelimiter.next() : ""; + String result = scannerWithoutDelimiter.hasNext() ? scannerWithoutDelimiter.next() : null; inputScanner.close(); scannerWithoutDelimiter.close(); input.close(); + if (result == null) { + result = ""; + } return result; } @@ -657,8 +661,9 @@ public void startRoutine(Device device, String utterance) throws IOException, UR JsonAutomation found = null; String deviceLocale = ""; for (JsonAutomation routine : getRoutines()) { - if (routine.triggers != null && routine.sequence != null) { - for (JsonAutomation.Trigger trigger : routine.triggers) { + Trigger[] triggers = routine.triggers; + if (triggers != null && routine.sequence != null) { + for (JsonAutomation.Trigger trigger : triggers) { if (trigger == null) { continue; } @@ -727,8 +732,9 @@ public JsonFeed[] getEnabledFlashBriefings() throws IOException, URISyntaxExcept String json = makeRequestAndReturnString("GET", alexaServer + "/api/content-skills/enabled-feeds", null, null, true); JsonEnabledFeeds result = parseJson(json, JsonEnabledFeeds.class); - if (result.enabledFeeds != null) { - return result.enabledFeeds; + JsonFeed[] enabledFeeds = result.enabledFeeds; + if (enabledFeeds != null) { + return enabledFeeds; } return new JsonFeed[0]; } @@ -749,8 +755,9 @@ public JsonNotificationSound[] getNotificationSounds(Device device) throws IOExc + "&deviceType=" + device.deviceType + "&softwareVersion=" + device.softwareVersion, null, null, true); JsonNotificationSounds result = parseJson(json, JsonNotificationSounds.class); - if (result.notificationSounds != null) { - return result.notificationSounds; + JsonNotificationSound[] notificationSounds = result.notificationSounds; + if (notificationSounds != null) { + return notificationSounds; } return new JsonNotificationSound[0]; } @@ -813,13 +820,13 @@ public List getSmartHomeDevices() throws IOException, URISy searchSmartHomeDevicesRecursive(gson, jsonObject, result); return result; } catch (Exception e) { - logger.error("getSmartHomeDevices fails: {}", e.getMessage()); + logger.warn("getSmartHomeDevices fails: {}", e.getMessage()); throw e; } } - private void searchSmartHomeDevicesRecursive(Gson gson, Object jsonNode, List result) { - + private void searchSmartHomeDevicesRecursive(Gson gson, @Nullable Object jsonNode, + List result) { if (jsonNode instanceof Map) { @SuppressWarnings("rawtypes") Map map = (Map) jsonNode; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java index 12c9bd41c8f42..189072db18462 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java @@ -188,7 +188,7 @@ protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResp returnHtml(resp, html); } catch (URISyntaxException e) { - logger.error("get failed with uri syntax error {}", e); + logger.warn("get failed with uri syntax error {}", e); } } @@ -249,7 +249,7 @@ private void returnHtml(HttpServletResponse resp, String html) { try { resp.getWriter().write(resultHtml); } catch (IOException e) { - logger.error("return html failed with uri syntax error {}", e); + logger.warn("return html failed with uri syntax error {}", e); } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java index c6fef5c4ea7e3..3c46fc62ed245 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java @@ -98,27 +98,23 @@ private void saveProperties() { } catch (IOException e) { logger.error("Saving properties failed {}", e); } - } private Properties initProperties() { - if (properties != null) { - return properties; - } - - Properties p = new Properties(); - - if (propertyFile.exists()) { - try { - FileReader fileReader = new FileReader(propertyFile); - p.load(fileReader); - fileReader.close(); - } catch (IOException e) { - logger.error("Error occured on writing the property file.", e); + Properties result = properties; + if (result == null) { + result = new Properties(); + if (propertyFile.exists()) { + try { + FileReader fileReader = new FileReader(propertyFile); + result.load(fileReader); + fileReader.close(); + } catch (IOException e) { + logger.error("Error occured on writing the property file.", e); + } } + properties = result; } - properties = p; - return p; - + return result; } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 822e58e83d102..74449eee24513 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -10,7 +10,6 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -21,6 +20,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; @@ -52,7 +52,7 @@ public class AmazonEchoDiscovery extends AbstractDiscoveryService { static boolean discoverAccount = true; public @Nullable static AmazonEchoDiscovery instance; - private final static List discoveryServices = new ArrayList<>(); + private final static Set discoveryServices = new HashSet<>(); private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); private final Map lastDeviceInformations = new HashMap<>(); @@ -64,11 +64,8 @@ public class AmazonEchoDiscovery extends AbstractDiscoveryService { public static void addDiscoveryHandler(IAmazonEchoDiscovery discoveryService) { synchronized (discoveryServices) { - if (!discoveryServices.contains(discoveryService)) { - discoveryServices.add(discoveryService); - } + discoveryServices.add(discoveryService); } - } public static void removeDiscoveryHandler(IAmazonEchoDiscovery discoveryService) { @@ -165,7 +162,7 @@ public synchronized void setSmartHomeDevices(ThingUID brigdeThingUID, List deviceInformations) { Set toRemove = new HashSet(lastSmartHomeDeviceInformations.keySet()); for (JsonSmartHomeDevice deviceInformation : deviceInformations) { - if (deviceInformation.manufacturerName != null && deviceInformation.manufacturerName.equals("openHAB")) { + if (StringUtils.equalsIgnoreCase(deviceInformation.manufacturerName, "openHAB")) { // Ignore devices provided by the openHAB skill continue; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java index 60431c4553754..cd3dd54285547 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java @@ -8,6 +8,7 @@ */ package org.openhab.binding.amazonechocontrol.internal.jsons; +import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; @@ -30,8 +31,7 @@ public class JsonBluetoothStates { return null; } for (BluetoothState state : bluetoothStates) { - if (state != null && state.deviceSerialNumber != null - && state.deviceSerialNumber.equals(device.serialNumber)) { + if (state != null && StringUtils.equals(state.deviceSerialNumber, device.serialNumber)) { return state; } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java index 6dd8179da1def..b4797e68a1a94 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -13,7 +13,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; +import java.util.Map; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Channel; @@ -113,8 +115,10 @@ public AmazonEchoDynamicStateDescriptionProvider() { } ArrayList options = new ArrayList(); options.add(new StateOption("", "")); - if (playLists.playlists != null) { - for (PlayList[] innerLists : playLists.playlists.values()) { + @Nullable + Map<@NonNull String, @Nullable PlayList @Nullable []> playlistMap = playLists.playlists; + if (playlistMap != null) { + for (PlayList[] innerLists : playlistMap.values()) { if (innerLists != null && innerLists.length > 0) { PlayList playList = innerLists[0]; if (playList.playlistId != null && playList.title != null) { From c7dd65184c421648eeb7227c300a95c68706d015 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sat, 7 Apr 2018 22:59:25 +0200 Subject: [PATCH 27/56] [amazonechocontrol] Code review fixes, method reference usage, empty lines removed Signed-off-by: Michael Geramb (github: mgeramb) --- .../handler/AccountHandler.java | 48 ++++++++----------- .../handler/EchoHandler.java | 15 ++---- .../handler/FlashBriefingProfileHandler.java | 11 ----- .../handler/SmartHomeBaseHandler.java | 15 +++--- .../handler/SmartHomeDimmerHandler.java | 1 - .../handler/SmartHomeSwitchHandler.java | 2 - .../internal/AccountConfiguration.java | 1 - .../internal/Connection.java | 18 ------- .../internal/HttpException.java | 1 - .../internal/LoginServlet.java | 11 ----- .../internal/StateStorage.java | 1 - .../discovery/AmazonEchoDiscovery.java | 25 +++------- ...onEchoDynamicStateDescriptionProvider.java | 8 ---- 13 files changed, 38 insertions(+), 119 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index 07660197a5b0e..afaa0733b4501 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -80,7 +80,6 @@ public AccountHandler(Bridge bridge, HttpService httpService) { this.httpService = httpService; stateStorage = new StateStorage(bridge); AmazonEchoDiscovery.setHandlerExist(); - } @Override @@ -91,7 +90,6 @@ public void initialize() { @Override public void handleCommand(ChannelUID channelUID, Command command) { logger.trace("Command '{}' received for channel '{}'", command, channelUID); - if (command instanceof RefreshType) { refreshData(); } @@ -122,7 +120,6 @@ public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBrie if (currentFlashBriefingJson.isEmpty()) { updateFlashBriefingProfiles(connection); } - flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson); } } @@ -142,13 +139,13 @@ private void initializeEchoHandler(EchoHandler echoHandler, Connection connectio @Nullable Device device = findDeviceJson(echoHandler); - BluetoothState state = null; JsonBluetoothStates states = null; if (connection.getIsLoggedIn()) { states = connection.getBluetoothConnectionStates(); } + BluetoothState state = null; if (states != null) { state = states.findStateByDevice(device); } @@ -170,32 +167,29 @@ public void handleRemoval() { @Override public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) { - - // echo handler? + // check for echo handler if (childHandler instanceof EchoHandler) { synchronized (echoHandlers) { echoHandlers.remove(childHandler); } - AmazonEchoDiscovery instance = AmazonEchoDiscovery.instance; if (instance != null) { instance.removeExistingEchoHandler(childThing.getUID()); } } - // flash briefing profile handler? + // check for flash briefing profile handler if (childHandler instanceof FlashBriefingProfileHandler) { synchronized (flashBriefingProfileHandlers) { flashBriefingProfileHandlers.remove(childHandler); } } - // smart home handler? + // check for smart home handler if (childHandler instanceof SmartHomeBaseHandler) { synchronized (smartHomeHandlers) { smartHomeHandlers.remove(childHandler); } - AmazonEchoDiscovery instance = AmazonEchoDiscovery.instance; if (instance != null) { instance.removeExistingSmartHomeHandler(childThing.getUID()); @@ -241,7 +235,8 @@ private void start() { AccountConfiguration config = getConfigAs(AccountConfiguration.class); - if (StringUtils.isEmpty(config.amazonSite)) { + String amazonSite = config.amazonSite; + if (StringUtils.isEmpty(amazonSite)) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Amazon site not configured"); cleanup(); return; @@ -282,11 +277,9 @@ private void start() { } synchronized (synchronizeConnection) { Connection connection = this.connection; - if (connection == null || !connection.getEmail().equals(config.email) - || !connection.getPassword().equals(config.password) - || !connection.getAmazonSite().equals(config.amazonSite)) { - this.connection = new Connection(config.email, config.password, config.amazonSite, - this.getThing().getUID().getId()); + if (connection == null || !connection.getEmail().equals(email) || !connection.getPassword().equals(password) + || !connection.getAmazonSite().equals(amazonSite)) { + this.connection = new Connection(email, password, amazonSite, this.getThing().getUID().getId()); } } if (this.loginServlet == null) { @@ -295,25 +288,24 @@ private void start() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login"); - if (refreshLogin != null) { - refreshLogin.cancel(false); + @Nullable + ScheduledFuture oldRefreshLogin = refreshLogin; + if (oldRefreshLogin != null) { + oldRefreshLogin.cancel(false); } - refreshLogin = scheduler.scheduleWithFixedDelay(() -> { - checkLogin(); - }, 0, 60, TimeUnit.SECONDS); + refreshLogin = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS); - if (refreshJob != null) { - refreshJob.cancel(false); + @Nullable + ScheduledFuture oldRefreshJob = refreshJob; + if (oldRefreshJob != null) { + oldRefreshJob.cancel(false); } - refreshJob = scheduler.scheduleWithFixedDelay(() -> { - refreshData(); - }, 4, pollingIntervalInSeconds, TimeUnit.SECONDS); + refreshJob = scheduler.scheduleWithFixedDelay(this::refreshData, 4, pollingIntervalInSeconds, TimeUnit.SECONDS); logger.debug("amazon account bridge handler started."); } private void checkLogin() { - synchronized (synchronizeConnection) { Connection currentConnection = this.connection; if (currentConnection == null) { @@ -397,10 +389,8 @@ private void checkLogin() { } private void handleValidLogin() { - updateDeviceList(false); updateStatus(ThingStatus.ONLINE); - AmazonEchoDiscovery.addDiscoveryHandler(this); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 2040b466b041f..a86b62e2eb456 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -102,9 +102,7 @@ public void initialize() { account.addEchoHandler(this); } } - } - } public void intialize(Connection connection, @Nullable Device deviceJson) { @@ -156,7 +154,6 @@ public String findSerialNumber() { @Override public void handleCommand(ChannelUID channelUID, Command command) { try { - int waitForUpdate = 1000; boolean needBluetoothRefresh = false; String lastKnownBluetoothId = this.lastKnownBluetoothId; @@ -331,6 +328,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { connection.playRadio(device, ""); } } + // notification if (channelId.equals(CHANNEL_REMIND)) { if (command instanceof StringType) { @@ -393,7 +391,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { connection.executeSequenceCommand(device, "Alexa.Weather.Play"); } } - if (channelId.equals(CHANNEL_START_ROUTINE)) { if (command instanceof StringType) { String utterance = ((StringType) command).toFullString(); @@ -427,7 +424,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else { this.updateStateJob = scheduler.schedule(doRefresh, waitForUpdate, TimeUnit.MILLISECONDS); } - } catch (IOException | URISyntaxException e) { logger.info("handleCommand fails: {}", e); } @@ -470,7 +466,6 @@ private void updateNotificationTimerState() { logger.warn("update notification state fails: {}", e); } if (stopCurrentNotifcation) { - if (currentNotification != null) { String type = currentNotification.type; if (type != null) { @@ -589,6 +584,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto if (StringUtils.isNotEmpty(bluetoothId)) { lastKnownBluetoothId = bluetoothId; } + // handle radio boolean isRadio = false; if (mediaState != null && StringUtils.isNotEmpty(mediaState.radioStationId)) { @@ -602,6 +598,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto && mediaState.radioStationId != null) { radioStationId = mediaState.radioStationId; } + // handle title, subtitle, imageUrl String title = ""; String subTitle1 = ""; @@ -644,6 +641,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } } } + // handle provider String providerDisplayName = ""; if (provider != null) { @@ -654,6 +652,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto providerDisplayName = provider.providerName; } } + // handle volume Integer volume = null; if (mediaState != null) { @@ -665,7 +664,6 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto volume = volumnInfo.volume; } } - if (volume != null && volume > 0) { lastKnownVolume = volume; } @@ -698,14 +696,11 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto updateState(CHANNEL_TITLE, new StringType(title)); updateState(CHANNEL_SUBTITLE1, new StringType(subTitle1)); updateState(CHANNEL_SUBTITLE2, new StringType(subTitle2)); - if (bluetoothState != null) { updateState(CHANNEL_BLUETOOTH, bluetoothIsConnected ? OnOffType.ON : OnOffType.OFF); updateState(CHANNEL_BLUETOOTH_ID, new StringType(bluetoothId)); updateState(CHANNEL_BLUETOOTH_ID_SELECTION, new StringType(bluetoothId)); updateState(CHANNEL_BLUETOOTH_DEVICE_NAME, new StringType(bluetoothDeviceName)); } - } - } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java index 388e4d7adb093..14491f78bf906 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java @@ -127,13 +127,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (updateStateJob != null) { updateStateJob.cancel(false); } - try { String channelId = channelUID.getId(); if (command instanceof RefreshType) { waitForUpdate = 0; } - if (channelId.equals(CHANNEL_SAVE)) { if (command.equals(OnOffType.ON)) { saveCurrentProfile(accountHandler); @@ -145,7 +143,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { String currentConfigurationJson = this.currentConfigurationJson; if (!currentConfigurationJson.isEmpty()) { accountHandler.setEnabledFlashBriefingsJson(currentConfigurationJson); - updateState(CHANNEL_ACTIVE, OnOffType.ON); waitForUpdate = 500; } @@ -156,10 +153,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { String deviceSerialOrName = ((StringType) command).toFullString(); String currentConfigurationJson = this.currentConfigurationJson; if (!currentConfigurationJson.isEmpty()) { - String old = accountHandler.getEnabledFlashBriefingsJson(); accountHandler.setEnabledFlashBriefingsJson(currentConfigurationJson); - Device device = accountHandler.findDeviceJsonBySerialOrName(deviceSerialOrName); if (device == null) { logger.warn("Device '{}' not found", deviceSerialOrName); @@ -180,7 +175,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { } updatePlayOnDevice = true; waitForUpdate = 1000; - } } } @@ -194,18 +188,15 @@ public void handleCommand(ChannelUID channelUID, Command command) { } public void initialize(AccountHandler handler, String currentConfigurationJson) { - updateState(CHANNEL_SAVE, OnOffType.OFF); if (updatePlayOnDevice) { updateState(CHANNEL_PLAY_ON_DEVICE, new StringType("")); } if (this.accountHandler != handler) { - this.accountHandler = handler; String configurationJson = this.stateStorage.findState("configurationJson"); if (configurationJson == null || configurationJson.isEmpty()) { this.currentConfigurationJson = saveCurrentProfile(handler); - } else { removeFromDiscovery(); this.currentConfigurationJson = configurationJson; @@ -222,7 +213,6 @@ public void initialize(AccountHandler handler, String currentConfigurationJson) } else { updateState(CHANNEL_ACTIVE, OnOffType.OFF); } - } private String saveCurrentProfile(AccountHandler connection) { @@ -230,7 +220,6 @@ private String saveCurrentProfile(AccountHandler connection) { configurationJson = connection.getEnabledFlashBriefingsJson(); removeFromDiscovery(); this.currentConfigurationJson = configurationJson; - if (!configurationJson.isEmpty()) { this.stateStorage.storeState("configurationJson", configurationJson); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java index c1d6798ac155f..b284b7cddd7a0 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java @@ -37,8 +37,8 @@ public abstract class SmartHomeBaseHandler extends BaseThingHandler { private final static HashMap instances = new HashMap(); - private final Logger logger = LoggerFactory.getLogger(SmartHomeBaseHandler.class); + private final Logger logger = LoggerFactory.getLogger(SmartHomeBaseHandler.class); private @Nullable Connection connection; protected @Nullable Connection findConnection() { @@ -91,6 +91,12 @@ private String findEntityId() { return id; } + public static @Nullable SmartHomeBaseHandler find(ThingUID uid) { + synchronized (instances) { + return instances.get(uid); + } + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { Connection connection = findConnection(); @@ -114,11 +120,4 @@ public void handleCommand(ChannelUID channelUID, Command command) { protected abstract void handleCommand(Connection connection, String entityId, String channelId, Command command) throws IOException, URISyntaxException; - - public static @Nullable SmartHomeBaseHandler find(ThingUID uid) { - synchronized (instances) { - return instances.get(uid); - } - } - } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java index eed8bd7a2e291..988a45be84266 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java @@ -69,5 +69,4 @@ public void handleCommand(Connection connection, String entityId, String channel } } } - } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java index fc2ef0236bb47..7db28c0ee8762 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java @@ -34,7 +34,6 @@ public SmartHomeSwitchHandler(Thing thing) { @Override public void handleCommand(Connection connection, String entityId, String channelId, Command command) throws IOException, URISyntaxException { - if (channelId.equals(CHANNEL_SWITCH)) { if (command == OnOffType.ON) { connection.sendSmartHomeDeviceCommand(entityId, "turnOn", null, null); @@ -46,5 +45,4 @@ public void handleCommand(Connection connection, String entityId, String channel } } } - } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java index c3cd328b06b9f..c4ff430b770c8 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java @@ -33,5 +33,4 @@ public class AccountConfiguration { // there seems to be different smarthome skill versions @Nullable public Boolean discoverSmartHomeDevices; - } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index abbee64d36c9a..88a9d72863fed 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -103,7 +103,6 @@ public Connection(@Nullable String email, @Nullable String password, @Nullable S } this.amazonSite = correctedAmazonSite; alexaServer = "https://alexa." + this.amazonSite; - } public @Nullable Date tryGetLoginTime() { @@ -141,7 +140,6 @@ public String serializeLoginData() { builder.append(cookies.size()); builder.append("\n"); for (HttpCookie cookie : cookies) { - writeValue(builder, cookie.getName()); writeValue(builder, cookie.getValue()); writeValue(builder, cookie.getComment()); @@ -153,7 +151,6 @@ public String serializeLoginData() { writeValue(builder, cookie.getVersion()); writeValue(builder, cookie.getSecure()); writeValue(builder, cookie.getDiscard()); - } return builder.toString(); } @@ -182,7 +179,6 @@ public boolean tryRestoreLogin(@Nullable String data) { if (StringUtils.isEmpty(data)) { return false; } - Scanner scanner = new Scanner(data); String version = scanner.nextLine(); if (!version.equals("4")) { @@ -196,7 +192,6 @@ public boolean tryRestoreLogin(@Nullable String data) { scanner.close(); return false; } - int passwordHash = Integer.parseInt(scanner.nextLine()); if (passwordHash != this.password.hashCode()) { scanner.close(); @@ -206,9 +201,7 @@ public boolean tryRestoreLogin(@Nullable String data) { // Recreate session and cookies sessionId = scanner.nextLine(); Date loginTime = new Date(Long.parseLong(scanner.nextLine())); - CookieStore cookieStore = cookieManager.getCookieStore(); - cookieStore.removeAll(); Integer numberOfCookies = Integer.parseInt(scanner.nextLine()); @@ -246,7 +239,6 @@ public boolean tryRestoreLogin(@Nullable String data) { this.sessionId = null; this.loginTime = null; return false; - } public String convertStream(InputStream input) throws IOException { @@ -281,7 +273,6 @@ public HttpsURLConnection makeRequest(String verb, String url, @Nullable String int code; HttpsURLConnection connection; try { - logger.debug("Make request to {}", url); connection = (HttpsURLConnection) new URL(currentUrl).openConnection(); connection.setRequestMethod(verb); @@ -372,7 +363,6 @@ public HttpsURLConnection makeRequest(String verb, String url, @Nullable String } if (code == 302 && location != null) { logger.debug("Redirected to {}", location); - currentUrl = location; if (autoredirect) { continue; @@ -395,7 +385,6 @@ public boolean getIsLoggedIn() { } public String getLoginPage() throws IOException, URISyntaxException { - // clear session data cookieManager.getCookieStore().removeAll(); sessionId = null; @@ -423,7 +412,6 @@ public String getLoginPage() throws IOException, URISyntaxException { public void makeLogin() throws IOException, URISyntaxException { try { - String loginFormHtml = getLoginPage(); // read hidden form inputs, the will be used later in the url and for posting Pattern inputPattern = Pattern @@ -440,7 +428,6 @@ public void makeLogin() throws IOException, URISyntaxException { } String queryParameters = postDataBuilder.toString() + "session-id=" + URLEncoder.encode(sessionId, "UTF-8"); - logger.debug("Login query String: {}", queryParameters); postDataBuilder.append("email"); @@ -472,7 +459,6 @@ public void makeLogin() throws IOException, URISyntaxException { public @Nullable String postLoginData(@Nullable String optionalQueryParameters, String postData) throws IOException, URISyntaxException { - // build query parameters @Nullable String queryParameters = optionalQueryParameters; @@ -603,7 +589,6 @@ public void bluetooth(Device device, @Nullable String address) throws IOExceptio makeRequest("POST", alexaServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber, null, "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true, true); - } } @@ -764,7 +749,6 @@ public JsonNotificationSound[] getNotificationSounds(Device device) throws IOExc public JsonNotificationResponse notification(Device device, String type, @Nullable String label, @Nullable JsonNotificationSound sound) throws IOException, URISyntaxException { - Date date = new Date(new Date().getTime()); long createdDate = date.getTime(); Date alarm = new Date(createdDate + 5000); // add 5 seconds, because amazon does not except calls for times in @@ -793,7 +777,6 @@ public JsonNotificationResponse notification(Device device, String type, @Nullab data, true); JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); return result; - } public void stopNotification(JsonNotificationResponse notification) throws IOException, URISyntaxException { @@ -846,7 +829,6 @@ private void searchSmartHomeDevicesRecursive(Gson gson, @Nullable Object jsonNod public void sendSmartHomeDeviceCommand(String entityId, String action, @Nullable String parameterName, @Nullable String parameter) throws IOException, URISyntaxException { - String command = "{" + "\"controlRequests\": [{" + "\"entityId\": \"" + entityId + "\", " + "\"entityType\": \"APPLIANCE\", " + "\"parameters\": {" + "\"action\": \"" + action + "\"" + (parameterName != null ? ", \"" + parameterName + "\": \"" + parameter + "\"" : "") + " }" + "}]" diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java index 82361aed357ef..59323f9a47271 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/HttpException.java @@ -28,6 +28,5 @@ public int getCode() { public HttpException(int code, String message) { super(message); this.code = code; - } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java index 189072db18462..ef571dc2c8160 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java @@ -64,10 +64,8 @@ public LoginServlet(HttpService httpService, String id, AccountHandler account, try { httpService.registerServlet(servletUrl, this, null, httpService.createDefaultHttpContext()); } catch (ServletException e) { - logger.warn("Register servlet fails {}", e); } catch (NamespaceException e) { - logger.warn("Register servlet fails {}", e); } } @@ -78,7 +76,6 @@ private Connection reCreateConnection() { public void dispose() { httpService.unregister(servletUrl); - } @Override @@ -95,7 +92,6 @@ protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletRes Map map = req.getParameterMap(); StringBuilder postDataBuilder = new StringBuilder(); for (String name : map.keySet()) { - if (postDataBuilder.length() > 0) { postDataBuilder.append('&'); } @@ -131,7 +127,6 @@ protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletRes String referer = "https://www." + connection.getAmazonSite(); String postData = postDataBuilder.toString(); HandleProxyRequest(resp, "POST", postUrl, referer, postData); - } @Override @@ -174,7 +169,6 @@ protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResp "The Account is already logged in. The account thing should be online.
Check Thing in Paper UI"); - return; } @@ -186,7 +180,6 @@ protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResp String html = this.connection.getLoginPage(); returnHtml(resp, html); - } catch (URISyntaxException e) { logger.warn("get failed with uri syntax error {}", e); } @@ -194,7 +187,6 @@ protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResp void HandleProxyRequest(HttpServletResponse resp, String verb, String url, @Nullable String referer, @Nullable String postData) throws IOException { - HttpsURLConnection urlConnection; try { urlConnection = connection.makeRequest(verb, url, referer, postData, false, false); @@ -232,9 +224,7 @@ void HandleProxyRequest(HttpServletResponse resp, String verb, String url, @Null return; } } - } catch (URISyntaxException e) { - returnError(resp, e.getLocalizedMessage()); return; } @@ -255,7 +245,6 @@ private void returnHtml(HttpServletResponse resp, String html) { void returnError(HttpServletResponse resp, String errorMessage) { try { - resp.getWriter().write("" + StringEscapeUtils.escapeHtml(errorMessage) + "
Try again"); } catch (IOException e) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java index 3c46fc62ed245..f5097625ff462 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java @@ -82,7 +82,6 @@ public void storeState(@Nullable String key, @Nullable String value) { } private void saveProperties() { - try { Properties properties = initProperties(); logger.debug("Create file {}.", propertyFile); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 74449eee24513..d0add18236468 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -49,10 +49,9 @@ @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.amazonechocontrol") public class AmazonEchoDiscovery extends AbstractDiscoveryService { - static boolean discoverAccount = true; - - public @Nullable static AmazonEchoDiscovery instance; + private static boolean discoverAccount = true; private final static Set discoveryServices = new HashSet<>(); + public @Nullable static AmazonEchoDiscovery instance; private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); private final Map lastDeviceInformations = new HashMap<>(); @@ -92,8 +91,11 @@ protected void startScan() { startScan(true); } - protected void startScan(boolean manual) { + protected void startAutomaticScan() { + startScan(false); + } + void startScan(boolean manual) { if (startScanStateJob != null) { startScanStateJob.cancel(false); startScanStateJob = null; @@ -105,14 +107,11 @@ protected void startScan(boolean manual) { ThingUID thingUID = new ThingUID(THING_TYPE_ACCOUNT, "account1"); DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("Amazon Account").build(); - logger.debug("Device [Amazon Account] found."); - thingDiscovered(result); } IAmazonEchoDiscovery[] accounts; - synchronized (discoveryServices) { accounts = new IAmazonEchoDiscovery[discoveryServices.size()]; accounts = discoveryServices.toArray(accounts); @@ -121,7 +120,6 @@ protected void startScan(boolean manual) { for (IAmazonEchoDiscovery discovery : accounts) { discovery.updateDeviceList(manual); } - } @Override @@ -131,12 +129,7 @@ protected void startBackgroundDiscovery() { startScanStateJob.cancel(false); startScanStateJob = null; } - - startScanStateJob = scheduler.schedule(() -> { - - startScan(false); - }, 3000, TimeUnit.MILLISECONDS); - + startScanStateJob = scheduler.schedule(this::startAutomaticScan, 3000, TimeUnit.MILLISECONDS); } @Override @@ -146,7 +139,6 @@ protected void stopBackgroundDiscovery() { startScanStateJob.cancel(false); startScanStateJob = null; } - } @Override @@ -199,14 +191,12 @@ public synchronized void setSmartHomeDevices(ThingUID brigdeThingUID, lastSmartHomeDeviceInformations.put(entityId, thingUID); } } - } } } } public synchronized void setDevices(ThingUID brigdeThingUID, Device[] deviceInformations) { - Set toRemove = new HashSet(lastDeviceInformations.keySet()); for (Device deviceInformation : deviceInformations) { String serialNumber = deviceInformation.serialNumber; @@ -296,5 +286,4 @@ public synchronized void removeExistingFlashBriefingProfile(@Nullable String cur discoverdFlashBriefings.remove(currentFlashBriefingJson); } } - } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java index b4797e68a1a94..c30572077359a 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -49,19 +49,13 @@ public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDe private final Logger logger = LoggerFactory.getLogger(AmazonEchoDynamicStateDescriptionProvider.class); - public AmazonEchoDynamicStateDescriptionProvider() { - - } - @Override public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { - if (originalStateDescription == null) { return null; } if (CHANNEL_TYPE_BLUETHOOTH_ID_SELECTION.equals(channel.getChannelTypeUID())) { - EchoHandler handler = EchoHandler.find(channel.getUID().getThingUID()); if (handler == null) { return originalStateDescription; @@ -133,7 +127,6 @@ public AmazonEchoDynamicStateDescriptionProvider() { originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); return result; } else if (CHANNEL_TYPE_PLAY_ALARM_SOUND.equals(channel.getChannelTypeUID())) { - EchoHandler handler = EchoHandler.find(channel.getUID().getThingUID()); if (handler == null) { return originalStateDescription; @@ -198,7 +191,6 @@ public AmazonEchoDynamicStateDescriptionProvider() { originalStateDescription.getMaximum(), originalStateDescription.getStep(), originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); return result; - } return originalStateDescription; } From 6945fdd8d19bd80a90a3f1c61fcb6015466b916e Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 8 Apr 2018 09:30:25 +0200 Subject: [PATCH 28/56] [amazonechocontrol] Credits section in readme added Signed-off-by: Michael Geramb (github: mgeramb) --- .../org.openhab.binding.amazonechocontrol/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 6f2b37743ea0a..1bb3e4de19749 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -2,9 +2,7 @@ This binding can control Amazon Echo devices (Alexa) from openhab. -The idea for writing this binding came from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German). Thank you Alex! - -The binding provide features to control and view the current state of echo dot devices: +It provide features to control and view the current state of echo dot devices: - volume - pause/continue/next track/previous track @@ -19,7 +17,7 @@ The binding provide features to control and view the current state of echo dot d - start daily briefing - start weather report - start automation routine -- have multiple configurations of flash briefings +- activate multiple configurations of flash briefings Some ideas what you can do in your home by using rules and other openHAB controlled devices: @@ -230,7 +228,9 @@ To get instead of the id fields an selection box, use the Selection element and ``` Selection item=Echo_Living_Room_RadioStationId mappings=[ ''='Off', 's1139'='Antenne Steiermark', 's8007'='Hitradio Ă–3', 's16793'='Radio 10', 's8235'='FM4' ] ``` +## Credits +The idea for writing this binding came from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German). Thank you Alex! ## Trademark Disclaimer From 5cae50fc27c56c817bc9d9e953d80f8efb0f321b Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 8 Apr 2018 10:09:38 +0200 Subject: [PATCH 29/56] [amazonechocontrol] Null reference warnings fixed Signed-off-by: Michael Geramb (github: mgeramb) --- .../internal/Connection.java | 9 ++++++--- .../discovery/AmazonEchoDiscovery.java | 20 +++++++++---------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 88a9d72863fed..60f4b9842a4af 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -241,7 +241,10 @@ public boolean tryRestoreLogin(@Nullable String data) { return false; } - public String convertStream(InputStream input) throws IOException { + public String convertStream(@Nullable InputStream input) throws IOException { + if (input == null) { + return ""; + } Scanner inputScanner = new Scanner(input); Scanner scannerWithoutDelimiter = inputScanner.useDelimiter("\\A"); String result = scannerWithoutDelimiter.hasNext() ? scannerWithoutDelimiter.next() : null; @@ -457,7 +460,7 @@ public void makeLogin() throws IOException, URISyntaxException { } } - public @Nullable String postLoginData(@Nullable String optionalQueryParameters, String postData) + public @Nullable String postLoginData(@Nullable String optionalQueryParameters, @Nullable String postData) throws IOException, URISyntaxException { // build query parameters @Nullable @@ -656,7 +659,7 @@ public void startRoutine(Device device, String utterance) throws IOException, UR if (payload == null) { continue; } - if (payload.utterance != null && payload.utterance.equalsIgnoreCase(utterance)) { + if (StringUtils.equalsIgnoreCase(payload.utterance, utterance)) { found = routine; deviceLocale = payload.locale; break; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index d0add18236468..416ea8153a568 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -96,10 +96,7 @@ protected void startAutomaticScan() { } void startScan(boolean manual) { - if (startScanStateJob != null) { - startScanStateJob.cancel(false); - startScanStateJob = null; - } + stopScanJob(); if (discoverAccount) { discoverAccount = false; @@ -125,18 +122,21 @@ void startScan(boolean manual) { @Override protected void startBackgroundDiscovery() { AmazonEchoDiscovery.instance = this; - if (startScanStateJob != null) { - startScanStateJob.cancel(false); - startScanStateJob = null; - } + stopScanJob(); startScanStateJob = scheduler.schedule(this::startAutomaticScan, 3000, TimeUnit.MILLISECONDS); } @Override protected void stopBackgroundDiscovery() { AmazonEchoDiscovery.instance = null; - if (startScanStateJob != null) { - startScanStateJob.cancel(false); + stopScanJob(); + } + + void stopScanJob() { + @Nullable + ScheduledFuture currentStartScanStateJob = startScanStateJob; + if (currentStartScanStateJob != null) { + currentStartScanStateJob.cancel(false); startScanStateJob = null; } } From edc38e3b9fe78c35713cc84471218f6ceb8c827f Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 8 Apr 2018 11:33:44 +0200 Subject: [PATCH 30/56] [amazonechocontrol] Null reference warnings fixed Signed-off-by: Michael Geramb (github: mgeramb) --- .../binding/amazonechocontrol/internal/Connection.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 60f4b9842a4af..34fa46107394f 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -168,13 +168,15 @@ private void writeValue(StringBuilder builder, @Nullable Object value) { private String readValue(Scanner scanner) { if (scanner.nextLine().equals("1")) { - return scanner.nextLine(); + String result = scanner.nextLine(); + if (result != null) { + return result; + } } return ""; } public boolean tryRestoreLogin(@Nullable String data) { - // verify store data if (StringUtils.isEmpty(data)) { return false; From 339b5e39a4c418dd9f116fe070bec0d4b789b18f Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Mon, 9 Apr 2018 19:33:07 +0200 Subject: [PATCH 31/56] [amazonechocontrol] intialize / dispose handling of Account handler, arrays in API changed to list Signed-off-by: Michael Geramb (github: mgeramb) --- .../handler/AccountHandler.java | 145 ++++++++---------- .../internal/Connection.java | 7 +- .../discovery/AmazonEchoDiscovery.java | 13 +- ...onEchoDynamicStateDescriptionProvider.java | 6 +- 4 files changed, 76 insertions(+), 95 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index afaa0733b4501..c3e3cc63c4e06 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.net.URISyntaxException; import java.net.UnknownHostException; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -70,7 +71,7 @@ public class AccountHandler extends BaseBridgeHandler implements IAmazonEchoDisc private @Nullable ScheduledFuture refreshLogin; private boolean updateSmartHomeDeviceList; private boolean discoverFlashProfiles; - private boolean smartHodeDeviceListEnabled; + private boolean smartHomeDeviceListEnabled; private String currentFlashBriefingJson = ""; private final HttpService httpService; private @Nullable LoginServlet loginServlet; @@ -84,7 +85,62 @@ public AccountHandler(Bridge bridge, HttpService httpService) { @Override public void initialize() { - start(); + logger.debug("amazon account bridge starting handler ..."); + + AccountConfiguration config = getConfigAs(AccountConfiguration.class); + + String amazonSite = config.amazonSite; + if (StringUtils.isEmpty(amazonSite)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Amazon site not configured"); + return; + } + String email = config.email; + if (StringUtils.isEmpty(email)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Account email not configured"); + return; + } + String password = config.password; + if (StringUtils.isEmpty(password)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Account password not configured"); + return; + } + Integer pollingIntervalInSeconds = config.pollingIntervalInSeconds; + if (pollingIntervalInSeconds == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Polling interval not configured"); + return; + } + Boolean discoverSmartHomeDevices = config.discoverSmartHomeDevices; + if (discoverSmartHomeDevices != null && discoverSmartHomeDevices) { + if (!smartHomeDeviceListEnabled) { + updateSmartHomeDeviceList = true; + } + smartHomeDeviceListEnabled = true; + } else { + smartHomeDeviceListEnabled = false; + } + + if (pollingIntervalInSeconds < 10) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Polling interval less than 10 seconds not allowed"); + return; + } + synchronized (synchronizeConnection) { + Connection connection = this.connection; + if (connection == null || !connection.getEmail().equals(email) || !connection.getPassword().equals(password) + || !connection.getAmazonSite().equals(amazonSite)) { + this.connection = new Connection(email, password, amazonSite, this.getThing().getUID().getId()); + } + } + if (this.loginServlet == null) { + this.loginServlet = new LoginServlet(httpService, this.getThing().getUID().getId(), this, config); + } + + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login"); + + refreshLogin = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS); + refreshJob = scheduler.scheduleWithFixedDelay(this::refreshData, 4, pollingIntervalInSeconds, TimeUnit.SECONDS); + + logger.debug("amazon account bridge handler started."); } @Override @@ -95,10 +151,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - public Device[] getLastKnownDevices() { - Device[] devices = new Device[jsonSerialNumberDeviceMapping.size()]; - jsonSerialNumberDeviceMapping.values().toArray(devices); - return devices; + public List getLastKnownDevices() { + return new ArrayList<>(jsonSerialNumberDeviceMapping.values()); } public void addEchoHandler(EchoHandler echoHandler) { @@ -230,81 +284,6 @@ private void cleanup() { } } - private void start() { - logger.debug("amazon account bridge starting handler ..."); - - AccountConfiguration config = getConfigAs(AccountConfiguration.class); - - String amazonSite = config.amazonSite; - if (StringUtils.isEmpty(amazonSite)) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Amazon site not configured"); - cleanup(); - return; - } - String email = config.email; - if (StringUtils.isEmpty(email)) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Account email not configured"); - cleanup(); - return; - } - String password = config.password; - if (StringUtils.isEmpty(password)) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Account password not configured"); - cleanup(); - return; - } - Integer pollingIntervalInSeconds = config.pollingIntervalInSeconds; - if (pollingIntervalInSeconds == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Polling interval not configured"); - cleanup(); - return; - } - Boolean discoverSmartHomeDevices = config.discoverSmartHomeDevices; - if (discoverSmartHomeDevices != null && discoverSmartHomeDevices) { - if (!smartHodeDeviceListEnabled) { - updateSmartHomeDeviceList = true; - } - smartHodeDeviceListEnabled = true; - } else { - smartHodeDeviceListEnabled = false; - } - - if (pollingIntervalInSeconds < 10) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Polling interval less than 10 seconds not allowed"); - cleanup(); - return; - } - synchronized (synchronizeConnection) { - Connection connection = this.connection; - if (connection == null || !connection.getEmail().equals(email) || !connection.getPassword().equals(password) - || !connection.getAmazonSite().equals(amazonSite)) { - this.connection = new Connection(email, password, amazonSite, this.getThing().getUID().getId()); - } - } - if (this.loginServlet == null) { - this.loginServlet = new LoginServlet(httpService, this.getThing().getUID().getId(), this, config); - } - - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login"); - - @Nullable - ScheduledFuture oldRefreshLogin = refreshLogin; - if (oldRefreshLogin != null) { - oldRefreshLogin.cancel(false); - } - refreshLogin = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS); - - @Nullable - ScheduledFuture oldRefreshJob = refreshJob; - if (oldRefreshJob != null) { - oldRefreshJob.cancel(false); - } - refreshJob = scheduler.scheduleWithFixedDelay(this::refreshData, 4, pollingIntervalInSeconds, TimeUnit.SECONDS); - - logger.debug("amazon account bridge handler started."); - } - private void checkLogin() { synchronized (synchronizeConnection) { Connection currentConnection = this.connection; @@ -487,7 +466,7 @@ public void updateDeviceList(boolean manualScan) { } AmazonEchoDiscovery discoveryService = AmazonEchoDiscovery.instance; - Device[] devices = null; + List devices = null; try { if (currentConnection.getIsLoggedIn()) { devices = currentConnection.getDeviceList(); @@ -521,7 +500,7 @@ public void updateDeviceList(boolean manualScan) { } updateFlashBriefingHandlers(currentConnection); - if (discoveryService != null && updateSmartHomeDeviceList && smartHodeDeviceListEnabled) { + if (discoveryService != null && updateSmartHomeDeviceList && smartHomeDeviceListEnabled) { updateSmartHomeDeviceList = false; List smartHomeDevices = null; try { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 34fa46107394f..719aaefce0b36 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -23,6 +23,7 @@ import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map; @@ -528,14 +529,14 @@ private T parseJson(String json, Class type) { // commands and states - public Device[] getDeviceList() throws IOException, URISyntaxException { + public List getDeviceList() throws IOException, URISyntaxException { String json = getDeviceListJson(); JsonDevices devices = parseJson(json, JsonDevices.class); Device[] result = devices.devices; if (result == null) { - result = new Device[0]; + return new ArrayList<>(); } - return result; + return new ArrayList(Arrays.asList(result)); } public String getDeviceListJson() throws IOException, URISyntaxException { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 416ea8153a568..6ed5ab3a1bc6e 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -196,14 +196,14 @@ public synchronized void setSmartHomeDevices(ThingUID brigdeThingUID, } } - public synchronized void setDevices(ThingUID brigdeThingUID, Device[] deviceInformations) { + public synchronized void setDevices(ThingUID brigdeThingUID, List deviceList) { Set toRemove = new HashSet(lastDeviceInformations.keySet()); - for (Device deviceInformation : deviceInformations) { - String serialNumber = deviceInformation.serialNumber; + for (Device device : deviceList) { + String serialNumber = device.serialNumber; if (serialNumber != null) { boolean alreadyfound = toRemove.remove(serialNumber); // new - String deviceFamily = deviceInformation.deviceFamily; + String deviceFamily = device.deviceFamily; if (!alreadyfound && deviceFamily != null) { ThingTypeUID thingTypeId; if (deviceFamily.equals("ECHO")) { @@ -223,14 +223,13 @@ public synchronized void setDevices(ThingUID brigdeThingUID, Device[] deviceInfo // Check if already created if (EchoHandler.find(thingUID) == null) { - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) - .withLabel(deviceInformation.accountName) + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel(device.accountName) .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) .withProperty(DEVICE_PROPERTY_FAMILY, deviceFamily) .withRepresentationProperty(DEVICE_PROPERTY_SERIAL_NUMBER).withBridge(brigdeThingUID) .build(); - logger.debug("Device [{}: {}] found. Mapped to thing type {}", deviceInformation.deviceFamily, + logger.debug("Device [{}: {}] found. Mapped to thing type {}", device.deviceFamily, serialNumber, thingTypeId.getAsString()); thingDiscovered(result); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java index c30572077359a..68c1b7b866433 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -175,8 +176,9 @@ public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDe if (accountHandler == null) { return originalStateDescription; } - Device[] devices = accountHandler.getLastKnownDevices(); - if (devices.length == 0) { + @NonNull + List<@NonNull Device> devices = accountHandler.getLastKnownDevices(); + if (devices.size() == 0) { return originalStateDescription; } From 563bdba8f63eeb6e8cc789fbb97aa6d47eef9ef7 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Tue, 17 Apr 2018 18:42:59 +0200 Subject: [PATCH 32/56] [amazonechocontrol] Exception handler and trace added for unexpected exceptions which can cause stopping background update Signed-off-by: Michael Geramb (github: mgeramb) --- .../handler/AccountHandler.java | 213 ++++++++++-------- .../internal/Connection.java | 17 +- 2 files changed, 129 insertions(+), 101 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index c3e3cc63c4e06..52899dd1e5684 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -265,6 +265,7 @@ public void dispose() { } private void cleanup() { + logger.debug("cleanup {}", getThing().getUID().getAsString()); @Nullable ScheduledFuture refreshJob = this.refreshJob; if (refreshJob != null) { @@ -285,85 +286,93 @@ private void cleanup() { } private void checkLogin() { - synchronized (synchronizeConnection) { - Connection currentConnection = this.connection; - if (currentConnection == null) { - return; - } - Date loginTime = currentConnection.tryGetLoginTime(); - Date currentDate = new Date(); - long currentTime = currentDate.getTime(); - if (loginTime != null && currentTime - loginTime.getTime() > 3600000) // One hour - { - try { - if (!currentConnection.verifyLogin()) { + try { + logger.debug("check login {}", getThing().getUID().getAsString()); + + synchronized (synchronizeConnection) { + Connection currentConnection = this.connection; + if (currentConnection == null) { + return; + } + Date verifyTime = currentConnection.tryGetVerifyTime(); + Date currentDate = new Date(); + long currentTime = currentDate.getTime(); + if (verifyTime != null && currentTime - verifyTime.getTime() > 3600000) // Every one hour + { + try { + if (!currentConnection.verifyLogin()) { + currentConnection.logout(); + } + } catch (IOException | URISyntaxException e) { + logger.info("logout failed: {}", e.getMessage()); currentConnection.logout(); } - } catch (IOException | URISyntaxException e) { - logger.info("logout failed: {}", e.getMessage()); - currentConnection.logout(); } - } - loginTime = currentConnection.tryGetLoginTime(); - if (loginTime != null && currentTime - loginTime.getTime() > 86400000 * 5) // 5 days - { - // Recreate session - this.stateStorage.storeState("sessionStorage", ""); - currentConnection = new Connection(currentConnection.getEmail(), currentConnection.getPassword(), - currentConnection.getAmazonSite(), this.getThing().getUID().getId()); - } - boolean loginIsValid = true; - if (!currentConnection.getIsLoggedIn()) { - try { - - // read session data from property - String sessionStore = this.stateStorage.findState("sessionStorage"); - - // try use the session data - if (!currentConnection.tryRestoreLogin(sessionStore)) { - // session data not valid -> login - int retry = 0; - while (true) { - try { - currentConnection.makeLogin(); - break; - } catch (ConnectionException e) { - // Up to 2 retries for login - retry++; - if (retry >= 2) { - currentConnection.logout(); - throw e; - } - // give amazon some time + Date loginTime = currentConnection.tryGetLoginTime(); + if (loginTime != null && currentTime - loginTime.getTime() > 86400000 * 5) // 5 days + { + // Recreate session + this.stateStorage.storeState("sessionStorage", ""); + currentConnection = new Connection(currentConnection.getEmail(), currentConnection.getPassword(), + currentConnection.getAmazonSite(), this.getThing().getUID().getId()); + } + boolean loginIsValid = true; + if (!currentConnection.getIsLoggedIn()) { + try { + + // read session data from property + String sessionStore = this.stateStorage.findState("sessionStorage"); + + // try use the session data + if (!currentConnection.tryRestoreLogin(sessionStore)) { + // session data not valid -> login + int retry = 0; + while (true) { try { - Thread.sleep(2000); - } catch (InterruptedException exception) { - // throw the original exception - throw e; + currentConnection.makeLogin(); + break; + } catch (ConnectionException e) { + // Up to 2 retries for login + retry++; + if (retry >= 2) { + currentConnection.logout(); + throw e; + } + // give amazon some time + try { + Thread.sleep(2000); + } catch (InterruptedException exception) { + // throw the original exception + throw e; + } } } + // store session data in property + String serializedStorage = currentConnection.serializeLoginData(); + this.stateStorage.storeState("sessionStorage", serializedStorage); } - // store session data in property - String serializedStorage = currentConnection.serializeLoginData(); - this.stateStorage.storeState("sessionStorage", serializedStorage); + updateSmartHomeDeviceList = true; + this.connection = currentConnection; + } catch (UnknownHostException e) { + loginIsValid = false; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unknown host name '" + + e.getMessage() + "'. Maybe your internet connection is offline"); + } catch (IOException e) { + loginIsValid = false; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + e.getLocalizedMessage()); + } catch (URISyntaxException e) { + loginIsValid = false; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + e.getLocalizedMessage()); + } + if (loginIsValid) { + handleValidLogin(); } - updateSmartHomeDeviceList = true; - this.connection = currentConnection; - } catch (UnknownHostException e) { - loginIsValid = false; - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Unknown host name '" + e.getMessage() + "'. Maybe your internet connection is offline"); - } catch (IOException e) { - loginIsValid = false; - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); - } catch (URISyntaxException e) { - loginIsValid = false; - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); - } - if (loginIsValid) { - handleValidLogin(); } } + } catch (Exception e) { + logger.error("check login fails {}", e); } } @@ -383,43 +392,49 @@ public void setConnection(Connection connection) { } private void refreshData() { - logger.debug("amazon account bridge refreshing data ..."); - - // check if logged in - Connection currentConnection = null; - synchronized (synchronizeConnection) { - currentConnection = connection; - if (currentConnection != null) { - if (!currentConnection.getIsLoggedIn()) { - return; + try { + logger.debug("refreshing data {}", getThing().getUID().getAsString()); + + // check if logged in + Connection currentConnection = null; + synchronized (synchronizeConnection) { + currentConnection = connection; + if (currentConnection != null) { + if (!currentConnection.getIsLoggedIn()) { + return; + } } } - } - if (currentConnection == null) { - return; - } + if (currentConnection == null) { + return; + } - // get all devices registered in the account - updateDeviceList(false); + // get all devices registered in the account + updateDeviceList(false); - // update bluetooth states - JsonBluetoothStates states = null; - if (currentConnection.getIsLoggedIn()) { - states = currentConnection.getBluetoothConnectionStates(); - } + // update bluetooth states + JsonBluetoothStates states = null; + if (currentConnection.getIsLoggedIn()) { + states = currentConnection.getBluetoothConnectionStates(); + } - // forward device information to echo handler - for (EchoHandler child : echoHandlers) { - Device device = findDeviceJson(child); - BluetoothState state = null; - if (states != null) { - state = states.findStateByDevice(device); + // forward device information to echo handler + for (EchoHandler child : echoHandlers) { + Device device = findDeviceJson(child); + BluetoothState state = null; + if (states != null) { + state = states.findStateByDevice(device); + } + child.updateState(device, state); } - child.updateState(device, state); - } - // update account state - updateStatus(ThingStatus.ONLINE); + // update account state + updateStatus(ThingStatus.ONLINE); + + logger.debug("refresh data {} finished", getThing().getUID().getAsString()); + } catch (Exception e) { + logger.error("refresh data {} failed: {}", getThing().getUID().getAsString(), e); + } } public @Nullable Device findDeviceJson(EchoHandler echoHandler) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 719aaefce0b36..528a6bbf7c743 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -81,6 +81,7 @@ public class Connection { private @Nullable String sessionId; private @Nullable Date loginTime; + private @Nullable Date verifyTime; public Connection(@Nullable String email, @Nullable String password, @Nullable String amazonSite, @Nullable String accountThingId) { @@ -110,6 +111,10 @@ public Connection(@Nullable String email, @Nullable String password, @Nullable S return loginTime; } + public @Nullable Date tryGetVerifyTime() { + return verifyTime; + } + public String getEmail() { return email; } @@ -241,6 +246,7 @@ public boolean tryRestoreLogin(@Nullable String data) { cookieStore.removeAll(); this.sessionId = null; this.loginTime = null; + this.verifyTime = null; return false; } @@ -395,6 +401,8 @@ public String getLoginPage() throws IOException, URISyntaxException { cookieManager.getCookieStore().removeAll(); sessionId = null; loginTime = null; + verifyTime = null; + logger.debug("Start Login to {}", alexaServer); // get login form String loginFormHtml = makeRequestAndReturnString(alexaServer); @@ -457,6 +465,7 @@ public void makeLogin() throws IOException, URISyntaxException { cookieManager.getCookieStore().removeAll(); sessionId = null; loginTime = null; + verifyTime = null; logger.info("Login failed: {}", e.getLocalizedMessage()); // rethrow throw e; @@ -503,8 +512,11 @@ public void makeLogin() throws IOException, URISyntaxException { public boolean verifyLogin() throws IOException, URISyntaxException { String response = makeRequestAndReturnString(alexaServer + "/api/bootstrap?version=0"); Boolean result = response.contains("\"authenticated\":true"); - if (result && loginTime == null) { - loginTime = new Date(); + if (result) { + verifyTime = new Date(); + if (loginTime == null) { + loginTime = verifyTime; + } } return result; } @@ -513,6 +525,7 @@ public void logout() { cookieManager.getCookieStore().removeAll(); sessionId = null; loginTime = null; + verifyTime = null; } // parser From e8d8f2f69592875e991d6f60646234e4743eabfa Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Tue, 17 Apr 2018 19:18:26 +0200 Subject: [PATCH 33/56] [amazonechocontrol] Fix travis build error and warning for readme Signed-off-by: Michael Geramb (github: mgeramb) --- addons/binding/org.openhab.binding.amazonechocontrol/README.md | 1 + .../binding/amazonechocontrol/handler/AccountHandler.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 1bb3e4de19749..780969cacaa94 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -228,6 +228,7 @@ To get instead of the id fields an selection box, use the Selection element and ``` Selection item=Echo_Living_Room_RadioStationId mappings=[ ''='Off', 's1139'='Antenne Steiermark', 's8007'='Hitradio Ă–3', 's16793'='Radio 10', 's8235'='FM4' ] ``` + ## Credits The idea for writing this binding came from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German). Thank you Alex! diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index 52899dd1e5684..1f0d498d29881 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -433,7 +433,7 @@ private void refreshData() { logger.debug("refresh data {} finished", getThing().getUID().getAsString()); } catch (Exception e) { - logger.error("refresh data {} failed: {}", getThing().getUID().getAsString(), e); + logger.error("refresh data failed: {}", e); } } From bf57fac9d286b3e76d9b75ce02b38c371544e8dc Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Tue, 24 Apr 2018 19:26:54 +0200 Subject: [PATCH 34/56] [amazonechocontrol] amazon.in added, tutorial for alarm sound rule added in readme, german translation text fixed Signed-off-by: Michael Geramb (github: mgeramb) --- .../i18n/amazonechocontrol_de.properties | 6 ++-- .../ESH-INF/thing/thing-types.xml | 3 +- .../README.md | 34 +++++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties index ad81af2e905d9..868a4c0dd8d5a 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties @@ -8,15 +8,13 @@ thing-type.amazonechocontrol.account.label = Amazon Konto thing-type.amazonechocontrol.account.description = Amazon Konto bei dem dein Amazon Echo registriert ist. Hinweis: 2 Faktor Authentifizierung ist nicht unterstützt. thing-type.config.amazonechocontrol.account.amazonSite.label = Amazon Seite -thing-type.config.amazonechocontrol.account.amazonSite.description = Wähle die Seite bei der dein Amazon Konto erstellt wurde. Hinweis: 2 Faktor Authentifizierung ist nicht unterstützt. +thing-type.config.amazonechocontrol.account.amazonSite.description = Wähle oder tippe die Seite ein bei der dein Amazon Konto erstellt wurde. Hinweis: 2 Faktor Authentifizierung ist nicht unterstützt. thing-type.config.amazonechocontrol.account.email.label = Amazon Konto E-Mail thing-type.config.amazonechocontrol.account.email.description = E-Mail des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde eingeben. thing-type.config.amazonechocontrol.account.password.label = Amazon Konto Kennwort -thing-type.config.amazonechocontrol.account.password.description = Kennwort des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde eingeben. WICHTIG: Sollte das Account-Thing nicht Online gehen und einen Login-Fehler melden, öffne die URL YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in deinem Browser (Z.B.: http://openhab:8080/amazonechocontrol/account) und versuche dich anzumelden. - e.g. http://openhab:8080/amazonechocontrol/account and try to login. - +thing-type.config.amazonechocontrol.account.password.description = Kennwort des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde eingeben. WICHTIG: Sollte das Account-Thing nicht Online gehen und einen Login-Fehler melden, öffne die URL YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in deinem Browser (Z.B.: http://openhab:8080/amazonechocontrol/account) und versuche dich anzumelden. thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.label = Status-Aktualisierungs-Intervall thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.description = Aktualtisierungs-Intervall für den Status in Sekunden. Kleinere Zeiten verursachen höheren Netzwerkverkehr. diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 57e301c6c99ef..9f08431a39e8f 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -13,10 +13,11 @@ + - Select the site where your amazon account is created. + Select or type in the site where your amazon account is created.
diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 780969cacaa94..2c88952bb8d01 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -228,6 +228,40 @@ To get instead of the id fields an selection box, use the Selection element and ``` Selection item=Echo_Living_Room_RadioStationId mappings=[ ''='Off', 's1139'='Antenne Steiermark', 's8007'='Hitradio Ă–3', 's16793'='Radio 10', 's8235'='FM4' ] ``` +## Tutorials + +**Playing an alarm sound for 15 seconds with an openHAB rule if an door contact was opened:** + +1) Open the PaperUI +2) Navigate to the Control Section +3) Open the Drop-Dow +4) Select the Sound you want to here +5) Write down the text in the square brackets. e.g. ECHO:system_alerts_repetitive01 for the nightstand sound +6) Create a rule for start playing the sound: + + +```php +var Timer stopAlarmTimer = null + +rule "Turn on alarm sound for 15 seconds if door opens" +when + Item Door_Contact changed to OPEN +then + Echo_Living_Room_PlayAlarmSound.sendCommand('ECHO:system_alerts_repetitive01') + if (stopAlarmTimer === null) + { + stopAlarmTimer = createTimer(now.plusSeconds(15)) [| + stopAlarmTimer.cancel() + stopAlarmTimer = null + Echo_Living_Room_PlayAlarmSound.sendCommand('') + ] + } +end +``` + +Note 1: Do not use a to short time for playing the sound, because alexa needs some time to start playing the sound. I recommend, that you to not use a time below 10 seconds. + +Note 2: The rule have no effect for your default alarm sound used in the alexa app. ## Credits From 024667354ffe3ba193a4715574565e5f15e7cb06 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 25 Apr 2018 20:53:41 +0200 Subject: [PATCH 35/56] [amazonechocontrol] Review comments (new lines and typos) Signed-off-by: Michael Geramb (github: mgeramb) --- .../ESH-INF/binding/binding.xml | 4 +++- .../i18n/amazonechocontrol_de.properties | 17 ++++++++--------- .../ESH-INF/thing/thing-types.xml | 5 ++--- .../OSGI-INF/.gitignore | 2 +- .../README.md | 4 ++-- .../handler/SmartHomeDimmerHandler.java | 6 ++---- .../internal/jsons/JsonFeed.java | 2 +- .../internal/jsons/JsonNetworkDetails.java | 1 - .../internal/jsons/JsonNotificationRequest.java | 1 - .../jsons/JsonNotificationResponse.java | 2 +- .../internal/jsons/JsonNotificationSound.java | 2 +- .../internal/jsons/JsonPlayerState.java | 1 - 12 files changed, 21 insertions(+), 26 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml index 28f384fcdf1f8..a26b2ed9c2254 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml @@ -1,6 +1,8 @@ + Amazon Echo Control Binding Binding to control Amazon Echo devices (Alexa). This binding enables openhab to control the volume, playing state, bluetooth connection of your amazon echo devices. Michael Geramb - \ No newline at end of file + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties index 868a4c0dd8d5a..d4042c9306016 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties @@ -32,11 +32,14 @@ thing-type.config.amazonechocontrol.echo.serialNumber.description = Die Seriennu thing-type.amazonechocontrol.echospot.label = Amazon Echo Spot thing-type.amazonechocontrol.echospot.description = Amazon Echo Spot Gerät +thing-type.config.amazonechocontrol.echospot.serialNumber.label = Seriennummer +thing-type.config.amazonechocontrol.echospot.serialNumber.description = Die Seriennummer findest du in der Alexa App. + thing-type.amazonechocontrol.echoshow.label = Amazon Echo Show thing-type.amazonechocontrol.echoshow.description = Amazon Echo Show Gerät -thing-type.config.amazonechocontrol.echospot.serialNumber.label = Seriennummer -thing-type.config.amazonechocontrol.echospot.serialNumber.description = Die Seriennummer findest du in der Alexa App. +thing-type.config.amazonechocontrol.echoshow.serialNumber.label = Seriennummer +thing-type.config.amazonechocontrol.echoshow.serialNumber.description = Die Seriennummer findest du in der Alexa App. thing-type.amazonechocontrol.wha.label = Amazon Multi-Raum Musik thing-type.amazonechocontrol.wha.description = Multi-Raum Musik Steuerung @@ -78,7 +81,7 @@ channel-type.amazonechocontrol.amazonMusicTrackId.label = Amazon Music Lied ID channel-type.amazonechocontrol.amazonMusicTrackId.description = ID des Liedes auf Amazon Music channel-type.amazonechocontrol.amazonMusic.label = Amazon Music -channel-type.amazonechocontrol.amazonMusic.description = Amazon Music eingeschalten +channel-type.amazonechocontrol.amazonMusic.description = Amazon Music eingeschaltet channel-type.amazonechocontrol.amazonMusicPlayListId.label = Amazon Music Playlist ID (Nur schreiben) channel-type.amazonechocontrol.amazonMusicPlayListId.description = ID der Playlist auf Amazon Music (Nur schreiben, kein aktueller Status). Auswahl funktioniert derzeit nur in PaperUI. @@ -96,7 +99,7 @@ channel-type.amazonechocontrol.bluetoothIdSelection.label = Bluetooth Verbindung channel-type.amazonechocontrol.bluetoothIdSelection.description = Bluetooth Verbindungungsauswahl (Derzeit nur in PaperUI) channel-type.amazonechocontrol.remind.label = Erinnere -channel-type.amazonechocontrol.remind.description = Spricht die Erinnerung und schickt eine Benachricht an die Alexa-APP (Nur schreiben) +channel-type.amazonechocontrol.remind.description = Spricht die Erinnerung und sendet eine Benachrichtigung an die Alexa-APP (Nur schreiben) channel-type.amazonechocontrol.playAlarmSound.label = Spielt Alarm Sound channel-type.amazonechocontrol.playAlarmSound.description = Spielt Alarm Sound ab (Nur schreiben) @@ -126,7 +129,7 @@ channel-type.amazonechocontrol.subtitle2.label = Untertitel 2 channel-type.amazonechocontrol.subtitle2.description = Untertitel 2 channel-type.amazonechocontrol.radio.label = TuneIn Radio -channel-type.amazonechocontrol.radio.description = Radio eingestalten +channel-type.amazonechocontrol.radio.description = Radio eingeschaltet channel-type.amazonechocontrol.bluetooth.label = Bluetooth Verbindung channel-type.amazonechocontrol.bluetooth.description = Verbindet zum letzten benutzten Bluetooth Gerätes @@ -157,7 +160,3 @@ channel-type.amazonechocontrol.switch.description = Schaltet das Ger channel-type.amazonechocontrol.dimmer.label = Dimmer channel-type.amazonechocontrol.dimmer.description = Dimmer Steuerung - - - - diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 9f08431a39e8f..292b81841d34c 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -270,7 +270,6 @@ Smart Home Dimmer - entityId @@ -365,7 +364,7 @@ String - Is of the playlist which was started with openHAB + Id of the playlist which was started with openHAB String @@ -437,4 +436,4 @@ Volume of the sound - \ No newline at end of file + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/OSGI-INF/.gitignore b/addons/binding/org.openhab.binding.amazonechocontrol/OSGI-INF/.gitignore index b81c7954b78b3..6722cd96e785a 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/OSGI-INF/.gitignore +++ b/addons/binding/org.openhab.binding.amazonechocontrol/OSGI-INF/.gitignore @@ -1 +1 @@ -*.xml \ No newline at end of file +*.xml diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 2c88952bb8d01..b3666f5552a53 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -2,7 +2,7 @@ This binding can control Amazon Echo devices (Alexa) from openhab. -It provide features to control and view the current state of echo dot devices: +It provide features to control and view the current state of echo devices: - volume - pause/continue/next track/previous track @@ -81,7 +81,7 @@ The Amazon Account thing need the following configurations: 2 factor authentication is not supported! -** HINT ** IMPORTANT: If the Account thing does not go online and reports and login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_ACCOUNT_THING (Replace YOUR_OPENHAB and ID_OF_ACCOUNT_THING with your configuration) in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. +** HINT ** IMPORTANT: If the Account thing does not go online and reports a login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_ACCOUNT_THING (Replace YOUR_OPENHAB and ID_OF_ACCOUNT_THING with your configuration) in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. ### Amazon devices diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java index 988a45be84266..cfd56eabcf35f 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java @@ -8,7 +8,7 @@ */ package org.openhab.binding.amazonechocontrol.handler; -import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; +import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.CHANNEL_DIMMER; import java.io.IOException; import java.net.URISyntaxException; @@ -47,14 +47,12 @@ public void initialize() { public void handleCommand(Connection connection, String entityId, String channelId, Command command) throws IOException, URISyntaxException { - if (channelId.equals(CHANNEL_SWITCH) || channelId.equals(CHANNEL_DIMMER)) { + if (channelId.equals(CHANNEL_DIMMER)) { if (command == OnOffType.ON) { connection.sendSmartHomeDeviceCommand(entityId, "turnOn", null, null); - updateState(CHANNEL_SWITCH, OnOffType.ON); } if (command == OnOffType.OFF) { connection.sendSmartHomeDeviceCommand(entityId, "turnOff", null, null); - updateState(CHANNEL_SWITCH, OnOffType.OFF); } } if (channelId.equals(CHANNEL_DIMMER)) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java index 4b75cd40c739d..73542f06f25c1 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonFeed.java @@ -22,4 +22,4 @@ public class JsonFeed { public @Nullable String name; public @Nullable String skillId; public @Nullable String imageUrl; -} \ No newline at end of file +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java index 1d68a036c13df..96f14435edfc2 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java @@ -19,5 +19,4 @@ @NonNullByDefault public class JsonNetworkDetails { public @Nullable String networkDetail; - } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java index 32223cd1f46a1..67a5d69cee9a3 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationRequest.java @@ -34,5 +34,4 @@ public class JsonNotificationRequest { public @Nullable String id = "createReminder"; public boolean isRecurring = false; public long createdDate; - } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java index 51b44f8206114..8bd8067716faa 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationResponse.java @@ -62,4 +62,4 @@ public class JsonNotificationResponse { *    "isSaveInFlight":true * } * - */ \ No newline at end of file + */ diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java index fca8478bb2312..3bfc30af716f4 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNotificationSound.java @@ -23,4 +23,4 @@ public class JsonNotificationSound { public @Nullable String id = "system_alerts_melodic_01"; public @Nullable String providerId = "ECHO"; public @Nullable String sampleUrl; -} \ No newline at end of file +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java index ab1f3b476879e..d8b1fe5475cc8 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayerState.java @@ -58,5 +58,4 @@ public class MainArt { } } - } From ae0346e1e07eba4f5a7f425b87693e898f7eddc7 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 29 Apr 2018 09:29:37 +0200 Subject: [PATCH 36/56] [amazonechocontrol] Feature for PlayMusicVoiceCommand added Signed-off-by: Michael Geramb (github: mgeramb) --- .../i18n/amazonechocontrol_de.properties | 8 +- .../ESH-INF/thing/thing-types.xml | 20 ++- .../README.md | 5 + .../AmazonEchoControlBindingConstants.java | 7 +- .../handler/AccountHandler.java | 2 +- .../handler/EchoHandler.java | 25 +++ .../internal/Connection.java | 145 ++++++++++++++---- .../internal/LoginServlet.java | 9 +- .../internal/jsons/JsonMusicProvider.java | 30 ++++ .../JsonPlaySearchPhraseOperationPayload.java | 28 ++++ .../jsons/JsonPlayValidationResult.java | 22 +++ .../jsons/JsonStartRoutineRequest.java | 2 +- ...onEchoDynamicStateDescriptionProvider.java | 31 ++++ 13 files changed, 292 insertions(+), 42 deletions(-) mode change 100755 => 100644 addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMusicProvider.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaySearchPhraseOperationPayload.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayValidationResult.java diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties index d4042c9306016..f4051468410dd 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties @@ -83,7 +83,7 @@ channel-type.amazonechocontrol.amazonMusicTrackId.description = ID des Liedes au channel-type.amazonechocontrol.amazonMusic.label = Amazon Music channel-type.amazonechocontrol.amazonMusic.description = Amazon Music eingeschaltet -channel-type.amazonechocontrol.amazonMusicPlayListId.label = Amazon Music Playlist ID (Nur schreiben) +channel-type.amazonechocontrol.amazonMusicPlayListId.label = Amazon Music Playlist ID channel-type.amazonechocontrol.amazonMusicPlayListId.description = ID der Playlist auf Amazon Music (Nur schreiben, kein aktueller Status). Auswahl funktioniert derzeit nur in PaperUI. channel-type.amazonechocontrol.amazonMusicPlayListIdLastUsed.label = Amazon Music letzte gestartete Playlist ID @@ -116,6 +116,12 @@ channel-type.amazonechocontrol.playTrafficNews.description = Started die Verkehr channel-type.amazonechocontrol.startRoutine.label = Started eine Routine channel-type.amazonechocontrol.startRoutine.description = Tippen sie ein, was Sie normalerweise zu Alexa sagen um eine Routine zu starten, ohne "Alexa" vorangestellt (Nur schreiben) +channel-type.amazonechocontrol.playMusicProvider.label = Musikanbieter für Starte Musik Sprachbefehl +channel-type.amazonechocontrol.playMusicProvider.description = Musikanbieter der für 'Starte Musik Sprachbefehl' verwendet wird (Nur schreiben) + +channel-type.amazonechocontrol.playMusicVoiceCommand.label = Starte Musik Sprachbefehl +channel-type.amazonechocontrol.playMusicVoiceCommand.description = Sprachbefehl als Text. Z.B. Yesterday von Beatles (Nur schreiben) + channel-type.amazonechocontrol.imageUrl.label = Bild URL channel-type.amazonechocontrol.imageUrl.description = URL des Album Covers oder des Radiostations Logos diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml old mode 100755 new mode 100644 index 292b81841d34c..5a19987d15cc2 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -76,6 +76,8 @@ + + serialNumber @@ -116,6 +118,8 @@ + + serialNumber @@ -156,6 +160,8 @@ + + serialNumber @@ -225,6 +231,8 @@ + + serialNumber @@ -358,7 +366,7 @@ String - + Amazon Music play list id (Write only, no current state) @@ -436,4 +444,14 @@ Volume of the sound + + String + + Music provider used for 'Start music voice command' (Write only) + + + String + + Voice command as text. E.g. 'Yesterday from the Beatles' (Write only) + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index b3666f5552a53..48bf01cfbfbb1 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -18,6 +18,7 @@ It provide features to control and view the current state of echo devices: - start weather report - start automation routine - activate multiple configurations of flash briefings +- start playing music by providing the voice command as text (Works with all music providers) Some ideas what you can do in your home by using rules and other openHAB controlled devices: @@ -125,6 +126,8 @@ The flashbriefingprofile thing has no configuration parameters. It will be confi | playWeatherReport | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the weather report | playTrafficNews | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the traffic news | startRoutine | Switch | W | echo, echoshow, echospot, unknown | Write Only! Type in what you normally say to Alexa without the preceding "Alexa," +| playMusicProvider | String | W | echo, echoshow, echospot, unknown | Write Only! Music provider used for 'Start music voice command' +| playMusicVoiceCommand | String | W | echo, echoshow, echospot, unknown | Write Only! Voice command as text. E.g. 'Yesterday from the Beatles' | save | Switch | W | flashbriefingprofile | Write Only! 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. @@ -178,6 +181,8 @@ Switch Echo_Living_Room_PlayFlashBriefing "Play Flash Briefing" Switch Echo_Living_Room_PlayWeatherReport "Play Weather Report" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playWeatherReport"} Switch Echo_Living_Room_PlayTrafficNews "Play Traffic News" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playTrafficNews"} String Echo_Living_Room_StartRoutine "Start Routine" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:startRoutine"} +String Echo_Living_Room_PlayMusicProvider "Music Provider (Write Only)" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playMusicProvider"} +String Echo_Living_Room_PlayMusicCommand "Play music voice command (Write Only)" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playMusicVoiceCommand"} Switch FlashBriefing_Technical_Save "Save (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:save"} Switch FlashBriefing_Technical_Active "Active" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing1:active"} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java index 02e39baf52900..b1eb2f00da900 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java @@ -73,6 +73,8 @@ public class AmazonEchoControlBindingConstants { public static final String CHANNEL_PLAY_WEATER_REPORT = "playWeatherReport"; public static final String CHANNEL_PLAY_TRAFFIC_NEWS = "playTrafficNews"; public static final String CHANNEL_START_ROUTINE = "startRoutine"; + public static final String CHANNEL_PLAY_MUSIC_PROVIDER = "playMusicProvider"; + public static final String CHANNEL_PLAY_MUSIC_VOICE_COMMAND = "playMusicVoiceCommand"; public static final String CHANNEL_SAVE = "save"; public static final String CHANNEL_ACTIVE = "active"; @@ -87,16 +89,15 @@ public class AmazonEchoControlBindingConstants { 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_PLAY_MUSIC_PROVIDER = new ChannelTypeUID(BINDING_ID, + "playMusicProvider"); // 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_ENTITY_ID = "entityId"; - public static final String DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE = "configurationJson"; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index 1f0d498d29881..e2e186f157dab 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -85,7 +85,7 @@ public AccountHandler(Bridge bridge, HttpService httpService) { @Override public void initialize() { - logger.debug("amazon account bridge starting handler ..."); + logger.debug("amazon account bridge starting..."); AccountConfiguration config = getConfigAs(AccountConfiguration.class); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index a86b62e2eb456..c6bb1a2e6237d 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -71,12 +71,14 @@ public class EchoHandler extends BaseThingHandler { private @Nullable String lastKnownRadioStationId; private @Nullable String lastKnownBluetoothId; private @Nullable String lastKnownAmazonMusicId; + private String musicProviderId = ""; private int lastKnownVolume = 25; private @Nullable BluetoothState bluetoothState; private boolean disableUpdate = false; private boolean updateRemind = true; private boolean updateAlarm = true; private boolean updateRoutine = true; + private boolean updatePlayMusicVoiceCommand = true; private @Nullable JsonNotificationResponse currentNotification; private @Nullable ScheduledFuture currentNotifcationUpdateTimer; @@ -229,6 +231,24 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } + // play music command + if (channelId.equals(CHANNEL_PLAY_MUSIC_PROVIDER)) { + if (command instanceof StringType) { + this.musicProviderId = ((StringType) command).toFullString(); + waitForUpdate = 0; + } + } + if (channelId.equals(CHANNEL_PLAY_MUSIC_VOICE_COMMAND)) { + if (command instanceof StringType) { + String voiceCommand = ((StringType) command).toFullString(); + if (!this.musicProviderId.isEmpty()) { + connection.playMusicVoiceCommand(device, this.musicProviderId, voiceCommand); + waitForUpdate = 2000; + updatePlayMusicVoiceCommand = true; + } + } + } + // bluetooth commands if (channelId.equals(CHANNEL_BLUETOOTH_ID) || channelId.equals(CHANNEL_BLUETOOTH_ID_SELECTION)) { needBluetoothRefresh = true; @@ -681,6 +701,11 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto updateRoutine = false; updateState(CHANNEL_START_ROUTINE, new StringType("")); } + if (updatePlayMusicVoiceCommand) { + updatePlayMusicVoiceCommand = false; + updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, new StringType("")); + } + updateState(CHANNEL_PLAY_MUSIC_PROVIDER, new StringType(musicProviderId)); updateState(CHANNEL_PLAY_FLASH_BRIEFING, OnOffType.OFF); updateState(CHANNEL_PLAY_WEATER_REPORT, OnOffType.OFF); updateState(CHANNEL_PLAY_TRAFFIC_NEWS, OnOffType.OFF); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 528a6bbf7c743..5e2df3674c607 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Scanner; @@ -45,11 +46,14 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonEnabledFeeds; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNetworkDetails; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationRequest; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSounds; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaySearchPhraseOperationPayload; +import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayValidationResult; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevice; @@ -60,6 +64,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; /** @@ -267,17 +272,17 @@ public String convertStream(@Nullable InputStream input) throws IOException { } public String makeRequestAndReturnString(String url) throws IOException, URISyntaxException { - return makeRequestAndReturnString("GET", url, null, null, false); + return makeRequestAndReturnString("GET", url, null, false, null); } - private String makeRequestAndReturnString(String verb, String url, @Nullable String referer, - @Nullable String postData, boolean json) throws IOException, URISyntaxException { - HttpsURLConnection connection = makeRequest(verb, url, referer, postData, json, true); + private String makeRequestAndReturnString(String verb, String url, @Nullable String postData, boolean json, + @Nullable Map customHeaders) throws IOException, URISyntaxException { + HttpsURLConnection connection = makeRequest(verb, url, postData, json, true, customHeaders); return convertStream(connection.getInputStream()); } - public HttpsURLConnection makeRequest(String verb, String url, @Nullable String referer, @Nullable String postData, - boolean json, boolean autoredirect) throws IOException, URISyntaxException { + public HttpsURLConnection makeRequest(String verb, String url, @Nullable String postData, boolean json, + boolean autoredirect, @Nullable Map customHeaders) throws IOException, URISyntaxException { String currentUrl = url; for (int i = 0; i < 30; i++) // loop for handling redirect, using automatic redirect is not possible, because // all response headers must be catched @@ -292,10 +297,14 @@ public HttpsURLConnection makeRequest(String verb, String url, @Nullable String connection.setRequestProperty("User-Agent", "Mozilla/5.0"); connection.setRequestProperty("DNT", "1"); connection.setRequestProperty("Upgrade-Insecure-Requests", "1"); - connection.setInstanceFollowRedirects(false); - if (referer != null) { - connection.setRequestProperty("Referer", referer); + if (customHeaders != null) { + for (String key : customHeaders.keySet()) { + String value = customHeaders.get(key); + connection.setRequestProperty(key, value); + } } + connection.setInstanceFollowRedirects(false); + // add cookies URI uri = connection.getURL().toURI(); @@ -483,10 +492,12 @@ public void makeLogin() throws IOException, URISyntaxException { // build referer link String referer = "https://www." + amazonSite + "/ap/signin?" + queryParameters; + Map headers = new HashMap(); + headers.put("Referer", referer); // make the request - URLConnection request = makeRequest("POST", "https://www." + amazonSite + "/ap/signin", referer, postData, - false, true); + URLConnection request = makeRequest("POST", "https://www." + amazonSite + "/ap/signin", postData, false, true, + headers); String response = convertStream(request.getInputStream()); logger.debug("Received content after login {}", response); @@ -594,7 +605,7 @@ public JsonPlaylists getPlaylists(Device device) throws IOException, URISyntaxEx public void command(Device device, String command) throws IOException, URISyntaxException { String url = alexaServer + "/api/np/command?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType; - makeRequest("POST", url, null, command, true, true); + makeRequest("POST", url, command, true, true, null); } @@ -602,12 +613,12 @@ public void bluetooth(Device device, @Nullable String address) throws IOExceptio if (StringUtils.isEmpty(address)) { // disconnect makeRequest("POST", - alexaServer + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" + device.serialNumber, - null, "", true, true); + alexaServer + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" + device.serialNumber, "", + true, true, null); } else { makeRequest("POST", - alexaServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber, null, - "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true, true); + alexaServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber, + "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true, true, null); } } @@ -619,7 +630,7 @@ public void playRadio(Device device, @Nullable String stationId) throws IOExcept alexaServer + "/api/tunein/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&guideId=" + stationId + "&contentType=station&callSign=&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId, - null, "", true, true); + "", true, true, null); } } @@ -632,7 +643,7 @@ public void playAmazonMusicTrack(Device device, @Nullable String trackId) throws alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId + "&shuffle=false", - null, command, true, true); + command, true, true, null); } } @@ -646,7 +657,7 @@ public void playAmazonMusicPlayList(Device device, @Nullable String playListId) alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + device.deviceOwnerCustomerId + "&shuffle=false", - null, command, true, true); + command, true, true, null); } } @@ -658,7 +669,7 @@ public void executeSequenceCommand(Device device, String command) throws IOExcep + "\\\",\\\"deviceSerialNumber\\\":\\\"" + device.serialNumber + "\\\",\\\"customerId\\\":\\\"" + device.deviceOwnerCustomerId + "\\\",\\\"locale\\\":\\\"\\\"}}}\",\n" + " \"status\": \"ENABLED\" }"; - makeRequest("POST", alexaServer + "/api/behaviors/preview", null, json, true, true); + makeRequest("POST", alexaServer + "/api/behaviors/preview", json, true, true, null); } public void startRoutine(Device device, String utterance) throws IOException, URISyntaxException { @@ -720,21 +731,20 @@ public void startRoutine(Device device, String utterance) throws IOException, UR request.sequenceJson = sequenceJson; String requestJson = gson.toJson(request); - makeRequest("POST", alexaServer + "/api/behaviors/preview", null, requestJson, true, true); + makeRequest("POST", alexaServer + "/api/behaviors/preview", requestJson, true, true, null); } else { logger.warn("Routine {} not found", utterance); } } public JsonAutomation[] getRoutines() throws IOException, URISyntaxException { - String json = makeRequestAndReturnString("GET", alexaServer + "/api/behaviors/automations", null, null, true); + String json = makeRequestAndReturnString(alexaServer + "/api/behaviors/automations"); JsonAutomation[] result = parseJson(json, JsonAutomation[].class); return result; } public JsonFeed[] getEnabledFlashBriefings() throws IOException, URISyntaxException { - String json = makeRequestAndReturnString("GET", alexaServer + "/api/content-skills/enabled-feeds", null, null, - true); + String json = makeRequestAndReturnString(alexaServer + "/api/content-skills/enabled-feeds"); JsonEnabledFeeds result = parseJson(json, JsonEnabledFeeds.class); JsonFeed[] enabledFeeds = result.enabledFeeds; if (enabledFeeds != null) { @@ -750,14 +760,13 @@ public void setEnabledFlashBriefings(JsonFeed[] enabledFlashBriefing) throws IOE gsonBuilder.serializeNulls(); Gson gson = gsonBuilder.create(); String json = gson.toJson(enabled); - makeRequest("POST", alexaServer + "/api/content-skills/enabled-feeds", null, json, true, true); + makeRequest("POST", alexaServer + "/api/content-skills/enabled-feeds", json, true, true, null); } public JsonNotificationSound[] getNotificationSounds(Device device) throws IOException, URISyntaxException { String json = makeRequestAndReturnString( - "GET", alexaServer + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber - + "&deviceType=" + device.deviceType + "&softwareVersion=" + device.softwareVersion, - null, null, true); + alexaServer + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + + device.deviceType + "&softwareVersion=" + device.softwareVersion); JsonNotificationSounds result = parseJson(json, JsonNotificationSounds.class); JsonNotificationSound[] notificationSounds = result.notificationSounds; if (notificationSounds != null) { @@ -792,27 +801,95 @@ public JsonNotificationResponse notification(Device device, String type, @Nullab Gson gson = gsonBuilder.create(); String data = gson.toJson(request); - String response = makeRequestAndReturnString("PUT", alexaServer + "/api/notifications/createReminder", null, - data, true); + String response = makeRequestAndReturnString("PUT", alexaServer + "/api/notifications/createReminder", data, + true, null); JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); return result; } public void stopNotification(JsonNotificationResponse notification) throws IOException, URISyntaxException { - makeRequestAndReturnString("DELETE", alexaServer + "/api/notifications/" + notification.id, null, null, true); + makeRequestAndReturnString("DELETE", alexaServer + "/api/notifications/" + notification.id, null, true, null); } public JsonNotificationResponse getNotificationState(JsonNotificationResponse notification) throws IOException, URISyntaxException { String response = makeRequestAndReturnString("GET", alexaServer + "/api/notifications/" + notification.id, null, - null, true); + true, null); JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); return result; } + public List getMusicProviders() { + String response; + try { + Map headers = new HashMap(); + headers.put("Routines-Version", "1.1.201102"); + response = makeRequestAndReturnString("GET", + alexaServer + "/api/behaviors/entities?skillId=amzn1.ask.1p.music", null, true, headers); + } catch (IOException | URISyntaxException e) { + logger.warn("getMusicProviders fails: {}", e.getMessage()); + return new ArrayList<>(); + } + if (StringUtils.isEmpty(response)) { + return new ArrayList<>(); + } + JsonMusicProvider[] result = parseJson(response, JsonMusicProvider[].class); + return Arrays.asList(result); + } + + public void playMusicVoiceCommand(Device device, String providerId, String voiceCommand) + throws IOException, URISyntaxException { + JsonPlaySearchPhraseOperationPayload payload = new JsonPlaySearchPhraseOperationPayload(); + payload.customerId = device.deviceOwnerCustomerId; + payload.locale = "ALEXA_CURRENT_LOCALE"; + payload.musicProviderId = providerId; + payload.searchPhrase = voiceCommand; + + Gson gson = new Gson(); + String playloadString = gson.toJson(payload); + + JsonObject postValidataionJson = new JsonObject(); + + postValidataionJson.addProperty("type", "Alexa.Music.PlaySearchPhrase"); + postValidataionJson.addProperty("operationPayload", playloadString); + + String postDataValidate = postValidataionJson.toString(); + + String validateResultJson = makeRequestAndReturnString("POST", + alexaServer + "/api/behaviors/operation/validate", postDataValidate, true, null); + + if (StringUtils.isNotEmpty(validateResultJson)) { + JsonPlayValidationResult validationResult = parseJson(validateResultJson, JsonPlayValidationResult.class); + JsonPlaySearchPhraseOperationPayload validatedOperationPayload = validationResult.operationPayload; + if (validatedOperationPayload != null) { + payload.sanitizedSearchPhrase = validatedOperationPayload.sanitizedSearchPhrase; + payload.searchPhrase = validatedOperationPayload.searchPhrase; + } + } + + payload.locale = null; + payload.deviceSerialNumber = device.serialNumber; + payload.deviceType = device.deviceType; + + JsonObject sequenceJson = new JsonObject(); + sequenceJson.addProperty("@type", "com.amazon.alexa.behaviors.model.Sequence"); + JsonObject startNodeJson = new JsonObject(); + startNodeJson.addProperty("@type", "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"); + startNodeJson.addProperty("type", "Alexa.Music.PlaySearchPhrase"); + startNodeJson.add("operationPayload", gson.toJsonTree(payload)); + sequenceJson.add("startNode", startNodeJson); + + JsonStartRoutineRequest startRoutineRequest = new JsonStartRoutineRequest(); + startRoutineRequest.sequenceJson = sequenceJson.toString(); + startRoutineRequest.status = null; + + String postData = gson.toJson(startRoutineRequest); + makeRequest("POST", alexaServer + "/api/behaviors/preview", postData, true, true, null); + } + public List getSmartHomeDevices() throws IOException, URISyntaxException { try { - String json = makeRequestAndReturnString("GET", alexaServer + "/api/phoenix", null, null, true); + String json = makeRequestAndReturnString(alexaServer + "/api/phoenix"); logger.debug("getSmartHomeDevices result: {}", json); JsonNetworkDetails networkDetails = parseJson(json, JsonNetworkDetails.class); @@ -853,7 +930,7 @@ public void sendSmartHomeDeviceCommand(String entityId, String action, @Nullable + (parameterName != null ? ", \"" + parameterName + "\": \"" + parameter + "\"" : "") + " }" + "}]" + "}"; - String json = makeRequestAndReturnString("PUT", alexaServer + "/api/phoenix/state", null, command, true); + String json = makeRequestAndReturnString("PUT", alexaServer + "/api/phoenix/state", command, true, null); json.toString(); } } \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java index ef571dc2c8160..1f573bc2b9fbb 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.net.URISyntaxException; import java.net.URLEncoder; +import java.util.HashMap; import java.util.Map; import javax.net.ssl.HttpsURLConnection; @@ -189,7 +190,13 @@ void HandleProxyRequest(HttpServletResponse resp, String verb, String url, @Null @Nullable String postData) throws IOException { HttpsURLConnection urlConnection; try { - urlConnection = connection.makeRequest(verb, url, referer, postData, false, false); + Map headers = null; + if (referer != null) { + headers = new HashMap(); + headers.put("Referer", referer); + } + + urlConnection = connection.makeRequest(verb, url, postData, false, false, headers); if (urlConnection.getResponseCode() == 302) { { String location = urlConnection.getHeaderField("location"); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMusicProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMusicProvider.java new file mode 100644 index 0000000000000..b5e1cbd898604 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonMusicProvider.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link JsonMusicProvider} encapsulate the GSON returned for a music provider + * + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class JsonMusicProvider { + public @Nullable String displayName; + public @Nullable List<@Nullable Object> @Nullable [] supportedTriggers; + public @Nullable String icon; + public @Nullable List<@Nullable String> supportedProperties; + public @Nullable String id; + public @Nullable String availability; + public @Nullable String description; +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaySearchPhraseOperationPayload.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaySearchPhraseOperationPayload.java new file mode 100644 index 0000000000000..3b0d2a300f146 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlaySearchPhraseOperationPayload.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link JsonPlaySearchPhraseOperationPayload} encapsulate the GSON for validation requests and results + * + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class JsonPlaySearchPhraseOperationPayload { + public @Nullable String deviceType = "ALEXA_CURRENT_DEVICE_TYPE"; + public @Nullable String deviceSerialNumber = "ALEXA_CURRENT_DSN"; + public @Nullable String locale = "ALEXA_CURRENT_LOCALE"; + public @Nullable String customerId; + public @Nullable String searchPhrase; + public @Nullable String sanitizedSearchPhrase; + public @Nullable String musicProviderId = "ALEXA_CURRENT_DSN"; +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayValidationResult.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayValidationResult.java new file mode 100644 index 0000000000000..bf95a0e7a5348 --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonPlayValidationResult.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal.jsons; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link JsonPlayValidationResult} encapsulate the GSON for validation result + * + * @author Michael Geramb - Initial contribution + */ +@NonNullByDefault +public class JsonPlayValidationResult { + public @Nullable JsonPlaySearchPhraseOperationPayload operationPayload; +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java index 47349e495d4c4..448a33e9f9d0a 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonStartRoutineRequest.java @@ -18,7 +18,7 @@ */ @NonNullByDefault public class JsonStartRoutineRequest { - public @Nullable String behaviorId; + public @Nullable String behaviorId = "PREVIEW"; public @Nullable String sequenceJson; public @Nullable String status = "ENABLED"; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java index 68c1b7b866433..16bf109e673a7 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -16,6 +16,7 @@ import java.util.Locale; import java.util.Map; +import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -30,6 +31,7 @@ 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; @@ -193,7 +195,36 @@ public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDe originalStateDescription.getMaximum(), originalStateDescription.getStep(), originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); return result; + } else if (CHANNEL_TYPE_PLAY_MUSIC_PROVIDER.equals(channel.getChannelTypeUID())) { + EchoHandler handler = EchoHandler.find(channel.getUID().getThingUID()); + if (handler == null) { + return originalStateDescription; + } + Connection connection = handler.findConnection(); + if (connection == null) { + return originalStateDescription; + } + List musicProviders = connection.getMusicProviders(); + + ArrayList options = new ArrayList(); + for (JsonMusicProvider musicProvider : musicProviders) { + @Nullable + List<@Nullable String> properties = musicProvider.supportedProperties; + String providerId = musicProvider.id; + String displayName = musicProvider.displayName; + if (properties != null && properties.contains("Alexa.Music.PlaySearchPhrase") + && StringUtils.isNotEmpty(providerId) + && StringUtils.equals(musicProvider.availability, "AVAILABLE") + && StringUtils.isNotEmpty(displayName)) { + options.add(new StateOption(providerId, String.format("%s [%s]", displayName, providerId))); + } + } + StateDescription result = new StateDescription(originalStateDescription.getMinimum(), + originalStateDescription.getMaximum(), originalStateDescription.getStep(), + originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); + return result; } return originalStateDescription; } + } From 05978c1481f40f45b6ea5da05e4362f1901b2635 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 29 Apr 2018 12:48:54 +0200 Subject: [PATCH 37/56] [amazonechocontrol] Smarthome feature remove, because it does not work as expected for the most smarthome skills Signed-off-by: Michael Geramb (github: mgeramb) --- .../ESH-INF/thing/thing-types.xml | 41 ------ .../AmazonEchoControlBindingConstants.java | 10 +- .../handler/AccountHandler.java | 59 +-------- .../handler/SmartHomeBaseHandler.java | 123 ------------------ .../handler/SmartHomeDimmerHandler.java | 70 ---------- .../handler/SmartHomeSwitchHandler.java | 48 ------- .../internal/AccountConfiguration.java | 6 - .../AmazonEchoControlHandlerFactory.java | 8 -- .../discovery/AmazonEchoDiscovery.java | 50 ------- 9 files changed, 3 insertions(+), 412 deletions(-) delete mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java delete mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java delete mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 5a19987d15cc2..4198453dd42b8 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -36,13 +36,6 @@ Refresh state interval in seconds. Lower time causes more network traffic. Seconds - @@ -254,40 +247,6 @@ - - - - - - Smart Home Switch - - - - entityId - - - - Use the search feature of openHAB to get the id - - - - - - - - - Smart Home Dimmer - - - - entityId - - - - Let discover the device to get the id - - - Switch diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java index b1eb2f00da900..8a6ca60d2fc75 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java @@ -38,13 +38,9 @@ public class AmazonEchoControlBindingConstants { public static final ThingTypeUID THING_TYPE_FLASH_BRIEFING_PROFILE = new ThingTypeUID(BINDING_ID, "flashbriefingprofile"); - public static final ThingTypeUID THING_TYPE_SMART_HOME_SWITCH = new ThingTypeUID(BINDING_ID, "smarthomeswitch"); - public static final ThingTypeUID THING_TYPE_SMART_HOME_DIMMER = new ThingTypeUID(BINDING_ID, "smarthomedimmer"); - public static final Set SUPPORTED_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_UNKNOWN, THING_TYPE_SMART_HOME_SWITCH, THING_TYPE_SMART_HOME_DIMMER, - THING_TYPE_FLASH_BRIEFING_PROFILE)); + THING_TYPE_ECHO_WHA, THING_TYPE_UNKNOWN, THING_TYPE_FLASH_BRIEFING_PROFILE)); // List of all Channel ids public static final String CHANNEL_PLAYER = "player"; @@ -80,9 +76,6 @@ public class AmazonEchoControlBindingConstants { public static final String CHANNEL_ACTIVE = "active"; public static final String CHANNEL_PLAY_ON_DEVICE = "playOnDevice"; - public static final String CHANNEL_SWITCH = "switch"; - public static final String CHANNEL_DIMMER = "dimmer"; - // List of channel Type UIDs public static final ChannelTypeUID CHANNEL_TYPE_BLUETHOOTH_ID_SELECTION = new ChannelTypeUID(BINDING_ID, "bluetoothIdSelection"); @@ -97,7 +90,6 @@ public class AmazonEchoControlBindingConstants { // 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_ENTITY_ID = "entityId"; public static final String DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE = "configurationJson"; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index e2e186f157dab..e7e43bf8417d9 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -44,7 +44,6 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevice; import org.osgi.service.http.HttpService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,15 +62,12 @@ public class AccountHandler extends BaseBridgeHandler implements IAmazonEchoDisc private StateStorage stateStorage; private @Nullable Connection connection; private final Set echoHandlers = new HashSet<>(); - private final Set smartHomeHandlers = new HashSet<>(); private final Set flashBriefingProfileHandlers = new HashSet<>(); private final Object synchronizeConnection = new Object(); private Map jsonSerialNumberDeviceMapping = new HashMap<>(); private @Nullable ScheduledFuture refreshJob; private @Nullable ScheduledFuture refreshLogin; - private boolean updateSmartHomeDeviceList; private boolean discoverFlashProfiles; - private boolean smartHomeDeviceListEnabled; private String currentFlashBriefingJson = ""; private final HttpService httpService; private @Nullable LoginServlet loginServlet; @@ -109,16 +105,6 @@ public void initialize() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Polling interval not configured"); return; } - Boolean discoverSmartHomeDevices = config.discoverSmartHomeDevices; - if (discoverSmartHomeDevices != null && discoverSmartHomeDevices) { - if (!smartHomeDeviceListEnabled) { - updateSmartHomeDeviceList = true; - } - smartHomeDeviceListEnabled = true; - } else { - smartHomeDeviceListEnabled = false; - } - if (pollingIntervalInSeconds < 10) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Polling interval less than 10 seconds not allowed"); @@ -178,16 +164,6 @@ public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBrie } } - public void addSmartHomeHandler(SmartHomeBaseHandler smartHomeHandler) { - synchronized (smartHomeHandlers) { - smartHomeHandlers.add(smartHomeHandler); - } - Connection connection = this.connection; - if (connection != null) { - smartHomeHandler.initialize(connection); - } - } - private void initializeEchoHandler(EchoHandler echoHandler, Connection connection) { intializeChildDevice(connection, echoHandler); @@ -239,16 +215,6 @@ public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) { } } - // check for smart home handler - if (childHandler instanceof SmartHomeBaseHandler) { - synchronized (smartHomeHandlers) { - smartHomeHandlers.remove(childHandler); - } - AmazonEchoDiscovery instance = AmazonEchoDiscovery.instance; - if (instance != null) { - instance.removeExistingSmartHomeHandler(childThing.getUID()); - } - } super.childHandlerDisposed(childHandler, childThing); } @@ -351,7 +317,6 @@ private void checkLogin() { String serializedStorage = currentConnection.serializeLoginData(); this.stateStorage.storeState("sessionStorage", serializedStorage); } - updateSmartHomeDeviceList = true; this.connection = currentConnection; } catch (UnknownHostException e) { loginIsValid = false; @@ -387,7 +352,6 @@ public void setConnection(Connection connection) { this.connection = connection; String serializedStorage = connection.serializeLoginData(); this.stateStorage.storeState("sessionStorage", serializedStorage); - updateSmartHomeDeviceList = true; handleValidLogin(); } @@ -471,7 +435,6 @@ private void refreshData() { @Override public void updateDeviceList(boolean manualScan) { if (manualScan) { - updateSmartHomeDeviceList = true; discoverFlashProfiles = true; } @@ -508,26 +471,8 @@ public void updateDeviceList(boolean manualScan) { initializeEchoHandler(child, currentConnection); } } - synchronized (smartHomeHandlers) { - for (SmartHomeBaseHandler child : smartHomeHandlers) { - child.initialize(currentConnection); - } - } - updateFlashBriefingHandlers(currentConnection); - - if (discoveryService != null && updateSmartHomeDeviceList && smartHomeDeviceListEnabled) { - updateSmartHomeDeviceList = false; - List smartHomeDevices = null; - try { - smartHomeDevices = currentConnection.getSmartHomeDevices(); - } catch (IOException | URISyntaxException e) { - logger.warn("Update smart home list failed {}", e); - } - if (smartHomeDevices != null) { - discoveryService.setSmartHomeDevices(getThing().getUID(), smartHomeDevices); - } - } + updateFlashBriefingHandlers(currentConnection); } public void setEnabledFlashBriefingsJson(String flashBriefingJson) { @@ -552,7 +497,7 @@ public void updateFlashBriefingHandlers() { } private void updateFlashBriefingHandlers(Connection currentConnection) { - synchronized (smartHomeHandlers) { + synchronized (flashBriefingProfileHandlers) { if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) { updateFlashBriefingProfiles(currentConnection); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java deleted file mode 100644 index b284b7cddd7a0..0000000000000 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeBaseHandler.java +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright (c) 2010-2018 by the respective copyright holders. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.amazonechocontrol.handler; - -import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.DEVICE_PROPERTY_ENTITY_ID; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.HashMap; - -import org.apache.commons.lang.StringUtils; -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.smarthome.core.thing.Bridge; -import org.eclipse.smarthome.core.thing.ChannelUID; -import org.eclipse.smarthome.core.thing.Thing; -import org.eclipse.smarthome.core.thing.ThingStatus; -import org.eclipse.smarthome.core.thing.ThingUID; -import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; -import org.eclipse.smarthome.core.types.Command; -import org.openhab.binding.amazonechocontrol.internal.Connection; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link SmartHomeBaseHandler} is the base class for all smart home devices provided by alexa skills - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public abstract class SmartHomeBaseHandler extends BaseThingHandler { - - private final static HashMap instances = new HashMap(); - - private final Logger logger = LoggerFactory.getLogger(SmartHomeBaseHandler.class); - private @Nullable Connection connection; - - protected @Nullable Connection findConnection() { - return this.connection; - } - - protected SmartHomeBaseHandler(Thing thing) { - super(thing); - - } - - @Override - public void initialize() { - logger.info("{} initialized", getClass().getSimpleName()); - synchronized (instances) { - instances.put(this.getThing().getUID(), this); - } - if (this.connection != null) { - updateStatus(ThingStatus.ONLINE); - } else { - updateStatus(ThingStatus.UNKNOWN); - Bridge bridge = this.getBridge(); - if (bridge != null) { - AccountHandler account = (AccountHandler) bridge.getHandler(); - if (account != null) { - account.addSmartHomeHandler(this); - } - } - } - } - - public void initialize(Connection connection) { - this.connection = connection; - updateStatus(ThingStatus.ONLINE); - } - - @Override - public void dispose() { - synchronized (instances) { - instances.remove(this.getThing().getUID()); - } - super.dispose(); - } - - private String findEntityId() { - String id = (String) getConfig().get(DEVICE_PROPERTY_ENTITY_ID); - if (id == null) { - return ""; - } - return id; - } - - public static @Nullable SmartHomeBaseHandler find(ThingUID uid) { - synchronized (instances) { - return instances.get(uid); - } - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Connection connection = findConnection(); - if (connection == null) { - return; - } - String entityId = findEntityId(); - if (entityId.isEmpty()) { - return; - } - String channelId = channelUID.getId(); - if (StringUtils.isEmpty(channelId)) { - return; - } - try { - handleCommand(connection, entityId, channelId, command); - } catch (IOException | URISyntaxException e) { - logger.warn("handle command {} for {} failed", command, channelUID, e); - } - } - - protected abstract void handleCommand(Connection connection, String entityId, String channelId, Command command) - throws IOException, URISyntaxException; -} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java deleted file mode 100644 index cfd56eabcf35f..0000000000000 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeDimmerHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2010-2018 by the respective copyright holders. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.amazonechocontrol.handler; - -import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.CHANNEL_DIMMER; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.Locale; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.smarthome.core.library.types.OnOffType; -import org.eclipse.smarthome.core.library.types.PercentType; -import org.eclipse.smarthome.core.thing.Thing; -import org.eclipse.smarthome.core.types.Command; -import org.openhab.binding.amazonechocontrol.internal.Connection; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link SmartHomeDimmerHandler} is responsible for the handling a smarthome dimmer device - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class SmartHomeDimmerHandler extends SmartHomeBaseHandler { - - private final Logger logger = LoggerFactory.getLogger(SmartHomeDimmerHandler.class); - - public SmartHomeDimmerHandler(Thing thing) { - super(thing); - } - - @Override - public void initialize() { - logger.info("SmartHomeDimmerHandler initialized"); - super.initialize(); - } - - @Override - public void handleCommand(Connection connection, String entityId, String channelId, Command command) - throws IOException, URISyntaxException { - - if (channelId.equals(CHANNEL_DIMMER)) { - if (command == OnOffType.ON) { - connection.sendSmartHomeDeviceCommand(entityId, "turnOn", null, null); - } - if (command == OnOffType.OFF) { - connection.sendSmartHomeDeviceCommand(entityId, "turnOff", null, null); - } - } - if (channelId.equals(CHANNEL_DIMMER)) { - if (command instanceof PercentType) { - PercentType value = (PercentType) command; - double percent = value.doubleValue(); - if (percent >= 0 && percent <= 100) { - String percentValue = String.format(Locale.ROOT, "%.2f", (percent / 100)); - connection.sendSmartHomeDeviceCommand(entityId, "setPercentage", "percentage", percentValue); - updateState(CHANNEL_DIMMER, value); - } - } - } - } -} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java deleted file mode 100644 index 7db28c0ee8762..0000000000000 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/SmartHomeSwitchHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2010-2018 by the respective copyright holders. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.amazonechocontrol.handler; - -import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.CHANNEL_SWITCH; - -import java.io.IOException; -import java.net.URISyntaxException; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.smarthome.core.library.types.OnOffType; -import org.eclipse.smarthome.core.thing.Thing; -import org.eclipse.smarthome.core.types.Command; -import org.openhab.binding.amazonechocontrol.internal.Connection; - -/** - * The {@link SmartHomeSwitchHandler} is responsible for the handling a smarthome switch device - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class SmartHomeSwitchHandler extends SmartHomeBaseHandler { - - public SmartHomeSwitchHandler(Thing thing) { - super(thing); - } - - @Override - public void handleCommand(Connection connection, String entityId, String channelId, Command command) - throws IOException, URISyntaxException { - if (channelId.equals(CHANNEL_SWITCH)) { - if (command == OnOffType.ON) { - connection.sendSmartHomeDeviceCommand(entityId, "turnOn", null, null); - updateState(CHANNEL_SWITCH, OnOffType.ON); - } - if (command == OnOffType.OFF) { - connection.sendSmartHomeDeviceCommand(entityId, "turnOff", null, null); - updateState(CHANNEL_SWITCH, OnOffType.OFF); - } - } - } -} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java index c4ff430b770c8..433d0f8e492c2 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountConfiguration.java @@ -27,10 +27,4 @@ public class AccountConfiguration { public String amazonSite; @Nullable public Integer pollingIntervalInSeconds; - - // The smarthome devices feature is currently not available in the configuration for public use, - // because there seems to be a problem in detecting and controlling devices, - // there seems to be different smarthome skill versions - @Nullable - public Boolean discoverSmartHomeDevices; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 30bf7c99d94c2..7d2d0a38bc69c 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -21,8 +21,6 @@ import org.openhab.binding.amazonechocontrol.handler.AccountHandler; import org.openhab.binding.amazonechocontrol.handler.EchoHandler; import org.openhab.binding.amazonechocontrol.handler.FlashBriefingProfileHandler; -import org.openhab.binding.amazonechocontrol.handler.SmartHomeDimmerHandler; -import org.openhab.binding.amazonechocontrol.handler.SmartHomeSwitchHandler; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.osgi.service.http.HttpService; @@ -60,12 +58,6 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { if (thingTypeUID.equals(THING_TYPE_FLASH_BRIEFING_PROFILE)) { return new FlashBriefingProfileHandler(thing); } - if (thingTypeUID.equals(THING_TYPE_SMART_HOME_DIMMER)) { - return new SmartHomeDimmerHandler(thing); - } - if (thingTypeUID.equals(THING_TYPE_SMART_HOME_SWITCH)) { - return new SmartHomeSwitchHandler(thing); - } if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { return new EchoHandler(thing); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 6ed5ab3a1bc6e..9fb075dd2764e 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -10,7 +10,6 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; -import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -20,7 +19,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; @@ -31,9 +29,7 @@ import org.eclipse.smarthome.core.thing.ThingUID; import org.openhab.binding.amazonechocontrol.handler.EchoHandler; import org.openhab.binding.amazonechocontrol.handler.FlashBriefingProfileHandler; -import org.openhab.binding.amazonechocontrol.handler.SmartHomeBaseHandler; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevice; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.slf4j.Logger; @@ -150,52 +146,6 @@ public void activate(@Nullable Map config) { } }; - public synchronized void setSmartHomeDevices(ThingUID brigdeThingUID, - List deviceInformations) { - Set toRemove = new HashSet(lastSmartHomeDeviceInformations.keySet()); - for (JsonSmartHomeDevice deviceInformation : deviceInformations) { - if (StringUtils.equalsIgnoreCase(deviceInformation.manufacturerName, "openHAB")) { - // Ignore devices provided by the openHAB skill - continue; - } - String entityId = deviceInformation.entityId; - if (entityId != null) { - boolean alreadyfound = toRemove.remove(entityId); - String[] actions = deviceInformation.actions; - if (!alreadyfound && actions != null) { - List actionList = Arrays.asList(actions); - if (actionList.contains("turnOn") && actionList.contains("turnOff")) { - - ThingTypeUID thingTypeId; - if (actionList.contains("setPercentage")) { - thingTypeId = THING_TYPE_SMART_HOME_DIMMER; - } else { - thingTypeId = THING_TYPE_SMART_HOME_SWITCH; - } - - ThingUID thingUID = new ThingUID(thingTypeId, brigdeThingUID, entityId); - - // Check if already created - if (SmartHomeBaseHandler.find(thingUID) == null) { - - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) - .withLabel(deviceInformation.friendlyName) - .withProperty(DEVICE_PROPERTY_ENTITY_ID, entityId) - .withRepresentationProperty(DEVICE_PROPERTY_ENTITY_ID).withBridge(brigdeThingUID) - .build(); - - logger.debug("Device [{}: {}] found. Mapped to thing type {}", - deviceInformation.friendlyName, entityId, thingTypeId.getAsString()); - - thingDiscovered(result); - lastSmartHomeDeviceInformations.put(entityId, thingUID); - } - } - } - } - } - } - public synchronized void setDevices(ThingUID brigdeThingUID, List deviceList) { Set toRemove = new HashSet(lastDeviceInformations.keySet()); for (Device device : deviceList) { From 53046c2ec3894f6a17dcba72b9b363f878c90587 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 29 Apr 2018 12:59:40 +0200 Subject: [PATCH 38/56] [amazonechocontrol] Smarthome feature remove, because it does not work as expected for the most smarthome skills Signed-off-by: Michael Geramb (github: mgeramb) --- .../internal/Connection.java | 52 +------------------ .../internal/jsons/JsonNetworkDetails.java | 22 -------- .../internal/jsons/JsonSmartHomeDevice.java | 25 --------- 3 files changed, 1 insertion(+), 98 deletions(-) delete mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java delete mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 5e2df3674c607..fcecb64b4b0cd 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -47,7 +47,6 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMediaState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNetworkDetails; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationRequest; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound; @@ -56,14 +55,12 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayValidationResult; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlayerState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; -import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevice; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonStartRoutineRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; @@ -886,51 +883,4 @@ public void playMusicVoiceCommand(Device device, String providerId, String voice String postData = gson.toJson(startRoutineRequest); makeRequest("POST", alexaServer + "/api/behaviors/preview", postData, true, true, null); } - - public List getSmartHomeDevices() throws IOException, URISyntaxException { - try { - String json = makeRequestAndReturnString(alexaServer + "/api/phoenix"); - logger.debug("getSmartHomeDevices result: {}", json); - - JsonNetworkDetails networkDetails = parseJson(json, JsonNetworkDetails.class); - Gson gson = new Gson(); - Object jsonObject = gson.fromJson(networkDetails.networkDetail, Object.class); - List result = new ArrayList(); - searchSmartHomeDevicesRecursive(gson, jsonObject, result); - return result; - } catch (Exception e) { - logger.warn("getSmartHomeDevices fails: {}", e.getMessage()); - throw e; - } - } - - private void searchSmartHomeDevicesRecursive(Gson gson, @Nullable Object jsonNode, - List result) { - if (jsonNode instanceof Map) { - @SuppressWarnings("rawtypes") - Map map = (Map) jsonNode; - if (map.containsKey("entityId") && map.containsKey("friendlyName") && map.containsKey("actions")) { - // device node found, create type element and add it to the results - JsonElement element = gson.toJsonTree(jsonNode); - JsonSmartHomeDevice device = gson.fromJson(element, JsonSmartHomeDevice.class); - result.add(device); - } else { - for (Object key : map.keySet()) { - Object value = map.get(key); - searchSmartHomeDevicesRecursive(gson, value, result); - } - } - } - } - - public void sendSmartHomeDeviceCommand(String entityId, String action, @Nullable String parameterName, - @Nullable String parameter) throws IOException, URISyntaxException { - String command = "{" + "\"controlRequests\": [{" + "\"entityId\": \"" + entityId + "\", " - + "\"entityType\": \"APPLIANCE\", " + "\"parameters\": {" + "\"action\": \"" + action + "\"" - + (parameterName != null ? ", \"" + parameterName + "\": \"" + parameter + "\"" : "") + " }" + "}]" - + "}"; - - String json = makeRequestAndReturnString("PUT", alexaServer + "/api/phoenix/state", command, true, null); - json.toString(); - } -} \ No newline at end of file +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java deleted file mode 100644 index 96f14435edfc2..0000000000000 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonNetworkDetails.java +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2010-2018 by the respective copyright holders. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonNetworkDetails} encapsulate the GSON data of a network query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonNetworkDetails { - public @Nullable String networkDetail; -} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java deleted file mode 100644 index f036780f86d98..0000000000000 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevice.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2010-2018 by the respective copyright holders. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.amazonechocontrol.internal.jsons; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link JsonSmartHomeDevice} encapsulate the GSON-part data of a network query - * - * @author Michael Geramb - Initial contribution - */ -@NonNullByDefault -public class JsonSmartHomeDevice { - public @Nullable String entityId; - public @Nullable String friendlyName; - public @Nullable String @Nullable [] actions; - public @Nullable String manufacturerName; -} From 6d590f0b828bd82fccbee99afa5ea9af5b6167ad Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 2 May 2018 21:28:27 +0200 Subject: [PATCH 39/56] [amazonechocontrol] Add new channel playGoodMorning, improve error handling for flashprofile update, add new tutorial to readme Signed-off-by: Michael Geramb (github: mgeramb) --- .../i18n/amazonechocontrol_de.properties | 3 ++ .../ESH-INF/thing/thing-types.xml | 9 ++++++ .../README.md | 28 ++++++++++++++++++- .../AmazonEchoControlBindingConstants.java | 1 + .../handler/AccountHandler.java | 4 +-- .../handler/EchoHandler.java | 10 ++++++- .../internal/Connection.java | 7 +++-- .../discovery/AmazonEchoDiscovery.java | 4 +++ 8 files changed, 59 insertions(+), 7 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties index f4051468410dd..57088cf657065 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties @@ -113,6 +113,9 @@ channel-type.amazonechocontrol.playWeatherReport.description = Startet den Wette channel-type.amazonechocontrol.playTrafficNews.label = Verkehrsnachrichten channel-type.amazonechocontrol.playTrafficNews.description = Started die Verkehrsnachrichten (Nur schreiben) +channel-type.amazonechocontrol.playGoodMorning.label = Guten Morgen +channel-type.amazonechocontrol.playGoodMorning.description = Started die Guten Morgen Nachricht (Nur schreiben) + channel-type.amazonechocontrol.startRoutine.label = Started eine Routine channel-type.amazonechocontrol.startRoutine.description = Tippen sie ein, was Sie normalerweise zu Alexa sagen um eine Routine zu starten, ohne "Alexa" vorangestellt (Nur schreiben) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 4198453dd42b8..ee37cb2174db3 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -68,6 +68,7 @@ + @@ -110,6 +111,7 @@ + @@ -152,6 +154,7 @@ + @@ -223,6 +226,7 @@ + @@ -303,6 +307,11 @@ Starts the traffic news (Write Only) + + Switch + + Starts good moring news (Write Only) + String diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 48bf01cfbfbb1..9cf002d566308 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -16,6 +16,7 @@ It provide features to control and view the current state of echo devices: - start traffic news - start daily briefing - start weather report +- start good moring report - start automation routine - activate multiple configurations of flash briefings - start playing music by providing the voice command as text (Works with all music providers) @@ -125,6 +126,7 @@ The flashbriefingprofile thing has no configuration parameters. It will be confi | playFlashBriefing | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the flash briefing | playWeatherReport | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the weather report | playTrafficNews | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the traffic news +| playGoodMorning | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the good moring report | startRoutine | Switch | W | echo, echoshow, echospot, unknown | Write Only! Type in what you normally say to Alexa without the preceding "Alexa," | playMusicProvider | String | W | echo, echoshow, echospot, unknown | Write Only! Music provider used for 'Start music voice command' | playMusicVoiceCommand | String | W | echo, echoshow, echospot, unknown | Write Only! Voice command as text. E.g. 'Yesterday from the Beatles' @@ -180,6 +182,7 @@ String Echo_Living_Room_PlayAlarmSound "Play Alarm Sound" Switch Echo_Living_Room_PlayFlashBriefing "Play Flash Briefing" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playFlashBriefing"} Switch Echo_Living_Room_PlayWeatherReport "Play Weather Report" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playWeatherReport"} Switch Echo_Living_Room_PlayTrafficNews "Play Traffic News" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playTrafficNews"} +Switch Echo_Living_Room_PlayGoodMoring "Play Good Morning News" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playGoodMorning"} String Echo_Living_Room_StartRoutine "Start Routine" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:startRoutine"} String Echo_Living_Room_PlayMusicProvider "Music Provider (Write Only)" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playMusicProvider"} String Echo_Living_Room_PlayMusicCommand "Play music voice command (Write Only)" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playMusicVoiceCommand"} @@ -217,6 +220,7 @@ sitemap amzonechocontrol label="Echo Devices" Switch item=Echo_Living_Room_PlayFlashBriefing Switch item=Echo_Living_Room_PlayWeatherReport Switch item=Echo_Living_Room_PlayTrafficNews + Switch item=Echo_Living_Room_PlayGoodMoring Text item=Echo_Living_Room_StartRoutine } @@ -239,7 +243,7 @@ To get instead of the id fields an selection box, use the Selection element and 1) Open the PaperUI 2) Navigate to the Control Section -3) Open the Drop-Dow +3) Open the Drop-Down of the 'Alarm Sound' channel 4) Select the Sound you want to here 5) Write down the text in the square brackets. e.g. ECHO:system_alerts_repetitive01 for the nightstand sound 6) Create a rule for start playing the sound: @@ -268,6 +272,28 @@ Note 1: Do not use a to short time for playing the sound, because alexa needs so Note 2: The rule have no effect for your default alarm sound used in the alexa app. +**Play a spotify playlist if a switch was changed to on:** + +1) Open the PaperUI +2) Navigate to the Control Section +3) Open the Drop-Down of the 'Music provider for the start music voice command' channel +4) Select the Provider you want to use +5) Write down the text in the square brackets. e.g. SPOTIFY for the spotify music provider +6) Create a rule for start playing a song or playlist: + + +```php +rule "Play a playlist on spotify if a switch was changed" +when + Item Spotify_Playlist_Switch changed to ON +then + Echo_Living_Room_PlayMusicProvider.sendCommand('SPOTIFY') + Echo_Living_Room_PlayMusicCommand.sendCommand('Playlist Party') +end +``` + +Note: I recommend, that you test the command send to play music command first with your voice on your alexa device. E.g. say 'Alexa, Playlist Party' + ## Credits The idea for writing this binding came from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German). Thank you Alex! diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java index 8a6ca60d2fc75..4cf3c2e0dbdfb 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java @@ -68,6 +68,7 @@ public class AmazonEchoControlBindingConstants { public static final String CHANNEL_PLAY_FLASH_BRIEFING = "playFlashBriefing"; public static final String CHANNEL_PLAY_WEATER_REPORT = "playWeatherReport"; public static final String CHANNEL_PLAY_TRAFFIC_NEWS = "playTrafficNews"; + public static final String CHANNEL_PLAY_GOOD_MORNING = "playGoodMorning"; public static final String CHANNEL_START_ROUTINE = "startRoutine"; public static final String CHANNEL_PLAY_MUSIC_PROVIDER = "playMusicProvider"; public static final String CHANNEL_PLAY_MUSIC_VOICE_COMMAND = "playMusicVoiceCommand"; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index e7e43bf8417d9..9e312c8c8bc45 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -49,6 +49,7 @@ import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; /** * Handles the connection to the amazon server. @@ -546,8 +547,7 @@ private void updateFlashBriefingProfiles(Connection currentConnection) { } Gson gson = new Gson(); this.currentFlashBriefingJson = gson.toJson(forSerializer); - - } catch (IOException | URISyntaxException e) { + } catch (JsonSyntaxException | IOException | URISyntaxException e) { logger.warn("get flash briefing profiles fails {}", e); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index c6bb1a2e6237d..1f989d55e9464 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -243,7 +243,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { String voiceCommand = ((StringType) command).toFullString(); if (!this.musicProviderId.isEmpty()) { connection.playMusicVoiceCommand(device, this.musicProviderId, voiceCommand); - waitForUpdate = 2000; + waitForUpdate = 3000; updatePlayMusicVoiceCommand = true; } } @@ -411,6 +411,13 @@ public void handleCommand(ChannelUID channelUID, Command command) { connection.executeSequenceCommand(device, "Alexa.Weather.Play"); } } + if (channelId.equals(CHANNEL_PLAY_GOOD_MORNING)) { + + if (command == OnOffType.ON) { + waitForUpdate = 1000; + connection.executeSequenceCommand(device, "Alexa.GoodMorning.Play"); + } + } if (channelId.equals(CHANNEL_START_ROUTINE)) { if (command instanceof StringType) { String utterance = ((StringType) command).toFullString(); @@ -709,6 +716,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto updateState(CHANNEL_PLAY_FLASH_BRIEFING, OnOffType.OFF); updateState(CHANNEL_PLAY_WEATER_REPORT, OnOffType.OFF); updateState(CHANNEL_PLAY_TRAFFIC_NEWS, OnOffType.OFF); + updateState(CHANNEL_PLAY_GOOD_MORNING, OnOffType.OFF); updateState(CHANNEL_AMAZON_MUSIC_TRACK_ID, new StringType(amazonMusicTrackId)); updateState(CHANNEL_AMAZON_MUSIC, playing && amazonMusic ? OnOffType.ON : OnOffType.OFF); updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID, new StringType(amazonMusicPlayListId)); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index fcecb64b4b0cd..b64d991c392bb 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -537,13 +537,13 @@ public void logout() { } // parser - private T parseJson(String json, Class type) { + private T parseJson(String json, Class type) throws JsonSyntaxException { try { Gson gson = new Gson(); return gson.fromJson(json, type); } catch (JsonSyntaxException e) { logger.warn("Parsing json failed {}", e); - logger.warn("{}", json); + logger.warn("Illegal json: {}", json); throw e; } } @@ -658,7 +658,8 @@ public void playAmazonMusicPlayList(Device device, @Nullable String playListId) } } - // commands: Alexa.Weather.Play, Alexa.Traffic.Play, Alexa.FlashBriefing.Play + // commands: Alexa.Weather.Play, Alexa.Traffic.Play, Alexa.FlashBriefing.Play, Alexa.GoodMorning.Play, + // Alexa.SingASong.Play, Alexa.TellStory.Play public void executeSequenceCommand(Device device, String command) throws IOException, URISyntaxException { String json = "{ \"behaviorId\": \"amzn1.alexa.automation.00000000-0000-0000-0000-000000000000\", " + " \"sequenceJson\": \"{\\\"@type\\\":\\\"com.amazon.alexa.behaviors.model.Sequence\\\",\\\"startNode\\\":{\\\"@type\\\":\\\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\\\",\\\"type\\\":\\\"" diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 9fb075dd2764e..f896718cf2d74 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -10,6 +10,7 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -56,6 +57,7 @@ public class AmazonEchoDiscovery extends AbstractDiscoveryService { @Nullable ScheduledFuture startScanStateJob; + long activateTimeStamp; public static void addDiscoveryHandler(IAmazonEchoDiscovery discoveryService) { synchronized (discoveryServices) { @@ -93,6 +95,7 @@ protected void startAutomaticScan() { void startScan(boolean manual) { stopScanJob(); + removeOlderResults(activateTimeStamp); if (discoverAccount) { discoverAccount = false; @@ -144,6 +147,7 @@ public void activate(@Nullable Map config) { if (config != null) { modified(config); } + activateTimeStamp = new Date().getTime(); }; public synchronized void setDevices(ThingUID brigdeThingUID, List deviceList) { From d0b21f31a5e520de0495be741ae189523e66b5d2 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sat, 5 May 2018 22:07:57 +0200 Subject: [PATCH 40/56] [amazonechocontrol] Fixed review comments (spelling, formatting) Signed-off-by: Michael Geramb (github: mgeramb) --- .../ESH-INF/binding/binding.xml | 9 +- .../i18n/amazonechocontrol_de.properties | 23 +- .../ESH-INF/thing/thing-types.xml | 838 +++++++++--------- .../README.md | 8 +- .../handler/EchoHandler.java | 2 +- .../internal/Connection.java | 2 +- 6 files changed, 428 insertions(+), 454 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml index a26b2ed9c2254..eee1e3b23af4e 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml @@ -1,8 +1,9 @@ - + - Amazon Echo Control Binding - Binding to control Amazon Echo devices (Alexa). This binding enables openhab to control the volume, playing state, bluetooth connection of your amazon echo devices. - Michael Geramb + Amazon Echo Control Binding + Binding to control Amazon Echo devices (Alexa). This binding enables openHAB to control the volume, playing state, bluetooth connection of your amazon echo devices. + Michael Geramb diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties index 57088cf657065..59742abe0a883 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties @@ -20,7 +20,7 @@ thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.label = Sta thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.description = Aktualtisierungs-Intervall für den Status in Sekunden. Kleinere Zeiten verursachen höheren Netzwerkverkehr. thing-type.config.amazonechocontrol.account.discoverSmartHomeDevices.label = Sucht Smart Home Geräte -thing-type.config.amazonechocontrol.account.discoverSmartHomeDevices.description = Sucht Smart Home Geräte die über einen Smart Home Alexa Skill verbunden sind. Der OpenHAB Alexa Skill wird ignoriert. +thing-type.config.amazonechocontrol.account.discoverSmartHomeDevices.description = Sucht Smart Home Geräte die über einen Smart Home Alexa Skill verbunden sind. Der openHAB Alexa Skill wird ignoriert. thing-type.amazonechocontrol.echo.label = Amazon Echo @@ -57,19 +57,6 @@ thing-type.amazonechocontrol.flashbriefingprofile.description = Speichert und l thing-type.config.amazonechocontrol.unknown.serialNumber.label = Seriennummer thing-type.config.amazonechocontrol.unknown.serialNumber.description = Die Seriennummer findest du in der Alexa App. -thing-type.amazonechocontrol.smarthomeswitch.label = Schalter -thing-type.amazonechocontrol.smarthomeswitch.description = Smart Home Switch - -thing-type.config.amazonechocontrol.smarthomeswitch.entityId.label = Entity ID -thing-type.config.amazonechocontrol.smarthomeswitch.entityId.description = Suchfunktion von OpenHAB verwenden um die ID zu erfahren. - -thing-type.amazonechocontrol.smarthomedimmer.label = Dimmer -thing-type.amazonechocontrol.smarthomedimmer.description = Smart Home Dimmer - -thing-type.config.amazonechocontrol.smarthomedimmer.entityId.label = Entity ID -thing-type.config.amazonechocontrol.smarthomedimmer.entityId.description = Suchfunktion von OpenHAB verwenden um die ID zu erfahren. - - # channel types channel-type.amazonechocontrol.bluetoothDeviceName.label = Bluetooth Gerät channel-type.amazonechocontrol.bluetoothDeviceName.description = Verbundenes Bluetoothgerät @@ -119,7 +106,7 @@ channel-type.amazonechocontrol.playGoodMorning.description = Started die Guten M channel-type.amazonechocontrol.startRoutine.label = Started eine Routine channel-type.amazonechocontrol.startRoutine.description = Tippen sie ein, was Sie normalerweise zu Alexa sagen um eine Routine zu starten, ohne "Alexa" vorangestellt (Nur schreiben) -channel-type.amazonechocontrol.playMusicProvider.label = Musikanbieter für Starte Musik Sprachbefehl +channel-type.amazonechocontrol.playMusicProvider.label = Musikanbieter channel-type.amazonechocontrol.playMusicProvider.description = Musikanbieter der für 'Starte Musik Sprachbefehl' verwendet wird (Nur schreiben) channel-type.amazonechocontrol.playMusicVoiceCommand.label = Starte Musik Sprachbefehl @@ -163,9 +150,3 @@ channel-type.amazonechocontrol.active.description = Aktiviert diese t channel-type.amazonechocontrol.playOnDevice.label = Wiedergabe am Gerät channel-type.amazonechocontrol.playOnDevice.description = Started die Wiedergabe am Gerät (Seriennummer oder Name, nur schreiben) - -channel-type.amazonechocontrol.switch.label = Schalter -channel-type.amazonechocontrol.switch.description = Schaltet das Gerät ein oder aus - -channel-type.amazonechocontrol.dimmer.label = Dimmer -channel-type.amazonechocontrol.dimmer.description = Dimmer Steuerung diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index ee37cb2174db3..832a114f6d030 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -1,425 +1,417 @@ - - - - Amazon Account where your amazon echo is registered. - - - - - false - - - - - - - - - - Select or type in the site where your amazon account is created. - - - - - Enter the email address of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! - - - - password - - Enter the password of the amazon account which is used for the amazon echo devices. IMPORTANT: If the account thing does not go online and reports and login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. - - - 60 - - Refresh state interval in seconds. Lower time causes more network traffic. - Seconds - - - - - - - - - Amazon Echo device (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - Amazon Echo Spot device - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - Amazon Echo Spot device - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - Amazon Multiroom Music - - - - - - - - - - - - - - - - serialNumber - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - Unknown Echo Device. Warning: Maybe not all channels will be supported from the device - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - Store and load a flash briefing configuration - - - - - - - - Switch - - Save the current flash briefing configuration (Write only) - - - Switch - - Activate this flash briefing configuration - - - String - - Plays the briefing on the device (serial number or name, write only) - - - Switch - - Turns the device on or off - - - Dimmer - - Dimmer control - - - String - - Connected bluetooth device - - - - String - - Id of the radio station - - - String - - Speak the reminder and send a notification to the Alexa app - - - Switch - - Starts the flash briefing (Write Only) - - - Switch - - Starts the weather report (Write Only) - - - Switch - - Starts the traffic news (Write Only) - - - Switch - - Starts good moring news (Write Only) - - - String - - Type in what you normally say to Alexa without the preceding "Alexa," (Write Only) - - - String - - Plays an alarm sound - - - String - - Id of the amazon music track - - - Switch - - Amazon Music turned on - - - String - - Amazon Music play list id (Write only, no current state) - - - String - - Id of the playlist which was started with openHAB - - - String - - Name of music provider - - - - String - - MAC-Address of the bluetooth connected device - - - String - - Bluetooth connection selection (Currently only in PaperUI) - - - String - - Url of the album image or radio station logo - - - - String - - Title - - - - String - - Subtitle 1 - - - - String - - Subtitle 2 - - - - Switch - - Radio turned on - - - Switch - - Connect to last used device - - - Switch - - Loop - - - Switch - - Shuffle play - - - Player - - Music Player - - - Dimmer - - Volume of the sound - - - String - - Music provider used for 'Start music voice command' (Write only) - - - String - - Voice command as text. E.g. 'Yesterday from the Beatles' (Write only) - + + + + Amazon Account where your amazon echo is registered. + + + + + false + + + + + + + + + + Select or type in the site where your amazon account is created. + + + + + Enter the email address of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! + + + + password + + Enter the password of the amazon account which is used for the amazon echo devices. IMPORTANT: If the account thing does not go online and reports and login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. + + + 60 + + Refresh state interval in seconds. Lower time causes more network traffic. + Seconds + + + + + + + + + Amazon Echo device (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + Amazon Echo Spot device + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + Amazon Echo Show device + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + Amazon Multiroom Music + + + + + + + + + + + + + + + + serialNumber + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + Unknown Echo Device. Warning: Maybe not all channels will be supported from the device + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serialNumber + + + + You will find the serial number of your device in the Alexa app + + + + + + + + + Store and load a flash briefing configuration + + + + + + + + Switch + + Save the current flash briefing configuration (Write only) + + + Switch + + Activate this flash briefing configuration + + + String + + Plays the briefing on the device (serial number or name, write only) + + + String + + Connected bluetooth device + + + + String + + Id of the radio station + + + String + + Speak the reminder and send a notification to the Alexa app + + + Switch + + Starts the flash briefing (Write Only) + + + Switch + + Starts the weather report (Write Only) + + + Switch + + Starts the traffic news (Write Only) + + + Switch + + Starts good morning news (Write Only) + + + String + + Type in what you normally say to Alexa without the preceding "Alexa," (Write Only) + + + String + + Plays an alarm sound + + + String + + Id of the amazon music track + + + Switch + + Amazon Music turned on + + + String + + Amazon Music play list id (Write only, no current state) + + + String + + Id of the playlist which was started with openHAB + + + String + + Name of music provider + + + + String + + MAC-Address of the bluetooth connected device + + + String + + Bluetooth connection selection (Currently only in PaperUI) + + + String + + Url of the album image or radio station logo + + + + String + + Title + + + + String + + Subtitle 1 + + + + String + + Subtitle 2 + + + + Switch + + Radio turned on + + + Switch + + Connect to last used device + + + Switch + + Loop + + + Switch + + Shuffle play + + + Player + + Music Player + + + Dimmer + + Volume of the sound + + + String + + Music provider used for 'Start music voice command' (Write only) + + + String + + Voice command as text. E.g. 'Yesterday from the Beatles' (Write only) + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 9cf002d566308..ff92c94cc8ade 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -1,8 +1,8 @@ # Amazon Echo Control Binding -This binding can control Amazon Echo devices (Alexa) from openhab. +This binding can control Amazon Echo devices (Alexa). -It provide features to control and view the current state of echo devices: +It provides features to control and view the current state of echo devices: - volume - pause/continue/next track/previous track @@ -83,9 +83,9 @@ The Amazon Account thing need the following configurations: 2 factor authentication is not supported! -** HINT ** IMPORTANT: If the Account thing does not go online and reports a login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_ACCOUNT_THING (Replace YOUR_OPENHAB and ID_OF_ACCOUNT_THING with your configuration) in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. +** HINT ** IMPORTANT: If the Account thing does not go online and reports a login error, open the url YOUR_OPENHAB/amazonechocontrol/ID_OF_ACCOUNT_THING (Replace YOUR_OPENHAB and ID_OF_ACCOUNT_THING with your configuration) in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. -### Amazon devices +### Amazon Devices All Amazon devices (echo, echospot, echoshow, wha, unknown) needs the following configurations: diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 1f989d55e9464..e2af80bef6da3 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -88,7 +88,7 @@ public EchoHandler(Thing thing) { @Override public void initialize() { - logger.info("Amazon Echo Control Binding initialized"); + logger.debug("Amazon Echo Control Binding initialized"); synchronized (instances) { instances.put(this.getThing().getUID(), this); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index b64d991c392bb..f73f2a9d3031c 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -395,7 +395,7 @@ public HttpsURLConnection makeRequest(String verb, String url, @Nullable String throw new HttpException(code, verb + " url '" + url + "' failed: " + connection.getResponseMessage()); } } - throw new ConnectionException("To many redirects"); + throw new ConnectionException("Too many redirects"); } public boolean getIsLoggedIn() { From 19c7d53cdf8f70cdc1e269ebd50d096109129c9c Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 6 May 2018 08:26:39 +0200 Subject: [PATCH 41/56] [amazonechocontrol] Fixed review comments (spelling) Signed-off-by: Michael Geramb (github: mgeramb) --- .../org.openhab.binding.amazonechocontrol/README.md | 7 ++++--- .../binding/amazonechocontrol/internal/LoginServlet.java | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index ff92c94cc8ade..871f2840a9032 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -95,7 +95,7 @@ All Amazon devices (echo, echospot, echoshow, wha, unknown) needs the following You will find the serial number in the alexa app. -### Flash briefing profile +### Flash Briefing Profile The flashbriefingprofile thing 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. @@ -237,11 +237,12 @@ To get instead of the id fields an selection box, use the Selection element and ``` Selection item=Echo_Living_Room_RadioStationId mappings=[ ''='Off', 's1139'='Antenne Steiermark', 's8007'='Hitradio Ă–3', 's16793'='Radio 10', 's8235'='FM4' ] ``` + ## Tutorials **Playing an alarm sound for 15 seconds with an openHAB rule if an door contact was opened:** -1) Open the PaperUI +1) Open the Paper UI 2) Navigate to the Control Section 3) Open the Drop-Down of the 'Alarm Sound' channel 4) Select the Sound you want to here @@ -274,7 +275,7 @@ 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) Open the PaperUI +1) Open the Paper UI 2) Navigate to the Control Section 3) Open the Drop-Down of the 'Music provider for the start music voice command' channel 4) Select the Provider you want to use diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java index 1f573bc2b9fbb..12ec7e731f174 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java @@ -32,7 +32,7 @@ import org.slf4j.LoggerFactory; /** - * Simple http proxy to forwards the login dialog from amazon to the user through the binding + * 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 * * @author Michael Geramb - Initial Contribution From cf8b5dcb9a022b9979d91b5aafadfd7105d6b5f1 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 6 May 2018 17:51:44 +0200 Subject: [PATCH 42/56] [amazonechocontrol] static fields and functions removed Signed-off-by: Michael Geramb (github: mgeramb) --- .../handler/AccountHandler.java | 22 ++++-- .../handler/EchoHandler.java | 15 ---- .../handler/FlashBriefingProfileHandler.java | 30 +------- .../AmazonEchoControlHandlerFactory.java | 29 ++++++- .../discovery/AmazonEchoDiscovery.java | 77 ++++++++++--------- ...covery.java => IAmazonAccountHandler.java} | 4 +- ...onEchoDynamicStateDescriptionProvider.java | 48 +++++++++--- 7 files changed, 124 insertions(+), 101 deletions(-) rename addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/{IAmazonEchoDiscovery.java => IAmazonAccountHandler.java} (75%) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index 9e312c8c8bc45..37d5ebf314cc5 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -39,7 +39,7 @@ import org.openhab.binding.amazonechocontrol.internal.LoginServlet; import org.openhab.binding.amazonechocontrol.internal.StateStorage; import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; -import org.openhab.binding.amazonechocontrol.internal.discovery.IAmazonEchoDiscovery; +import org.openhab.binding.amazonechocontrol.internal.discovery.IAmazonAccountHandler; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; @@ -57,7 +57,7 @@ * @author Michael Geramb - Initial Contribution */ @NonNullByDefault -public class AccountHandler extends BaseBridgeHandler implements IAmazonEchoDiscovery { +public class AccountHandler extends BaseBridgeHandler implements IAmazonAccountHandler { private final Logger logger = LoggerFactory.getLogger(AccountHandler.class); private StateStorage stateStorage; @@ -71,13 +71,15 @@ public class AccountHandler extends BaseBridgeHandler implements IAmazonEchoDisc private boolean discoverFlashProfiles; private String currentFlashBriefingJson = ""; private final HttpService httpService; + private final AmazonEchoDiscovery amazonEchoDiscovery; private @Nullable LoginServlet loginServlet; - public AccountHandler(Bridge bridge, HttpService httpService) { + public AccountHandler(Bridge bridge, HttpService httpService, AmazonEchoDiscovery amazonEchoDiscovery) { super(bridge); this.httpService = httpService; + this.amazonEchoDiscovery = amazonEchoDiscovery; + this.amazonEchoDiscovery.resetDiscoverAccount(); stateStorage = new StateStorage(bridge); - AmazonEchoDiscovery.setHandlerExist(); } @Override @@ -226,7 +228,7 @@ public void dispose() { loginServlet.dispose(); } this.loginServlet = null; - AmazonEchoDiscovery.removeDiscoveryHandler(this); + this.amazonEchoDiscovery.removeAccountHandler(this); cleanup(); super.dispose(); } @@ -345,7 +347,7 @@ private void checkLogin() { private void handleValidLogin() { updateDeviceList(false); updateStatus(ThingStatus.ONLINE); - AmazonEchoDiscovery.addDiscoveryHandler(this); + this.amazonEchoDiscovery.addAccountHandler(this); } // used to set a valid connection from the web proxy login @@ -503,8 +505,9 @@ private void updateFlashBriefingHandlers(Connection currentConnection) { updateFlashBriefingProfiles(currentConnection); } + boolean flashBriefingProfileFound = false; for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) { - child.initialize(this, currentFlashBriefingJson); + flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson); } if (flashBriefingProfileHandlers.isEmpty()) { discoverFlashProfiles = true; // discover at least one device @@ -513,7 +516,10 @@ private void updateFlashBriefingHandlers(Connection currentConnection) { if (discoveryService != null) { if (discoverFlashProfiles) { discoverFlashProfiles = false; - discoveryService.discoverFlashBriefingProfiles(getThing().getUID(), this.currentFlashBriefingJson); + if (!flashBriefingProfileFound) { + discoveryService.discoverFlashBriefingProfiles(getThing().getUID(), + this.currentFlashBriefingJson); + } } } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index e2af80bef6da3..05d4a7d76973a 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -12,7 +12,6 @@ import java.io.IOException; import java.net.URISyntaxException; -import java.util.HashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -30,7 +29,6 @@ import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; -import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; @@ -64,7 +62,6 @@ public class EchoHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(EchoHandler.class); - private final static HashMap instances = new HashMap(); private @Nullable Device device; private @Nullable Connection connection; private @Nullable ScheduledFuture updateStateJob; @@ -90,9 +87,6 @@ public EchoHandler(Thing thing) { public void initialize() { logger.debug("Amazon Echo Control Binding initialized"); - synchronized (instances) { - instances.put(this.getThing().getUID(), this); - } if (this.connection != null) { updateStatus(ThingStatus.ONLINE); } else { @@ -115,9 +109,6 @@ public void intialize(Connection connection, @Nullable Device deviceJson) { @Override public void dispose() { - synchronized (instances) { - instances.remove(this.getThing().getUID()); - } stopCurrentNotification(); ScheduledFuture updateStateJob = this.updateStateJob; this.updateStateJob = null; @@ -127,12 +118,6 @@ public void dispose() { super.dispose(); } - public static @Nullable EchoHandler find(ThingUID uid) { - synchronized (instances) { - return instances.get(uid); - } - } - public @Nullable BluetoothState findBluetoothState() { return this.bluetoothState; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java index 14491f78bf906..833dfc26eaaa0 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java @@ -12,10 +12,10 @@ import java.io.IOException; import java.net.URISyntaxException; -import java.util.HashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.types.OnOffType; @@ -24,7 +24,6 @@ import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; -import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; @@ -44,7 +43,6 @@ public class FlashBriefingProfileHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(FlashBriefingProfileHandler.class); - private static HashMap instances = new HashMap(); @Nullable AccountHandler accountHandler; @@ -62,30 +60,10 @@ public FlashBriefingProfileHandler(Thing thing) { return this.accountHandler; } - public static @Nullable FlashBriefingProfileHandler find(ThingUID uid) { - synchronized (instances) { - return instances.get(uid); - } - } - - public static boolean exist(String profileJson) { - synchronized (instances) { - for (FlashBriefingProfileHandler handler : instances.values()) { - if (handler.currentConfigurationJson.equals(profileJson)) { - return true; - } - } - } - return false; - } - @Override public void initialize() { updatePlayOnDevice = true; logger.info("{} initialized", getClass().getSimpleName()); - synchronized (instances) { - instances.put(this.getThing().getUID(), this); - } if (!this.currentConfigurationJson.isEmpty()) { updateStatus(ThingStatus.ONLINE); } else { @@ -102,9 +80,6 @@ public void initialize() { @Override public void dispose() { - synchronized (instances) { - instances.remove(getThing().getUID(), this); - } ScheduledFuture updateStateJob = this.updateStateJob; this.updateStateJob = null; if (updateStateJob != null) { @@ -187,7 +162,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - public void initialize(AccountHandler handler, String currentConfigurationJson) { + public boolean initialize(AccountHandler handler, String currentConfigurationJson) { updateState(CHANNEL_SAVE, OnOffType.OFF); if (updatePlayOnDevice) { updateState(CHANNEL_PLAY_ON_DEVICE, new StringType("")); @@ -213,6 +188,7 @@ public void initialize(AccountHandler handler, String currentConfigurationJson) } else { updateState(CHANNEL_ACTIVE, OnOffType.OFF); } + return StringUtils.equals(this.currentConfigurationJson, currentConfigurationJson); } private String saveCurrentProfile(AccountHandler connection) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 7d2d0a38bc69c..99385e1d593b6 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -12,6 +12,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.DiscoveryService; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingTypeUID; @@ -21,8 +22,11 @@ import org.openhab.binding.amazonechocontrol.handler.AccountHandler; import org.openhab.binding.amazonechocontrol.handler.EchoHandler; import org.openhab.binding.amazonechocontrol.handler.FlashBriefingProfileHandler; +import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.http.HttpService; /** @@ -38,6 +42,9 @@ public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { @Nullable HttpService httpService; + @Nullable + AmazonEchoDiscovery amazonEchoDiscovery; + @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); @@ -51,8 +58,13 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { if (httpService == null) { return null; } + AmazonEchoDiscovery amazonEchoDiscovery = this.amazonEchoDiscovery; + if (amazonEchoDiscovery == null) { + return null; + } + if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { - AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService); + AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService, amazonEchoDiscovery); return bridgeHandler; } if (thingTypeUID.equals(THING_TYPE_FLASH_BRIEFING_PROFILE)) { @@ -64,7 +76,20 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return null; } - @Reference + @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC) + protected void setDiscoverService(DiscoveryService discoverService) { + if (discoverService instanceof AmazonEchoDiscovery) { + amazonEchoDiscovery = (AmazonEchoDiscovery) discoverService; + } + } + + protected void unsetDiscoverService(DiscoveryService discoverService) { + if (discoverService == amazonEchoDiscovery) { + amazonEchoDiscovery = null; + } + } + + @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC) protected void setHttpService(HttpService httpService) { this.httpService = httpService; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index f896718cf2d74..3c50f8ba00d2a 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -26,13 +26,15 @@ import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.ThingRegistry; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; -import org.openhab.binding.amazonechocontrol.handler.EchoHandler; -import org.openhab.binding.amazonechocontrol.handler.FlashBriefingProfileHandler; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; 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.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,26 +48,39 @@ @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.amazonechocontrol") public class AmazonEchoDiscovery extends AbstractDiscoveryService { - private static boolean discoverAccount = true; - private final static Set discoveryServices = new HashSet<>(); + private @Nullable ThingRegistry thingRegistry; + private boolean discoverAccount = true; + private final Set discoveryServices = new HashSet<>(); public @Nullable static AmazonEchoDiscovery instance; private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); private final Map lastDeviceInformations = new HashMap<>(); - private final Map lastSmartHomeDeviceInformations = new HashMap<>(); private final HashSet discoverdFlashBriefings = new HashSet(); @Nullable ScheduledFuture startScanStateJob; long activateTimeStamp; - public static void addDiscoveryHandler(IAmazonEchoDiscovery discoveryService) { + @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC) + protected void setThingRegistry(ThingRegistry thingRegistry) { + this.thingRegistry = thingRegistry; + } + + protected void unsetThingRegistry(ThingRegistry thingRegistry) { + this.thingRegistry = thingRegistry; + } + + public void resetDiscoverAccount() { + this.discoverAccount = false; + } + + public void addAccountHandler(IAmazonAccountHandler discoveryService) { synchronized (discoveryServices) { discoveryServices.add(discoveryService); } } - public static void removeDiscoveryHandler(IAmazonEchoDiscovery discoveryService) { + public void removeAccountHandler(IAmazonAccountHandler discoveryService) { synchronized (discoveryServices) { discoveryServices.remove(discoveryService); } @@ -80,10 +95,6 @@ public void deactivate() { super.deactivate(); } - public static void setHandlerExist() { - discoverAccount = false; - } - @Override protected void startScan() { startScan(true); @@ -107,13 +118,13 @@ void startScan(boolean manual) { thingDiscovered(result); } - IAmazonEchoDiscovery[] accounts; + IAmazonAccountHandler[] accounts; synchronized (discoveryServices) { - accounts = new IAmazonEchoDiscovery[discoveryServices.size()]; + accounts = new IAmazonAccountHandler[discoveryServices.size()]; accounts = discoveryServices.toArray(accounts); } - for (IAmazonEchoDiscovery discovery : accounts) { + for (IAmazonAccountHandler discovery : accounts) { discovery.updateDeviceList(manual); } } @@ -151,6 +162,11 @@ public void activate(@Nullable Map config) { }; public synchronized void setDevices(ThingUID brigdeThingUID, List deviceList) { + ThingRegistry thingRegistry = this.thingRegistry; + if (thingRegistry == null) { + return; + } + Set toRemove = new HashSet(lastDeviceInformations.keySet()); for (Device device : deviceList) { String serialNumber = device.serialNumber; @@ -173,10 +189,8 @@ public synchronized void setDevices(ThingUID brigdeThingUID, List device } ThingUID thingUID = new ThingUID(thingTypeId, brigdeThingUID, serialNumber); - // Check if already created - if (EchoHandler.find(thingUID) == null) { - + if (thingRegistry.get(thingUID) == null) { DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel(device.accountName) .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) .withProperty(DEVICE_PROPERTY_FAMILY, deviceFamily) @@ -201,20 +215,19 @@ public synchronized void discoverFlashBriefingProfiles(ThingUID brigdeThingUID, if (discoverdFlashBriefings.contains(currentFlashBriefingJson)) { return; } - if (!FlashBriefingProfileHandler.exist(currentFlashBriefingJson)) { - if (!discoverdFlashBriefings.contains(currentFlashBriefingJson)) { - String id = UUID.randomUUID().toString(); - ThingUID thingUID = new ThingUID(THING_TYPE_FLASH_BRIEFING_PROFILE, brigdeThingUID, id); + if (!discoverdFlashBriefings.contains(currentFlashBriefingJson)) { - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("FlashBriefing") - .withProperty(DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE, currentFlashBriefingJson) - .withBridge(brigdeThingUID).build(); - logger.debug("Flash Briefing {} discovered", currentFlashBriefingJson); + String id = UUID.randomUUID().toString(); + ThingUID thingUID = new ThingUID(THING_TYPE_FLASH_BRIEFING_PROFILE, brigdeThingUID, id); - thingDiscovered(result); - discoverdFlashBriefings.add(currentFlashBriefingJson); - } + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("FlashBriefing") + .withProperty(DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE, currentFlashBriefingJson) + .withBridge(brigdeThingUID).build(); + logger.debug("Flash Briefing {} discovered", currentFlashBriefingJson); + + thingDiscovered(result); + discoverdFlashBriefings.add(currentFlashBriefingJson); } } @@ -226,14 +239,6 @@ public synchronized void removeExistingEchoHandler(ThingUID uid) { } } - public synchronized void removeExistingSmartHomeHandler(ThingUID uid) { - for (String id : lastSmartHomeDeviceInformations.keySet()) { - if (lastSmartHomeDeviceInformations.get(id).equals(uid)) { - lastSmartHomeDeviceInformations.remove(id); - } - } - } - public synchronized void removeExistingFlashBriefingProfile(@Nullable String currentFlashBriefingJson) { if (currentFlashBriefingJson != null) { discoverdFlashBriefings.remove(currentFlashBriefingJson); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonAccountHandler.java similarity index 75% rename from addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java rename to addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonAccountHandler.java index 4186c5f160bee..97979ebfb0bd8 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonAccountHandler.java @@ -9,10 +9,10 @@ package org.openhab.binding.amazonechocontrol.internal.discovery; /** - * The {@link AmazonEcIAmazonEchoDiscoveryhoDiscovery} is responsible connection between account and discovery service + * The {@link IAmazonAccountHandler} is responsible connection between account and discovery service * * @author Michael Geramb - Initial contribution */ -public interface IAmazonEchoDiscovery { +public interface IAmazonAccountHandler { void updateDeviceList(boolean manual); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java index 16bf109e673a7..11b5794f11637 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -21,6 +21,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingRegistry; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider; import org.eclipse.smarthome.core.types.StateDescription; import org.eclipse.smarthome.core.types.StateOption; @@ -36,6 +39,9 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists.PlayList; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +57,28 @@ public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDescriptionProvider { private final Logger logger = LoggerFactory.getLogger(AmazonEchoDynamicStateDescriptionProvider.class); + private @Nullable ThingRegistry thingRegistry; + + @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC) + protected void setThingRegistry(ThingRegistry thingRegistry) { + this.thingRegistry = thingRegistry; + } + + protected void unsetThingRegistry(ThingRegistry thingRegistry) { + this.thingRegistry = thingRegistry; + } + + public @Nullable ThingHandler findHandler(Channel channel) { + ThingRegistry thingRegistry = this.thingRegistry; + if (thingRegistry == null) { + return null; + } + Thing thing = thingRegistry.get(channel.getUID().getThingUID()); + if (thing == null) { + return null; + } + return thing.getHandler(); + } @Override public @Nullable StateDescription getStateDescription(Channel channel, @@ -58,17 +86,19 @@ public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDe if (originalStateDescription == null) { return null; } + ThingRegistry thingRegistry = this.thingRegistry; + if (thingRegistry == null) { + return originalStateDescription; + } if (CHANNEL_TYPE_BLUETHOOTH_ID_SELECTION.equals(channel.getChannelTypeUID())) { - EchoHandler handler = EchoHandler.find(channel.getUID().getThingUID()); + EchoHandler handler = (EchoHandler) findHandler(channel); if (handler == null) { return originalStateDescription; } - BluetoothState bluetoothState = handler.findBluetoothState(); if (bluetoothState == null) { return originalStateDescription; } - PairedDevice[] pairedDeviceList = bluetoothState.pairedDeviceList; if (pairedDeviceList == null) { return originalStateDescription; @@ -90,8 +120,7 @@ public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDe return result; } else if (CHANNEL_TYPE_AMAZON_MUSIC_PLAY_LIST_ID.equals(channel.getChannelTypeUID())) { - - EchoHandler handler = EchoHandler.find(channel.getUID().getThingUID()); + EchoHandler handler = (EchoHandler) findHandler(channel); if (handler == null) { return originalStateDescription; } @@ -130,7 +159,7 @@ public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDe originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); return result; } else if (CHANNEL_TYPE_PLAY_ALARM_SOUND.equals(channel.getChannelTypeUID())) { - EchoHandler handler = EchoHandler.find(channel.getUID().getThingUID()); + EchoHandler handler = (EchoHandler) findHandler(channel); if (handler == null) { return originalStateDescription; } @@ -163,17 +192,15 @@ public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDe } } - StateDescription result = new StateDescription(originalStateDescription.getMinimum(), originalStateDescription.getMaximum(), originalStateDescription.getStep(), originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); return result; } else if (CHANNEL_TYPE_CHANNEL_PLAY_ON_DEVICE.equals(channel.getChannelTypeUID())) { - FlashBriefingProfileHandler handler = FlashBriefingProfileHandler.find(channel.getUID().getThingUID()); + FlashBriefingProfileHandler handler = (FlashBriefingProfileHandler) findHandler(channel); if (handler == null) { return originalStateDescription; } - AccountHandler accountHandler = handler.findAccountHandler(); if (accountHandler == null) { return originalStateDescription; @@ -196,7 +223,7 @@ public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDe originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); return result; } else if (CHANNEL_TYPE_PLAY_MUSIC_PROVIDER.equals(channel.getChannelTypeUID())) { - EchoHandler handler = EchoHandler.find(channel.getUID().getThingUID()); + EchoHandler handler = (EchoHandler) findHandler(channel); if (handler == null) { return originalStateDescription; } @@ -226,5 +253,4 @@ public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDe } return originalStateDescription; } - } From c2dfa613ffe599ed55cf6f8ce847c7caf915ea26 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Thu, 10 May 2018 15:20:43 +0200 Subject: [PATCH 43/56] [amazonechocontrol] linebreaks to readme added Signed-off-by: Michael Geramb (github: mgeramb) --- .../README.md | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 871f2840a9032..31e8fa161da69 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -16,7 +16,7 @@ It provides features to control and view the current state of echo devices: - start traffic news - start daily briefing - start weather report -- start good moring report +- start good morning report - start automation routine - activate multiple configurations of flash briefings - start playing music by providing the voice command as text (Works with all music providers) @@ -35,18 +35,23 @@ Some ideas what you can do in your home by using rules and other openHAB control ## Note ## -This binding uses the same API as the Web-Browser-Based Alexa site (alexa.amazon.de). In other words, it simulates a user which is using the web page. +This binding uses the same API as the Web-Browser-Based Alexa site (alexa.amazon.de). +In other words, it simulates a user which is using the web page. Unfortunately, the binding can get broken if Amazon change the web site. The binding is tested with amazon.de and amazon.co.uk accounts, but should also work with all others. ## Warning ## -For the connection to the Amazon server, your password of the Amazon account is required, this will be stored in your openHAB thing device configuration. So you should be sure, that nobody other has access to your configuration! +For the connection to the Amazon server, your password of the Amazon account is required, this will be stored in your openHAB thing device configuration. +So you should be sure, that nobody other has access to your configuration! ## What else you should know ## -All the display options are updated by polling the amazon server. The polling time can be configured, but a minimum of 10 seconds is required. The default is 60 seconds, which means the it can take up to 60 seconds to see the correct state. I do not know, if there is a limit implemented in the amazon server if the polling is too fast and maybe amazon will lock your account. 60 seconds seems to be safe. +All the display options are updated by polling the amazon server. +The polling time can be configured, but a minimum of 10 seconds is required. +The default is 60 seconds, which means the it can take up to 60 seconds to see the correct state. +I do not know, if there is a limit implemented in the amazon server if the polling is too fast and maybe amazon will lock your account. 60 seconds seems to be safe. ## Supported Things @@ -64,11 +69,14 @@ All the display options are updated by polling the amazon server. The polling ti ## Discovery -The first 'Amazon Account' thing will be automatically discovered. After configuration of the thing with the account data, a 'Amazon ' thing will be discovered for each registered device. If the device type is not known by the binding, an 'Unknown' device will be created. +The first 'Amazon Account' thing will be automatically discovered. +After configuration of the thing with the account data, a 'Amazon ' thing will be discovered for each registered device. +If the device type is not known by the binding, an 'Unknown' device will be created. ## Binding Configuration -The binding does not have any configuration. The configuration of your amazon account must be done in the 'Amazon Account' device. +The binding does not have any configuration. +The configuration of your amazon account must be done in the 'Amazon Account' device. ## Thing Configuration @@ -97,7 +105,8 @@ You will find the serial number in the alexa app. ### Flash Briefing Profile -The flashbriefingprofile thing 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. +The flashbriefingprofile thing 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. ## Channels @@ -154,7 +163,8 @@ You will find the serial number in the Alexa app. ### amzonechocontrol.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. +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. ``` Group Alexa_Living_Room @@ -232,7 +242,7 @@ sitemap amzonechocontrol label="Echo Devices" } ``` -To get instead of the id fields an selection box, use the Selection element and provide mappings for your favorite id's: +To get instead of the id fields an selection box, use the selection element and provide mappings for your favorite id's: ``` Selection item=Echo_Living_Room_RadioStationId mappings=[ ''='Off', 's1139'='Antenne Steiermark', 's8007'='Hitradio Ă–3', 's16793'='Radio 10', 's8235'='FM4' ] @@ -269,7 +279,8 @@ then end ``` -Note 1: Do not use a to short time for playing the sound, because alexa needs some time to start playing the sound. I recommend, that you to not use a time below 10 seconds. +Note 1: Do not use a to short time for playing the sound, because alexa needs some time to start playing the sound. +I recommend, that you to not use a time below 10 seconds. Note 2: The rule have no effect for your default alarm sound used in the alexa app. @@ -297,8 +308,10 @@ Note: I recommend, that you test the command send to play music command first wi ## Credits -The idea for writing this binding came from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German). Thank you Alex! +The idea for writing this binding came from this blog: http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German). +Thank you Alex! ## Trademark Disclaimer -TuneIn, Amazon Echo, Amazon Echo Spot, Amazon Echo Show, Amazon Music, Amazon Prime, Alexa and all other products and Amazon, TuneIn and other companies are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. +TuneIn, Amazon Echo, Amazon Echo Spot, Amazon Echo Show, Amazon Music, Amazon Prime, Alexa and all other products and Amazon, TuneIn and other companies are trademarks™ or registered® trademarks of their respective holders. +Use of them does not imply any affiliation with or endorsement by them. From 90fc5d35e13dea2b790d36d71b3a3ec7afdd3f01 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Fri, 11 May 2018 23:06:30 +0200 Subject: [PATCH 44/56] [amazonechocontrol] fix bug in factory Signed-off-by: Michael Geramb (github: mgeramb) --- .../internal/AmazonEchoControlHandlerFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 99385e1d593b6..13dae0f47d9ee 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -76,7 +76,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return null; } - @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC) + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) protected void setDiscoverService(DiscoveryService discoverService) { if (discoverService instanceof AmazonEchoDiscovery) { amazonEchoDiscovery = (AmazonEchoDiscovery) discoverService; From b7332a81913b9795d4e3794e6becfc2ce59193f4 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sat, 12 May 2018 20:34:43 +0200 Subject: [PATCH 45/56] [amazonechocontrol] Refactor the channels, unkown device removed, Text To Speech channel added, new samples in the readme Signed-off-by: Michael Geramb (github: mgeramb) --- .../ESH-INF/binding/binding.xml | 8 + .../i18n/amazonechocontrol_de.properties | 38 ++- .../ESH-INF/thing/thing-types.xml | 172 +++++-------- .../META-INF/MANIFEST.MF | 1 + .../README.md | 228 ++++++++++-------- .../AmazonEchoControlBindingConstants.java | 25 +- .../handler/AccountHandler.java | 30 +-- .../handler/EchoHandler.java | 182 +++++++++----- .../handler/FlashBriefingProfileHandler.java | 11 +- .../AmazonEchoControlHandlerFactory.java | 57 +++-- .../internal/Connection.java | 54 ++++- .../discovery/AmazonEchoDiscovery.java | 86 +++---- ...onEchoDynamicStateDescriptionProvider.java | 54 ++++- 13 files changed, 532 insertions(+), 414 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml index eee1e3b23af4e..76d3cc998d22e 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml @@ -6,4 +6,12 @@ Binding to control Amazon Echo devices (Alexa). This binding enables openHAB to control the volume, playing state, bluetooth connection of your amazon echo devices. Michael Geramb + + + + Shows ID's in the channel drop downs which needed for setting the value from a rule + false + + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties index 59742abe0a883..b488331982180 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties @@ -73,17 +73,14 @@ channel-type.amazonechocontrol.amazonMusic.description = Amazon Music eingeschal channel-type.amazonechocontrol.amazonMusicPlayListId.label = Amazon Music Playlist ID channel-type.amazonechocontrol.amazonMusicPlayListId.description = ID der Playlist auf Amazon Music (Nur schreiben, kein aktueller Status). Auswahl funktioniert derzeit nur in PaperUI. -channel-type.amazonechocontrol.amazonMusicPlayListIdLastUsed.label = Amazon Music letzte gestartete Playlist ID -channel-type.amazonechocontrol.amazonMusicPlayListIdLastUsed.description = Zuletzt über openHAB gestartete Amazon Music Playlist - channel-type.amazonechocontrol.providerDisplayName.label = Anbieter Name channel-type.amazonechocontrol.providerDisplayName.description = Name des Musikanbieters -channel-type.amazonechocontrol.bluetoothId.label = Bluetooth Verbindung -channel-type.amazonechocontrol.bluetoothId.description = MAC-Adresse des verbundenen Bluetoothgerätes +channel-type.amazonechocontrol.bluetoothMAC.label = Bluetooth Verbindung +channel-type.amazonechocontrol.bluetoothMAC.description = MAC-Adresse des verbundenen Bluetoothgerätes -channel-type.amazonechocontrol.bluetoothIdSelection.label = Bluetooth Verbindungungsauswahl -channel-type.amazonechocontrol.bluetoothIdSelection.description = Bluetooth Verbindungungsauswahl (Derzeit nur in PaperUI) +channel-type.amazonechocontrol.textToSpeech.label = Sprich +channel-type.amazonechocontrol.textToSpeech.description = Spricht den Text (Nur schreiben) channel-type.amazonechocontrol.remind.label = Erinnere channel-type.amazonechocontrol.remind.description = Spricht die Erinnerung und sendet eine Benachrichtigung an die Alexa-APP (Nur schreiben) @@ -91,25 +88,13 @@ channel-type.amazonechocontrol.remind.description = Spricht die Erinnerung und s channel-type.amazonechocontrol.playAlarmSound.label = Spielt Alarm Sound channel-type.amazonechocontrol.playAlarmSound.description = Spielt Alarm Sound ab (Nur schreiben) -channel-type.amazonechocontrol.playFlashBriefing.label = Tägliche Zusammenfassung -channel-type.amazonechocontrol.playFlashBriefing.description = Startet die tägliche Zusammenfassung (Nur schreiben) - -channel-type.amazonechocontrol.playWeatherReport.label = Wetterbericht -channel-type.amazonechocontrol.playWeatherReport.description = Startet den Wetterbericht (Nur schreiben) - -channel-type.amazonechocontrol.playTrafficNews.label = Verkehrsnachrichten -channel-type.amazonechocontrol.playTrafficNews.description = Started die Verkehrsnachrichten (Nur schreiben) - -channel-type.amazonechocontrol.playGoodMorning.label = Guten Morgen -channel-type.amazonechocontrol.playGoodMorning.description = Started die Guten Morgen Nachricht (Nur schreiben) - channel-type.amazonechocontrol.startRoutine.label = Started eine Routine channel-type.amazonechocontrol.startRoutine.description = Tippen sie ein, was Sie normalerweise zu Alexa sagen um eine Routine zu starten, ohne "Alexa" vorangestellt (Nur schreiben) -channel-type.amazonechocontrol.playMusicProvider.label = Musikanbieter -channel-type.amazonechocontrol.playMusicProvider.description = Musikanbieter der für 'Starte Musik Sprachbefehl' verwendet wird (Nur schreiben) +channel-type.amazonechocontrol.musicProviderId.label = Musikanbieter +channel-type.amazonechocontrol.musicProviderId.description = Musikanbieter -channel-type.amazonechocontrol.playMusicVoiceCommand.label = Starte Musik Sprachbefehl +channel-type.amazonechocontrol.playMusicVoiceCommand.label = Musik Sprachbefehl channel-type.amazonechocontrol.playMusicVoiceCommand.description = Sprachbefehl als Text. Z.B. Yesterday von Beatles (Nur schreiben) channel-type.amazonechocontrol.imageUrl.label = Bild URL @@ -150,3 +135,12 @@ channel-type.amazonechocontrol.active.description = Aktiviert diese t channel-type.amazonechocontrol.playOnDevice.label = Wiedergabe am Gerät channel-type.amazonechocontrol.playOnDevice.description = Started die Wiedergabe am Gerät (Seriennummer oder Name, nur schreiben) + +channel-type.amazonechocontrol.playCommand.label = Started +channel-type.amazonechocontrol.playCommand.description = Started Information (Nur schreiben) +channel-type.amazonechocontrol.playCommand.option.Weather = Wetter +channel-type.amazonechocontrol.playCommand.option.Traffic = Verkehr +channel-type.amazonechocontrol.playCommand.option.GoodMorning = Guten Morgen +channel-type.amazonechocontrol.playCommand.option.SingASong = Lied +channel-type.amazonechocontrol.playCommand.option.TellStory = Geschichte +channel-type.amazonechocontrol.playCommand.option.TellStory = Zusammenfassung diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 832a114f6d030..6b8e4643a3038 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -32,8 +32,8 @@ Enter the password of the amazon account which is used for the amazon echo devices. IMPORTANT: If the account thing does not go online and reports and login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. - - 60 + + 30 Refresh state interval in seconds. Lower time causes more network traffic. Seconds @@ -55,25 +55,21 @@ - - - - + + + - + - - - - - - + + + serialNumber @@ -98,25 +94,21 @@ - - - - + + + - + - - - - - - + + + serialNumber @@ -141,25 +133,21 @@ - - - - + + + - + - - - - - - + + + serialNumber @@ -187,51 +175,7 @@ - - - - serialNumber - - - - You will find the serial number of your device in the Alexa app - - - - - - - - - Unknown Echo Device. Warning: Maybe not all channels will be supported from the device - - - - - - - - - - - - - - - - - - - - - - - - - - - serialNumber @@ -241,6 +185,7 @@ + @@ -263,7 +208,7 @@ Activate this flash briefing configuration - + String Plays the briefing on the device (serial number or name, write only) @@ -284,26 +229,6 @@ Speak the reminder and send a notification to the Alexa app - - Switch - - Starts the flash briefing (Write Only) - - - Switch - - Starts the weather report (Write Only) - - - Switch - - Starts the traffic news (Write Only) - - - Switch - - Starts good morning news (Write Only) - String @@ -319,12 +244,12 @@ Id of the amazon music track - + Switch Amazon Music turned on - + String Amazon Music play list id (Write only, no current state) @@ -334,22 +259,17 @@ Id of the playlist which was started with openHAB - + String Name of music provider - + String MAC-Address of the bluetooth connected device - - String - - Bluetooth connection selection (Currently only in PaperUI) - String @@ -362,24 +282,24 @@ Title - + String Subtitle 1 - + String Subtitle 2 - + Switch Radio turned on - + Switch Connect to last used device @@ -404,14 +324,34 @@ Volume of the sound - + String - Music provider used for 'Start music voice command' (Write only) + Music provider String - + Voice command as text. E.g. 'Yesterday from the Beatles' (Write only) + + String + + Speak the text (Write only) + + + String + + Start information (Write only) + + + + + + + + + + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF index bb6c935559107..5b5e997d80963 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF +++ b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF @@ -27,6 +27,7 @@ Import-Package: com.google.gson;resolution:=optional, org.openhab.binding.amazonechocontrol, org.openhab.binding.amazonechocontrol.handler, org.osgi.framework, + org.osgi.service.component;version="1.3.0", org.osgi.service.component.annotations;resolution:=optional, org.osgi.service.http, org.slf4j diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 31e8fa161da69..c5645074460c5 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -4,6 +4,7 @@ This binding can control Amazon Echo devices (Alexa). It provides features to control and view the current state of echo devices: +- use echo device as text to speech from a rule - volume - pause/continue/next track/previous track - connect/disconnect bluetooth devices @@ -32,6 +33,7 @@ Some ideas what you can do in your home by using rules and other openHAB control - Start a routine which switch a smart home device connected to alexa - Start your briefing if you turn on the light first time in the morning - Have different flash briefing in the morning and evening +- Let alexa say 'welcome' to you if you open the door ## Note ## @@ -39,7 +41,7 @@ This binding uses the same API as the Web-Browser-Based Alexa site (alexa.amazon In other words, it simulates a user which is using the web page. Unfortunately, the binding can get broken if Amazon change the web site. -The binding is tested with amazon.de and amazon.co.uk accounts, but should also work with all others. +The binding is tested with amazon.de, amazon.com and amazon.co.uk accounts, but should also work with all others. ## Warning ## @@ -63,15 +65,15 @@ I do not know, if there is a limit implemented in the amazon server if the polli | echoshow | Amazon Echo Show Device | | wha | Amazon Echo Whole House Audio Control | | flashbriefingprofile | Flash briefing profile | -| unknown | Unknown Echo Device or App\* | - -\* The unknown device will provide all channels, but maybe not all of them supported by your device. ## Discovery The first 'Amazon Account' thing will be automatically discovered. After configuration of the thing with the account data, a 'Amazon ' thing will be discovered for each registered device. -If the device type is not known by the binding, an 'Unknown' device will be created. +If the device type is not known by the binding, the device will not be discovered. +But you can define any device listed in your alexa app with the best matching existing device (e.g. echo). +You will find the required serial number in settings of the device in the alexa app. + ## Binding Configuration @@ -87,15 +89,15 @@ The Amazon Account thing need the following configurations: | amazonSite | The amazon site where the echos are registered. e.g. amazon.de | | email | Email of your amazon account | | password | Password of your amazon account | -| pollingIntervalInSeconds | Polling interval for the device state in seconds. Default 60, minimum 10 | +| pollingIntervalInSeconds | Polling interval for the device state in seconds. Default 30, minimum 10 | 2 factor authentication is not supported! -** HINT ** IMPORTANT: If the Account thing does not go online and reports a login error, open the url YOUR_OPENHAB/amazonechocontrol/ID_OF_ACCOUNT_THING (Replace YOUR_OPENHAB and ID_OF_ACCOUNT_THING with your configuration) in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. +** HINT ** IMPORTANT: If the Account thing does not go online and reports a login error, open the url YOUR_OPENHAB/amazonechocontrol/ID_OF_ACCOUNT_THING (Replace YOUR_OPENHAB and ID_OF_ACCOUNT_THING with your configuration) in your browser (e.g. http://openhab:8080/amazonechocontrol/account1) and try to login. ### Amazon Devices -All Amazon devices (echo, echospot, echoshow, wha, unknown) needs the following configurations: +All Amazon devices (echo, echospot, echoshow, wha) needs the following configurations: | Configuration name | Description | |--------------------------|----------------------------------------------------| @@ -110,38 +112,33 @@ It will be configured at runtime by using the save channel to store the current ## Channels -| Channel Type ID | Item Type | Access Mode | Thing Type | Description -|---------------------|-----------|-------------|------------|------------------------------------------------------------------------------------------ -| player | Player | R/W | echo, echoshow, echospot, wha, unknown | Control the music player e.g. pause/continue/next track/previous track -| volume | Dimmer | R/W | echo, echoshow, echospot, unknown | Control the volume -| shuffle | Switch | R/W | echo, echoshow, echospot, wha, unknown | Shuffle play if applicable, e.g. playing a playlist -| imageUrl | String | R | echo, echoshow, echospot, wha, unknown | Url of the album image or radio station logo -| title | String | R | echo, echoshow, echospot, wha, unknown | Title of the current media -| subtitle1 | String | R | echo, echoshow, echospot, wha, unknown | Subtitle of the current media -| subtitle2 | String | R | echo, echoshow, echospot, wha, unknown | Additional subtitle of the current media -| providerDisplayName | String | R | echo, echoshow, echospot, wha, unknown | Name of the music provider -| bluetoothId | String | R/W | echo, echoshow, echospot, unknown | Bluetooth device id. Used to connect to a specific device or disconnect if a empty string was provided -| bluetoothIdSelection| String | R/W | echo, echoshow, echospot, unknown | Bluetooth device selection. The selection currently only works in PaperUI -| bluetooth | Switch | R/W | echo, echoshow, echospot, unknown | 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, unknown | User friendly name of the connected bluetooth device -| radioStationId | String | R/W | echo, echoshow, echospot, wha, unknown | Start playing of a TuneIn radio station by specifying it's id or stops playing if a empty string was provided -| radio | Switch | R/W | echo, echoshow, echospot, wha, unknown | 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, unknown | Start playing of a Amazon Music track by it's id od stops playing if a empty string was provided -| amazonMusicPlayListId | String | W | echo, echoshow, echospot, wha, unknown | Write Only! Start playing of a Amazon Music playlist by specifying it's id od stops playing if a empty string was provided. Selection will only work in PaperUI -| amazonMusicPlayListIdLastUsed | String | R | echo, echoshow, echospot, wha, unknown | The last play list id started from openHAB -| amazonMusic | Switch | R/W | echo, echoshow, echospot, wha, unknown | 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, unknown | Write Only! Speak the reminder and sends a notification to the Alexa app (Currently the reminder is played and notified two times, this seems to be a bug in the amazon software) -| playAlarmSound | String | R/W | echo, echoshow, echospot, unknown | Write Only! Plays an alarm sound. In PaperUI will be a selection box available. For rules use the value shown in the square brackets -| playFlashBriefing | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the flash briefing -| playWeatherReport | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the weather report -| playTrafficNews | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the traffic news -| playGoodMorning | Switch | W | echo, echoshow, echospot, unknown | Write Only! Starts the good moring report -| startRoutine | Switch | W | echo, echoshow, echospot, unknown | Write Only! Type in what you normally say to Alexa without the preceding "Alexa," -| playMusicProvider | String | W | echo, echoshow, echospot, unknown | Write Only! Music provider used for 'Start music voice command' -| playMusicVoiceCommand | String | W | echo, echoshow, echospot, unknown | Write Only! Voice command as text. E.g. 'Yesterday from the Beatles' -| save | Switch | W | flashbriefingprofile | Write Only! 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. +| Channel Type ID | Item Type | Access Mode | Thing Type | Description +|-----------------------|-----------|-------------|------------|------------------------------------------------------------------------------------------ +| player | Player | R/W | echo, echoshow, echospot, wha | Control the music player e.g. pause/continue/next track/previous track +| volume | Dimmer | R/W | echo, echoshow, echospot | Control the volume +| 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 a 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 it's id or stops playing if a 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 a Amazon Music track by it's id od stops playing if a empty string was provided +| amazonMusicPlayListId | String | W | echo, echoshow, echospot, wha | Write Only! Start playing of a Amazon Music playlist by specifying it's id od stops playing if a empty string was provided. Selection will only work in PaperUI +| 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 | Write Only! Speak the reminder and sends a notification to the Alexa app (Currently the reminder is played and notified two times, this seems to be a bug in the amazon software) +| startRoutine | Switch | W | echo, echoshow, echospot | Write Only! Type in what you normally say to Alexa without the preceding "Alexa," +| musicProviderId | String | R/W | echo, echoshow, echospot | Current Music provider +| playMusicVoiceCommand | String | W | echo, echoshow, echospot | Write Only! Voice command as text. E.g. 'Yesterday from the Beatles' +| startCommand | String | W | echo, echoshow, echospot | Write Only! Used to start anything. Available options: Weather, Traffic, GoodMorning, SingASong, TellStory, FlashBriefing and FlashBriefing. (Note: The options are case sensitive) +| textToSpeech | String | W | echo, echoshow, echospot | Write Only! Write some text to this channel and alexa will speak it +| save | Switch | W | flashbriefingprofile | Write Only! 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. ## Full Example @@ -150,12 +147,12 @@ It will be configured at runtime by using the save channel to store the current ``` Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" [amazonSite="amazon.de", email="myaccountemail@myprovider.com", password="secure", pollingIntervalInSeconds=60] { - Thing echo echo1 "Alexa" @ "Living Room" [serialNumber="SERIAL_NUMBER"] - Thing echoshow echo2 "Alexa" @ "Kitchen" [serialNumber="SERIAL_NUMBER"] - Thing echospot echo3 "Alexa" @ "Sleeping Room" [serialNumber="SERIAL_NUMBER"] - Thing wha echo4 "Alexa" @ "Ground Floor Music Group" [serialNumber="SERIAL_NUMBER"] - Thing unknown echo5 "Alexa" @ "Very new echo device" [serialNumber="SERIAL_NUMBER"] - Thing flashbriefingprofile flashbriefing1 "Flash Briefing" @ "Flash Briefings" + Thing echo echo1 "Alexa" @ "Living Room" [serialNumber="SERIAL_NUMBER"] + Thing echoshow echoshow1 "Alexa" @ "Kitchen" [serialNumber="SERIAL_NUMBER"] + Thing echospot echospot1 "Alexa" @ "Sleeping Room" [serialNumber="SERIAL_NUMBER"] + Thing wha wha1 "Ground Floor Music Group" @ "Music Groups" [serialNumber="SERIAL_NUMBER"] + Thing flashbriefingprofile flashbriefing1 "Flash Briefing Technical" @ "Flash Briefings" + Thing flashbriefingprofile flashbriefing2 "Flash Briefing Life Style" @ "Flash Briefings" } ``` @@ -169,37 +166,51 @@ Take a look in the channel description above to know, which channels are support ``` Group Alexa_Living_Room +// Player control Player Echo_Living_Room_Player "Player" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:player"} Dimmer Echo_Living_Room_Volume "Volume [%.0f %%]" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:volume"} Switch Echo_Living_Room_Shuffle "Shuffle" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:shuffle"} + +// 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_BluetoothId "Bluetooth Mac Address" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothId"} -String Echo_Living_Room_BluetoothId_Selection "Bluetooth Device" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothId"} -Switch Echo_Living_Room_Bluetooth "Bluetooth" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetooth"} -String Echo_Living_Room_BluetoothDeviceName "Bluetooth Device" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothDeviceName"} + +// 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 (Write Only)" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusicPlayListId"} -String Echo_Living_Room_AmazonMusicPlayListIdLastUsed "Amazon Music Playlist Id last used" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:amazonMusicPlayListIdLastUsed"} +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"} + +// Bluetooth +String Echo_Living_Room_BluetoothMAC "Bluetooth MAC Address" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothMAC"} +Switch Echo_Living_Room_Bluetooth "Bluetooth" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetooth"} +String Echo_Living_Room_BluetoothDeviceName "Bluetooth Device" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:bluetoothDeviceName"} + +// Commands +String Echo_Living_Room_TTS "Text to Speech" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:textToSpeech"} 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"} -Switch Echo_Living_Room_PlayFlashBriefing "Play Flash Briefing" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playFlashBriefing"} -Switch Echo_Living_Room_PlayWeatherReport "Play Weather Report" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playWeatherReport"} -Switch Echo_Living_Room_PlayTrafficNews "Play Traffic News" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playTrafficNews"} -Switch Echo_Living_Room_PlayGoodMoring "Play Good Morning News" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playGoodMorning"} String Echo_Living_Room_StartRoutine "Start Routine" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:startRoutine"} -String Echo_Living_Room_PlayMusicProvider "Music Provider (Write Only)" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playMusicProvider"} -String Echo_Living_Room_PlayMusicCommand "Play music voice command (Write Only)" (Alexa_Living_Room) {channel="amazonechocontrol:echo:account1:echo1:playMusicVoiceCommand"} +// 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"} ``` ### amzonechocontrol.sitemap: @@ -208,57 +219,86 @@ String FlashBriefing_Technical_Play "Play (Write only)" { channel="amazonechocon sitemap amzonechocontrol label="Echo Devices" { Frame label="Alexa" { - Default item=Echo_Living_Room_Player - Slider item=Echo_Living_Room_Volume - Switch item=Echo_Living_Room_Shuffle - Text item=Echo_Living_Room_Title - Text item=Echo_Living_Room_Subtitle1 - Text item=Echo_Living_Room_Subtitle2 - Text item=Echo_Living_Room_ProviderDisplayName - Text item=Echo_Living_Room_BluetoothId_Selection - Text item=Echo_Living_Room_BluetoothId - Switch item=Echo_Living_Room_Bluetooth - Text item=Echo_Living_Room_BluetoothDeviceName + Default item=Echo_Living_Room_Player + Slider item=Echo_Living_Room_Volume + Switch item=Echo_Living_Room_Shuffle + Image item=Echo_Living_Room_ImageUrl label="" + Text item=Echo_Living_Room_Title + Text item=Echo_Living_Room_Subtitle1 + Text item=Echo_Living_Room_Subtitle2 + Text item=Echo_Living_Room_ProviderDisplayName + + // The listed providers are only samples, you could have more + Selection item=Echo_Living_Room_MusicProviderId mappings=[ 'TUNEIN'='Radio', 'SPOTIFY'='Spotify', 'AMAZON_MUSIC'='Amazon Music', 'CLOUDPLAYER'='Amazon'] + Text item=Echo_Living_Room_MusicProviderId + + // To start one of your flashbriefings use Flashbriefing. + Selection item=Echo_Living_Room_StartCommand mappings=[ 'Weather'='Weather', 'Traffic'='Traffic', 'GoodMorning'='Good Morning', 'SingASong'='Song', 'TellStory'='Story', 'FlashBriefing'='Flash Briefing', 'FlashBriefing.flashbriefing1'='Technical', 'FlashBriefing.flashbriefing2'='Life Style' ] + + Selection item=Echo_Living_Room_RadioStationId mappings=[ ''='Off', 's1139'='Antenne Steiermark', 's8007'='Hitradio Ă–3', 's16793'='Radio 10', 's8235'='FM4' ] Text item=Echo_Living_Room_RadioStationId Switch item=Echo_Living_Room_Radio + Text item=Echo_Living_Room_AmazonMusicTrackId Text item=Echo_Living_Room_AmazonMusicPlayListId - Text item=Echo_Living_Room_AmazonMusicPlayListIdLastUsed Switch item=Echo_Living_Room_AmazonMusic - Text item=Echo_Living_Room_Remind - Text item=Echo_Living_Room_PlayAlarmSound - Switch item=Echo_Living_Room_PlayFlashBriefing - Switch item=Echo_Living_Room_PlayWeatherReport - Switch item=Echo_Living_Room_PlayTrafficNews - Switch item=Echo_Living_Room_PlayGoodMoring - Text item=Echo_Living_Room_StartRoutine + + Text item=Echo_Living_Room_BluetoothMAC + // Change the Place holder with the MAC address shown, if alexa is connected to the device + Selection item=Echo_Living_Room_BluetoothMAC mappings=[ ''='Disconnected', ''='Bluetooth Device 1', ''='Bluetooth Device 2'] + + // These are only view of the possible options. Enable ShowIDsInGUI in the binding configuration and look in drop-down-box of this channel in the paper UI Control section + Selection item=Echo_Living_Room_PlayAlarmSound mappings=[ ''='None', 'ECHO:system_alerts_soothing_01'='Adrift', 'ECHO:system_alerts_atonal_02'='Clangy'] + + Switch item=Echo_Living_Room_Bluetooth + Text item=Echo_Living_Room_BluetoothDeviceName } - Frame label="Flash Briefing 1" { + Frame label="Flash Briefing Technical" { Switch item=FlashBriefing_Technical_Save Switch item=FlashBriefing_Technical_Active Text item=FlashBriefing_Technical_Play } + + Frame label="Flash Briefing Life Style" { + Switch item=FlashBriefing_LifeStyle_Save + Switch item=FlashBriefing_LifeStyle_Active + Text item=FlashBriefing_LifeStyle_Play + } } ``` -To get instead of the id fields an selection box, use the selection element and provide mappings for your favorite id's: +## How To Get ID's +Simple way to get the ID's required by the Selection element or an rule: -``` - Selection item=Echo_Living_Room_RadioStationId mappings=[ ''='Off', 's1139'='Antenne Steiermark', 's8007'='Hitradio Ă–3', 's16793'='Radio 10', 's8235'='FM4' ] -``` +1) Open the paper UI +2) Navigate to the Configuration / Bindings section +3) Click on the edit button (Pencil) of the Amazon Echo Control Binding +4) Enable the 'Show ID's in the GUI' option and save it +5) Navigate to the Control section +6) Most of the channels which requires a ID show now a drop-down with the ID within []-brackets. +If there are no drop downs, check if you have defined the channel and sometimes a browser refresh helps. ## Tutorials -**Playing an alarm sound for 15 seconds with an openHAB rule if an door contact was opened:** +**Let alexa speak a text from a rule:** -1) Open the Paper UI -2) Navigate to the Control Section -3) Open the Drop-Down of the 'Alarm Sound' channel -4) Select the Sound you want to here -5) Write down the text in the square brackets. e.g. ECHO:system_alerts_repetitive01 for the nightstand sound -6) Create a rule for start playing the sound: +1) Create a rule with a trigger of your choice +```php +rule "Say welcome if the door opens" +when + Item Door_Contact changed to OPEN +then + Echo_Living_Room_TTS.sendCommand('Hello World') +end +``` + +**Playing an alarm sound for 15 seconds with an openHAB rule if an door contact was opened:** + +1) Do get the ID of your sound, follow the steps in "How To Get ID's" +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: ```php var Timer stopAlarmTimer = null @@ -286,13 +326,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) Open the Paper UI -2) Navigate to the Control Section -3) Open the Drop-Down of the 'Music provider for the start music voice command' channel -4) Select the Provider you want to use -5) Write down the text in the square brackets. e.g. SPOTIFY for the spotify music provider -6) 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 ID's" +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: ```php rule "Play a playlist on spotify if a switch was changed" diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java index 4cf3c2e0dbdfb..d930b53b02f82 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java @@ -33,14 +33,13 @@ 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_UNKNOWN = new ThingTypeUID(BINDING_ID, "unknown"); public static final ThingTypeUID THING_TYPE_FLASH_BRIEFING_PROFILE = new ThingTypeUID(BINDING_ID, "flashbriefingprofile"); public static final Set SUPPORTED_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_UNKNOWN, THING_TYPE_FLASH_BRIEFING_PROFILE)); + THING_TYPE_ECHO_WHA, THING_TYPE_FLASH_BRIEFING_PROFILE)); // List of all Channel ids public static final String CHANNEL_PLAYER = "player"; @@ -53,8 +52,7 @@ public class AmazonEchoControlBindingConstants { public static final String CHANNEL_SUBTITLE1 = "subtitle1"; public static final String CHANNEL_SUBTITLE2 = "subtitle2"; public static final String CHANNEL_PROVIDER_DISPLAY_NAME = "providerDisplayName"; - public static final String CHANNEL_BLUETOOTH_ID = "bluetoothId"; - public static final String CHANNEL_BLUETOOTH_ID_SELECTION = "bluetoothIdSelection"; + 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"; @@ -62,35 +60,34 @@ public class AmazonEchoControlBindingConstants { 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_AMAZON_MUSIC_PLAY_LIST_ID_LAST_USED = "amazonMusicPlayListIdLastUsed"; + public static final String CHANNEL_TEXT_TO_SPEECH = "textToSpeech"; public static final String CHANNEL_REMIND = "remind"; public static final String CHANNEL_PLAY_ALARM_SOUND = "playAlarmSound"; - public static final String CHANNEL_PLAY_FLASH_BRIEFING = "playFlashBriefing"; - public static final String CHANNEL_PLAY_WEATER_REPORT = "playWeatherReport"; - public static final String CHANNEL_PLAY_TRAFFIC_NEWS = "playTrafficNews"; - public static final String CHANNEL_PLAY_GOOD_MORNING = "playGoodMorning"; public static final String CHANNEL_START_ROUTINE = "startRoutine"; - public static final String CHANNEL_PLAY_MUSIC_PROVIDER = "playMusicProvider"; + public static final String CHANNEL_MUSIC_PROVIDER_ID = "musicProviderId"; public static final String CHANNEL_PLAY_MUSIC_VOICE_COMMAND = "playMusicVoiceCommand"; + public static final String CHANNEL_START_COMMAND = "startCommand"; 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_ID_SELECTION = new ChannelTypeUID(BINDING_ID, - "bluetoothIdSelection"); + 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_PLAY_MUSIC_PROVIDER = new ChannelTypeUID(BINDING_ID, - "playMusicProvider"); + 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_FLASH_BRIEFING_PROFILE = "configurationJson"; + // Other + public static final String FLASH_BRIEFING_COMMAND_PREFIX = "FlashBriefing."; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index 37d5ebf314cc5..d3b6802e8a9e0 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -105,8 +105,7 @@ public void initialize() { } Integer pollingIntervalInSeconds = config.pollingIntervalInSeconds; if (pollingIntervalInSeconds == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Polling interval not configured"); - return; + pollingIntervalInSeconds = 30; } if (pollingIntervalInSeconds < 10) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, @@ -140,6 +139,10 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } + public List getFlashBriefingProfileHandlers() { + return new ArrayList<>(this.flashBriefingProfileHandlers); + } + public List getLastKnownDevices() { return new ArrayList<>(jsonSerialNumberDeviceMapping.values()); } @@ -205,10 +208,6 @@ public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) { synchronized (echoHandlers) { echoHandlers.remove(childHandler); } - AmazonEchoDiscovery instance = AmazonEchoDiscovery.instance; - if (instance != null) { - instance.removeExistingEchoHandler(childThing.getUID()); - } } // check for flash briefing profile handler @@ -445,7 +444,6 @@ public void updateDeviceList(boolean manualScan) { if (currentConnection == null) { return; } - AmazonEchoDiscovery discoveryService = AmazonEchoDiscovery.instance; List devices = null; try { @@ -464,10 +462,7 @@ public void updateDeviceList(boolean manualScan) { } } jsonSerialNumberDeviceMapping = newJsonSerialDeviceMapping; - - if (discoveryService != null) { - discoveryService.setDevices(getThing().getUID(), devices); - } + amazonEchoDiscovery.setDevices(getThing().getUID(), devices); } synchronized (echoHandlers) { for (EchoHandler child : echoHandlers) { @@ -512,14 +507,11 @@ private void updateFlashBriefingHandlers(Connection currentConnection) { if (flashBriefingProfileHandlers.isEmpty()) { discoverFlashProfiles = true; // discover at least one device } - AmazonEchoDiscovery discoveryService = AmazonEchoDiscovery.instance; - if (discoveryService != null) { - if (discoverFlashProfiles) { - discoverFlashProfiles = false; - if (!flashBriefingProfileFound) { - discoveryService.discoverFlashBriefingProfiles(getThing().getUID(), - this.currentFlashBriefingJson); - } + if (discoverFlashProfiles) { + discoverFlashProfiles = false; + if (!flashBriefingProfileFound) { + amazonEchoDiscovery.discoverFlashBriefingProfiles(getThing().getUID(), + this.currentFlashBriefingJson, this.flashBriefingProfileHandlers.size() + 1); } } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 05d4a7d76973a..809eb921ce9d8 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -29,6 +29,7 @@ import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; @@ -64,23 +65,30 @@ public class EchoHandler extends BaseThingHandler { private @Nullable Device device; private @Nullable Connection connection; + private @Nullable AccountHandler account; private @Nullable ScheduledFuture updateStateJob; private @Nullable String lastKnownRadioStationId; - private @Nullable String lastKnownBluetoothId; + private @Nullable String lastKnownBluetoothMAC; private @Nullable String lastKnownAmazonMusicId; - private String musicProviderId = ""; + private String musicProviderId = "TUNEIN"; + private boolean isPlaying = false; + private boolean isPaused = false; private int lastKnownVolume = 25; private @Nullable BluetoothState bluetoothState; private boolean disableUpdate = false; private boolean updateRemind = true; + private boolean updateTextToSpeech = true; private boolean updateAlarm = true; private boolean updateRoutine = true; private boolean updatePlayMusicVoiceCommand = true; + private boolean updateStartCommand = true; + private boolean showIdsInGUI = false; private @Nullable JsonNotificationResponse currentNotification; private @Nullable ScheduledFuture currentNotifcationUpdateTimer; - public EchoHandler(Thing thing) { + public EchoHandler(Thing thing, boolean showIdsInGUI) { super(thing); + this.showIdsInGUI = showIdsInGUI; } @Override @@ -95,6 +103,7 @@ public void initialize() { if (bridge != null) { AccountHandler account = (AccountHandler) bridge.getHandler(); if (account != null) { + this.account = account; account.addEchoHandler(this); } } @@ -118,6 +127,10 @@ public void dispose() { super.dispose(); } + public boolean getShowIdsInGUI() { + return this.showIdsInGUI; + } + public @Nullable BluetoothState findBluetoothState() { return this.bluetoothState; } @@ -126,6 +139,10 @@ public void dispose() { return this.connection; } + public @Nullable AccountHandler findAccount() { + return this.account; + } + public @Nullable Device findDevice() { return this.device; } @@ -143,7 +160,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { try { int waitForUpdate = 1000; boolean needBluetoothRefresh = false; - String lastKnownBluetoothId = this.lastKnownBluetoothId; + String lastKnownBluetoothMAC = this.lastKnownBluetoothMAC; ScheduledFuture updateStateJob = this.updateStateJob; this.updateStateJob = null; @@ -166,7 +183,12 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command == PlayPauseType.PAUSE || command == OnOffType.OFF) { connection.command(device, "{\"type\":\"PauseCommand\"}"); } else if (command == PlayPauseType.PLAY || command == OnOffType.ON) { - connection.command(device, "{\"type\":\"PlayCommand\"}"); + if (isPaused) { + connection.command(device, "{\"type\":\"PlayCommand\"}"); + } else { + connection.playMusicVoiceCommand(device, this.musicProviderId, "!"); + waitForUpdate = 3000; + } } else if (command == NextPreviousType.NEXT) { connection.command(device, "{\"type\":\"NextCommand\"}"); } else if (command == NextPreviousType.PREVIOUS) { @@ -217,10 +239,18 @@ public void handleCommand(ChannelUID channelUID, Command command) { } // play music command - if (channelId.equals(CHANNEL_PLAY_MUSIC_PROVIDER)) { + if (channelId.equals(CHANNEL_MUSIC_PROVIDER_ID)) { if (command instanceof StringType) { - this.musicProviderId = ((StringType) command).toFullString(); waitForUpdate = 0; + String musicProviderId = ((StringType) command).toFullString(); + if (!StringUtils.equals(musicProviderId, this.musicProviderId)) { + this.musicProviderId = musicProviderId; + if (this.isPlaying) { + connection.playMusicVoiceCommand(device, this.musicProviderId, "!"); + waitForUpdate = 3000; + } + } + } } if (channelId.equals(CHANNEL_PLAY_MUSIC_VOICE_COMMAND)) { @@ -235,7 +265,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } // bluetooth commands - if (channelId.equals(CHANNEL_BLUETOOTH_ID) || channelId.equals(CHANNEL_BLUETOOTH_ID_SELECTION)) { + if (channelId.equals(CHANNEL_BLUETOOTH_MAC)) { needBluetoothRefresh = true; if (command instanceof StringType) { String address = ((StringType) command).toFullString(); @@ -249,7 +279,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { needBluetoothRefresh = true; if (command == OnOffType.ON) { waitForUpdate = 4000; - String bluetoothId = lastKnownBluetoothId; + String bluetoothId = lastKnownBluetoothMAC; BluetoothState state = bluetoothState; if (state != null && (StringUtils.isEmpty(bluetoothId))) { PairedDevice[] pairedDeviceList = state.pairedDeviceList; @@ -259,14 +289,14 @@ public void handleCommand(ChannelUID channelUID, Command command) { continue; } if (StringUtils.isNotEmpty(paired.address)) { - lastKnownBluetoothId = paired.address; + lastKnownBluetoothMAC = paired.address; break; } } } } - if (lastKnownBluetoothId != null && !lastKnownBluetoothId.isEmpty()) { - connection.bluetooth(device, lastKnownBluetoothId); + if (StringUtils.isNotEmpty(lastKnownBluetoothMAC)) { + connection.bluetooth(device, lastKnownBluetoothMAC); } } else if (command == OnOffType.OFF) { connection.bluetooth(device, null); @@ -293,7 +323,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { String playListId = ((StringType) command).toFullString(); if (StringUtils.isNotEmpty(playListId)) { waitForUpdate = 3000; - updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID_LAST_USED, new StringType(playListId)); } connection.playAmazonMusicPlayList(device, playListId); @@ -375,32 +404,47 @@ public void handleCommand(ChannelUID channelUID, Command command) { } // routine commands - if (channelId.equals(CHANNEL_PLAY_FLASH_BRIEFING)) { - - if (command == OnOffType.ON) { - waitForUpdate = 1000; - connection.executeSequenceCommand(device, "Alexa.FlashBriefing.Play"); - } - } - if (channelId.equals(CHANNEL_PLAY_TRAFFIC_NEWS)) { - - if (command == OnOffType.ON) { - waitForUpdate = 1000; - connection.executeSequenceCommand(device, "Alexa.Traffic.Play"); - } - } - if (channelId.equals(CHANNEL_PLAY_WEATER_REPORT)) { - - if (command == OnOffType.ON) { - waitForUpdate = 1000; - connection.executeSequenceCommand(device, "Alexa.Weather.Play"); + if (channelId.equals(CHANNEL_TEXT_TO_SPEECH)) { + if (command instanceof StringType) { + String text = ((StringType) command).toFullString(); + if (StringUtils.isNotEmpty(text)) { + waitForUpdate = 1000; + updateTextToSpeech = true; + connection.textToSpeech(device, text); + } } } - if (channelId.equals(CHANNEL_PLAY_GOOD_MORNING)) { - - if (command == OnOffType.ON) { - waitForUpdate = 1000; - connection.executeSequenceCommand(device, "Alexa.GoodMorning.Play"); + if (channelId.equals(CHANNEL_START_COMMAND)) { + if (command instanceof StringType) { + String commandText = ((StringType) command).toFullString(); + if (StringUtils.isNotEmpty(commandText)) { + updateStartCommand = true; + if (commandText.startsWith(FLASH_BRIEFING_COMMAND_PREFIX)) { + // Handle custom flashbriefings commands + String flashbriefing = commandText.substring(FLASH_BRIEFING_COMMAND_PREFIX.length()); + + AccountHandler account = this.account; + if (account != null) { + for (FlashBriefingProfileHandler flashBriefing : account + .getFlashBriefingProfileHandlers()) { + ThingUID flashBriefingId = flashBriefing.getThing().getUID(); + if (StringUtils.equals(flashBriefing.getThing().getUID().getId(), flashbriefing)) { + flashBriefing.handleCommand( + new ChannelUID(flashBriefingId, CHANNEL_PLAY_ON_DEVICE), + new StringType(device.serialNumber)); + break; + } + } + } + } else { + // Handle standard commands + if (!commandText.startsWith("Alexa.")) { + commandText = "Alexa." + commandText + ".Play"; + } + waitForUpdate = 1000; + connection.executeSequenceCommand(device, commandText, null); + } + } } } if (channelId.equals(CHANNEL_START_ROUTINE)) { @@ -553,16 +597,41 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } catch (IOException | URISyntaxException e) { logger.info("getMediaState fails: {}", e); } - // check playing - boolean playing = playerInfo != null && StringUtils.equals(playerInfo.state, "PLAYING"); + isPlaying = (playerInfo != null && StringUtils.equals(playerInfo.state, "PLAYING")) + || (mediaState != null && StringUtils.equals(mediaState.currentState, "PLAYING")); + + isPaused = (playerInfo != null && StringUtils.equals(playerInfo.state, "PAUSED")) + || (mediaState != null && StringUtils.equals(mediaState.currentState, "PAUSED")); + // handle music provider id + + if (provider != null && isPlaying) { + String musicProviderId; + if (mediaState != null && StringUtils.equals(mediaState.currentState, "PLAYING")) { + musicProviderId = mediaState.providerId; + } else { + musicProviderId = provider.providerName; + } + // Map the music provider id to the one used for starting music with voice command + if (musicProviderId != null) { + musicProviderId = musicProviderId.toUpperCase(); + } + if (StringUtils.equals(musicProviderId, "CLOUD_PLAYER")) { + musicProviderId = "AMAZON_MUSIC"; + } + if (StringUtils.equals(musicProviderId, "TUNE_IN")) { + musicProviderId = "TUNEIN"; + } + if (musicProviderId != null) { + this.musicProviderId = musicProviderId; + } + } // handle amazon music String amazonMusicTrackId = ""; String amazonMusicPlayListId = ""; boolean amazonMusic = false; - if (mediaState != null && StringUtils.equals(mediaState.currentState, "PLAYING") - && StringUtils.equals(mediaState.providerId, "CLOUD_PLAYER") + if (mediaState != null && isPlaying && StringUtils.equals(mediaState.providerId, "CLOUD_PLAYER") && StringUtils.isNotEmpty(mediaState.contentId)) { amazonMusicTrackId = mediaState.contentId; lastKnownAmazonMusicId = amazonMusicTrackId; @@ -570,7 +639,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } // handle bluetooth - String bluetoothId = ""; + String bluetoothMAC = ""; String bluetoothDeviceName = ""; boolean bluetoothIsConnected = false; if (bluetoothState != null) { @@ -583,7 +652,7 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } if (paired.connected && paired.address != null) { bluetoothIsConnected = true; - bluetoothId = paired.address; + bluetoothMAC = paired.address; bluetoothDeviceName = paired.friendlyName; if (StringUtils.isEmpty(bluetoothDeviceName)) { bluetoothDeviceName = paired.address; @@ -593,8 +662,8 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto } } } - if (StringUtils.isNotEmpty(bluetoothId)) { - lastKnownBluetoothId = bluetoothId; + if (StringUtils.isNotEmpty(bluetoothMAC)) { + lastKnownBluetoothMAC = bluetoothMAC; } // handle radio @@ -693,31 +762,34 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto updateRoutine = false; updateState(CHANNEL_START_ROUTINE, new StringType("")); } + if (updateTextToSpeech) { + updateTextToSpeech = false; + updateState(CHANNEL_TEXT_TO_SPEECH, new StringType("")); + } if (updatePlayMusicVoiceCommand) { updatePlayMusicVoiceCommand = false; updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, new StringType("")); } - updateState(CHANNEL_PLAY_MUSIC_PROVIDER, new StringType(musicProviderId)); - updateState(CHANNEL_PLAY_FLASH_BRIEFING, OnOffType.OFF); - updateState(CHANNEL_PLAY_WEATER_REPORT, OnOffType.OFF); - updateState(CHANNEL_PLAY_TRAFFIC_NEWS, OnOffType.OFF); - updateState(CHANNEL_PLAY_GOOD_MORNING, OnOffType.OFF); + if (updateStartCommand) { + updateStartCommand = false; + updateState(CHANNEL_START_COMMAND, new StringType("")); + } + updateState(CHANNEL_MUSIC_PROVIDER_ID, new StringType(musicProviderId)); updateState(CHANNEL_AMAZON_MUSIC_TRACK_ID, new StringType(amazonMusicTrackId)); - updateState(CHANNEL_AMAZON_MUSIC, playing && amazonMusic ? OnOffType.ON : OnOffType.OFF); + updateState(CHANNEL_AMAZON_MUSIC, isPlaying && amazonMusic ? OnOffType.ON : OnOffType.OFF); updateState(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID, new StringType(amazonMusicPlayListId)); updateState(CHANNEL_RADIO_STATION_ID, new StringType(radioStationId)); - updateState(CHANNEL_RADIO, playing && isRadio ? OnOffType.ON : OnOffType.OFF); + updateState(CHANNEL_RADIO, isPlaying && isRadio ? OnOffType.ON : OnOffType.OFF); updateState(CHANNEL_VOLUME, volume != null ? new PercentType(volume) : UnDefType.UNDEF); updateState(CHANNEL_PROVIDER_DISPLAY_NAME, new StringType(providerDisplayName)); - updateState(CHANNEL_PLAYER, playing ? PlayPauseType.PLAY : PlayPauseType.PAUSE); + updateState(CHANNEL_PLAYER, isPlaying ? PlayPauseType.PLAY : PlayPauseType.PAUSE); updateState(CHANNEL_IMAGE_URL, new StringType(imageUrl)); updateState(CHANNEL_TITLE, new StringType(title)); updateState(CHANNEL_SUBTITLE1, new StringType(subTitle1)); updateState(CHANNEL_SUBTITLE2, new StringType(subTitle2)); if (bluetoothState != null) { updateState(CHANNEL_BLUETOOTH, bluetoothIsConnected ? OnOffType.ON : OnOffType.OFF); - updateState(CHANNEL_BLUETOOTH_ID, new StringType(bluetoothId)); - updateState(CHANNEL_BLUETOOTH_ID_SELECTION, new StringType(bluetoothId)); + updateState(CHANNEL_BLUETOOTH_MAC, new StringType(bluetoothMAC)); updateState(CHANNEL_BLUETOOTH_DEVICE_NAME, new StringType(bluetoothDeviceName)); } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java index 833dfc26eaaa0..d0e10fa04152f 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java @@ -50,9 +50,11 @@ public class FlashBriefingProfileHandler extends BaseThingHandler { boolean updatePlayOnDevice = true; String currentConfigurationJson = ""; private @Nullable ScheduledFuture updateStateJob; + private final AmazonEchoDiscovery amazonEchoDiscovery; - public FlashBriefingProfileHandler(Thing thing) { + public FlashBriefingProfileHandler(Thing thing, AmazonEchoDiscovery amazonEchoDiscovery) { super(thing); + this.amazonEchoDiscovery = amazonEchoDiscovery; stateStorage = new StateStorage(thing); } @@ -140,7 +142,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { logger.warn("Connection for '{}' not found", accountHandler.getThing().getUID().getId()); } else { - connection.executeSequenceCommand(device, "Alexa.FlashBriefing.Play"); + connection.executeSequenceCommand(device, "Alexa.FlashBriefing.Play", null); scheduler.schedule(() -> accountHandler.setEnabledFlashBriefingsJson(old), 1000, TimeUnit.MILLISECONDS); @@ -203,9 +205,6 @@ private String saveCurrentProfile(AccountHandler connection) { } private void removeFromDiscovery() { - AmazonEchoDiscovery instance = AmazonEchoDiscovery.instance; - if (instance != null) { - instance.removeExistingFlashBriefingProfile(this.currentConfigurationJson); - } + amazonEchoDiscovery.removeExistingFlashBriefingProfile(this.currentConfigurationJson); } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 13dae0f47d9ee..c978dafe1e630 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -10,6 +10,8 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; +import java.util.Hashtable; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.config.discovery.DiscoveryService; @@ -23,6 +25,8 @@ import org.openhab.binding.amazonechocontrol.handler.EchoHandler; import org.openhab.binding.amazonechocontrol.handler.FlashBriefingProfileHandler; import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; @@ -41,15 +45,42 @@ public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { @Nullable HttpService httpService; - @Nullable AmazonEchoDiscovery amazonEchoDiscovery; + boolean showIdsInGUI; + @Nullable + ServiceRegistration discoverServiceRegistration; + @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); } + @Override + protected void activate(ComponentContext componentContext) { + super.activate(componentContext); + Object configShowIdsInGui = componentContext.getProperties().get("showIdsInGUI"); + showIdsInGUI = (configShowIdsInGui instanceof Boolean) ? (Boolean) configShowIdsInGui : false; + + } + + @Override + protected void deactivate(ComponentContext componentContext) { + super.deactivate(componentContext); + AmazonEchoDiscovery amazonEchoDiscovery = this.amazonEchoDiscovery; + if (amazonEchoDiscovery != null) { + amazonEchoDiscovery.deactivate(); + } + this.amazonEchoDiscovery = null; + @Nullable + ServiceRegistration discoverServiceRegistration = this.discoverServiceRegistration; + if (discoverServiceRegistration != null) { + discoverServiceRegistration.unregister(); + this.discoverServiceRegistration = null; + } + } + @Override protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); @@ -60,7 +91,12 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } AmazonEchoDiscovery amazonEchoDiscovery = this.amazonEchoDiscovery; if (amazonEchoDiscovery == null) { - return null; + amazonEchoDiscovery = new AmazonEchoDiscovery(); + discoverServiceRegistration = bundleContext.registerService(DiscoveryService.class.getName(), + amazonEchoDiscovery, new Hashtable()); + amazonEchoDiscovery.activate(); + this.amazonEchoDiscovery = amazonEchoDiscovery; + } if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { @@ -68,27 +104,14 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return bridgeHandler; } if (thingTypeUID.equals(THING_TYPE_FLASH_BRIEFING_PROFILE)) { - return new FlashBriefingProfileHandler(thing); + return new FlashBriefingProfileHandler(thing, amazonEchoDiscovery); } if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new EchoHandler(thing); + return new EchoHandler(thing, showIdsInGUI); } return null; } - @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) - protected void setDiscoverService(DiscoveryService discoverService) { - if (discoverService instanceof AmazonEchoDiscovery) { - amazonEchoDiscovery = (AmazonEchoDiscovery) discoverService; - } - } - - protected void unsetDiscoverService(DiscoveryService discoverService) { - if (discoverService == amazonEchoDiscovery) { - amazonEchoDiscovery = null; - } - } - @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC) protected void setHttpService(HttpService httpService) { this.httpService = httpService; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index f73f2a9d3031c..7433d9339ed00 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.Date; import java.util.HashMap; +import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Scanner; @@ -603,7 +604,6 @@ public void command(Device device, String command) throws IOException, URISyntax String url = alexaServer + "/api/np/command?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType; makeRequest("POST", url, command, true, true, null); - } public void bluetooth(Device device, @Nullable String address) throws IOException, URISyntaxException { @@ -658,14 +658,52 @@ public void playAmazonMusicPlayList(Device device, @Nullable String playListId) } } + public void textToSpeech(Device device, String text) throws IOException, URISyntaxException { + Map parameters = new Hashtable(); + parameters.put("textToSpeak", text); + executeSequenceCommand(device, "Alexa.Speak", parameters); + } + // commands: Alexa.Weather.Play, Alexa.Traffic.Play, Alexa.FlashBriefing.Play, Alexa.GoodMorning.Play, - // Alexa.SingASong.Play, Alexa.TellStory.Play - public void executeSequenceCommand(Device device, String command) throws IOException, URISyntaxException { - String json = "{ \"behaviorId\": \"amzn1.alexa.automation.00000000-0000-0000-0000-000000000000\", " - + " \"sequenceJson\": \"{\\\"@type\\\":\\\"com.amazon.alexa.behaviors.model.Sequence\\\",\\\"startNode\\\":{\\\"@type\\\":\\\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\\\",\\\"type\\\":\\\"" - + command + "\\\",\\\"operationPayload\\\":{\\\"deviceType\\\":\\\"" + device.deviceType - + "\\\",\\\"deviceSerialNumber\\\":\\\"" + device.serialNumber + "\\\",\\\"customerId\\\":\\\"" - + device.deviceOwnerCustomerId + "\\\",\\\"locale\\\":\\\"\\\"}}}\",\n" + " \"status\": \"ENABLED\" }"; + // Alexa.SingASong.Play, Alexa.TellStory.Play, Alexa.Speak (textToSpeach) + public void executeSequenceCommand(Device device, String command, @Nullable Map parameters) + throws IOException, URISyntaxException { + Gson gson = new Gson(); + + JsonObject operationPayload = new JsonObject(); + operationPayload.addProperty("deviceType", device.deviceType); + operationPayload.addProperty("deviceSerialNumber", device.serialNumber); + operationPayload.addProperty("locale", ""); + operationPayload.addProperty("customerId", device.deviceOwnerCustomerId); + if (parameters != null) { + for (String key : parameters.keySet()) { + Object value = parameters.get(key); + if (value instanceof String) { + operationPayload.addProperty(key, (String) value); + } else if (value instanceof Number) { + operationPayload.addProperty(key, (Number) value); + } else if (value instanceof Boolean) { + operationPayload.addProperty(key, (Boolean) value); + } else if (value instanceof Character) { + operationPayload.addProperty(key, (Character) value); + } else { + operationPayload.add(key, gson.toJsonTree(value)); + } + } + } + + JsonObject startNode = new JsonObject(); + startNode.addProperty("@type", "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"); + startNode.addProperty("type", command); + startNode.add("operationPayload", operationPayload); + + JsonObject sequenceJson = new JsonObject(); + sequenceJson.addProperty("@type", "com.amazon.alexa.behaviors.model.Sequence"); + sequenceJson.add("startNode", startNode); + + JsonStartRoutineRequest request = new JsonStartRoutineRequest(); + request.sequenceJson = gson.toJson(sequenceJson); + String json = gson.toJson(request); makeRequest("POST", alexaServer + "/api/behaviors/preview", json, true, true, null); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 3c50f8ba00d2a..4cc3bcc05489c 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -11,8 +11,8 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; import java.util.Date; -import java.util.HashMap; import java.util.HashSet; +import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; @@ -25,16 +25,12 @@ import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; -import org.eclipse.smarthome.config.discovery.DiscoveryService; -import org.eclipse.smarthome.core.thing.ThingRegistry; +import org.eclipse.smarthome.config.discovery.DiscoveryServiceCallback; +import org.eclipse.smarthome.config.discovery.ExtendedDiscoveryService; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; 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.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,29 +41,23 @@ * @author Michael Geramb - Initial contribution */ @NonNullByDefault -@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.amazonechocontrol") -public class AmazonEchoDiscovery extends AbstractDiscoveryService { +public class AmazonEchoDiscovery extends AbstractDiscoveryService implements ExtendedDiscoveryService { - private @Nullable ThingRegistry thingRegistry; private boolean discoverAccount = true; private final Set discoveryServices = new HashSet<>(); - public @Nullable static AmazonEchoDiscovery instance; private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); - private final Map lastDeviceInformations = new HashMap<>(); private final HashSet discoverdFlashBriefings = new HashSet(); @Nullable ScheduledFuture startScanStateJob; long activateTimeStamp; - @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC) - protected void setThingRegistry(ThingRegistry thingRegistry) { - this.thingRegistry = thingRegistry; - } + private @Nullable DiscoveryServiceCallback discoveryServiceCallback; - protected void unsetThingRegistry(ThingRegistry thingRegistry) { - this.thingRegistry = thingRegistry; + @Override + public void setDiscoveryServiceCallback(DiscoveryServiceCallback discoveryServiceCallback) { + this.discoveryServiceCallback = discoveryServiceCallback; } public void resetDiscoverAccount() { @@ -90,6 +80,10 @@ public AmazonEchoDiscovery() { super(SUPPORTED_THING_TYPES_UIDS, 10); } + public void activate() { + super.activate(new Hashtable()); + } + @Override public void deactivate() { super.deactivate(); @@ -131,14 +125,12 @@ void startScan(boolean manual) { @Override protected void startBackgroundDiscovery() { - AmazonEchoDiscovery.instance = this; stopScanJob(); startScanStateJob = scheduler.schedule(this::startAutomaticScan, 3000, TimeUnit.MILLISECONDS); } @Override protected void stopBackgroundDiscovery() { - AmazonEchoDiscovery.instance = null; stopScanJob(); } @@ -162,19 +154,15 @@ public void activate(@Nullable Map config) { }; public synchronized void setDevices(ThingUID brigdeThingUID, List deviceList) { - ThingRegistry thingRegistry = this.thingRegistry; - if (thingRegistry == null) { + DiscoveryServiceCallback discoveryServiceCallback = this.discoveryServiceCallback; + if (discoveryServiceCallback == null) { return; } - - Set toRemove = new HashSet(lastDeviceInformations.keySet()); for (Device device : deviceList) { String serialNumber = device.serialNumber; if (serialNumber != null) { - boolean alreadyfound = toRemove.remove(serialNumber); - // new String deviceFamily = device.deviceFamily; - if (!alreadyfound && deviceFamily != null) { + if (deviceFamily != null) { ThingTypeUID thingTypeId; if (deviceFamily.equals("ECHO")) { thingTypeId = THING_TYPE_ECHO; @@ -185,30 +173,34 @@ public synchronized void setDevices(ThingUID brigdeThingUID, List device } else if (deviceFamily.equals("WHA")) { thingTypeId = THING_TYPE_ECHO_WHA; } else { - thingTypeId = THING_TYPE_UNKNOWN; + logger.debug("Unknown thing type '{}'", deviceFamily); + continue; } ThingUID thingUID = new ThingUID(thingTypeId, brigdeThingUID, serialNumber); - // Check if already created - if (thingRegistry.get(thingUID) == null) { - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel(device.accountName) - .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) - .withProperty(DEVICE_PROPERTY_FAMILY, deviceFamily) - .withRepresentationProperty(DEVICE_PROPERTY_SERIAL_NUMBER).withBridge(brigdeThingUID) - .build(); - - logger.debug("Device [{}: {}] found. Mapped to thing type {}", device.deviceFamily, - serialNumber, thingTypeId.getAsString()); - - thingDiscovered(result); - lastDeviceInformations.put(serialNumber, thingUID); + if (discoveryServiceCallback.getExistingDiscoveryResult(thingUID) != null) { + continue; + } + if (discoveryServiceCallback.getExistingThing(thingUID) != null) { + continue; } + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel(device.accountName) + .withProperty(DEVICE_PROPERTY_SERIAL_NUMBER, serialNumber) + .withProperty(DEVICE_PROPERTY_FAMILY, deviceFamily) + .withRepresentationProperty(DEVICE_PROPERTY_SERIAL_NUMBER).withBridge(brigdeThingUID) + .build(); + + logger.debug("Device [{}: {}] found. Mapped to thing type {}", device.deviceFamily, serialNumber, + thingTypeId.getAsString()); + + thingDiscovered(result); } } } } - public synchronized void discoverFlashBriefingProfiles(ThingUID brigdeThingUID, String currentFlashBriefingJson) { + public synchronized void discoverFlashBriefingProfiles(ThingUID brigdeThingUID, String currentFlashBriefingJson, + int number) { if (currentFlashBriefingJson.isEmpty()) { return; } @@ -221,7 +213,7 @@ public synchronized void discoverFlashBriefingProfiles(ThingUID brigdeThingUID, String id = UUID.randomUUID().toString(); ThingUID thingUID = new ThingUID(THING_TYPE_FLASH_BRIEFING_PROFILE, brigdeThingUID, id); - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("FlashBriefing") + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("FlashBriefing " + number) .withProperty(DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE, currentFlashBriefingJson) .withBridge(brigdeThingUID).build(); logger.debug("Flash Briefing {} discovered", currentFlashBriefingJson); @@ -231,14 +223,6 @@ public synchronized void discoverFlashBriefingProfiles(ThingUID brigdeThingUID, } } - public synchronized void removeExistingEchoHandler(ThingUID uid) { - for (String id : lastDeviceInformations.keySet()) { - if (lastDeviceInformations.get(id).equals(uid)) { - lastDeviceInformations.remove(id); - } - } - } - public synchronized void removeExistingFlashBriefingProfile(@Nullable String currentFlashBriefingJson) { if (currentFlashBriefingJson != null) { discoverdFlashBriefings.remove(currentFlashBriefingJson); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java index 11b5794f11637..c8ab164f1456a 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -80,6 +80,14 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { return thing.getHandler(); } + StateOption CreateStateOption(@Nullable String id, @Nullable String displayValue, boolean showIdsInGUI) { + if (showIdsInGUI) { + return new StateOption(id, String.format("%s [%s]", displayValue, id)); + } else { + return new StateOption(id, displayValue); + } + } + @Override public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { @@ -90,7 +98,7 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { if (thingRegistry == null) { return originalStateDescription; } - if (CHANNEL_TYPE_BLUETHOOTH_ID_SELECTION.equals(channel.getChannelTypeUID())) { + if (CHANNEL_TYPE_BLUETHOOTH_MAC.equals(channel.getChannelTypeUID())) { EchoHandler handler = (EchoHandler) findHandler(channel); if (handler == null) { return originalStateDescription; @@ -111,7 +119,7 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { continue; } if (device.address != null && device.friendlyName != null) { - options.add(new StateOption(device.address, device.friendlyName)); + options.add(CreateStateOption(device.address, device.friendlyName, handler.getShowIdsInGUI())); } } StateDescription result = new StateDescription(originalStateDescription.getMinimum(), @@ -148,8 +156,9 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { if (innerLists != null && innerLists.length > 0) { PlayList playList = innerLists[0]; if (playList.playlistId != null && playList.title != null) { - options.add(new StateOption(playList.playlistId, - String.format("%s [%d]", playList.title, playList.trackCount))); + options.add(CreateStateOption(playList.playlistId, + String.format("%s (%d)", playList.title, playList.trackCount), + handler.getShowIdsInGUI())); } } } @@ -183,13 +192,11 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { 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, - String.format("%s [%s]", notificationSound.displayName, providerSoundId))); - + options.add(CreateStateOption(providerSoundId, notificationSound.displayName, + handler.getShowIdsInGUI())); } } StateDescription result = new StateDescription(originalStateDescription.getMinimum(), @@ -222,7 +229,7 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { originalStateDescription.getMaximum(), originalStateDescription.getStep(), originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); return result; - } else if (CHANNEL_TYPE_PLAY_MUSIC_PROVIDER.equals(channel.getChannelTypeUID())) { + } else if (CHANNEL_TYPE_MUSIC_PROVIDER_ID.equals(channel.getChannelTypeUID())) { EchoHandler handler = (EchoHandler) findHandler(channel); if (handler == null) { return originalStateDescription; @@ -243,13 +250,40 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { && StringUtils.isNotEmpty(providerId) && StringUtils.equals(musicProvider.availability, "AVAILABLE") && StringUtils.isNotEmpty(displayName)) { - options.add(new StateOption(providerId, String.format("%s [%s]", displayName, providerId))); + options.add(CreateStateOption(providerId, displayName, handler.getShowIdsInGUI())); } } StateDescription result = new StateDescription(originalStateDescription.getMinimum(), originalStateDescription.getMaximum(), originalStateDescription.getStep(), originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); return result; + } else if (CHANNEL_TYPE_START_COMMAND.equals(channel.getChannelTypeUID())) { + EchoHandler handler = (EchoHandler) findHandler(channel); + if (handler == null) { + return originalStateDescription; + } + AccountHandler account = handler.findAccount(); + if (account == null) { + return originalStateDescription; + } + @NonNull + List<@NonNull FlashBriefingProfileHandler> flashbriefings = account.getFlashBriefingProfileHandlers(); + if (flashbriefings.isEmpty()) { + return originalStateDescription; + } + + ArrayList 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(CreateStateOption(value, displayName, handler.getShowIdsInGUI())); + } + StateDescription result = new StateDescription(originalStateDescription.getMinimum(), + originalStateDescription.getMaximum(), originalStateDescription.getStep(), + originalStateDescription.getPattern(), originalStateDescription.isReadOnly(), options); + return result; } return originalStateDescription; } From 5a95a10cc08dd0475f2f6708469280a624f8d1c3 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Fri, 18 May 2018 22:15:22 +0200 Subject: [PATCH 46/56] [amazonechocontrol] Review comments, better error if login fails in the account detail status Signed-off-by: Michael Geramb (github: mgeramb) --- .../.classpath | 19 +++-- .../ESH-INF/binding/binding.xml | 6 +- .../i18n/amazonechocontrol_de.properties | 35 +++------ .../ESH-INF/thing/thing-types.xml | 20 +++-- .../META-INF/MANIFEST.MF | 13 ++-- .../README.md | 69 +++++++++++++----- .../build.properties | 1 + .../lib/jsoup-1.10.1.jar | Bin 0 -> 345536 bytes .../pom.xml | 2 +- .../handler/AccountHandler.java | 15 ++-- .../handler/EchoHandler.java | 26 +++++-- .../internal/Connection.java | 50 ++++++++----- ...onEchoDynamicStateDescriptionProvider.java | 34 +++++---- 13 files changed, 171 insertions(+), 119 deletions(-) create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/lib/jsoup-1.10.1.jar diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/.classpath b/addons/binding/org.openhab.binding.amazonechocontrol/.classpath index a9d178f069e87..d0631ba8106d7 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/.classpath +++ b/addons/binding/org.openhab.binding.amazonechocontrol/.classpath @@ -1,7 +1,12 @@ - - - - - - - + + + + + + + + + + + + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml index 76d3cc998d22e..429a4fd36c2bd 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml @@ -3,13 +3,13 @@ id="amazonechocontrol" xsi:schemaLocation="http://eclipse.org/smarthome/schemas/binding/v1.0.0 http://eclipse.org/smarthome/schemas/binding-1.0.0.xsd"> Amazon Echo Control Binding - Binding to control Amazon Echo devices (Alexa). This binding enables openHAB to control the volume, playing state, bluetooth connection of your amazon echo devices. + Binding to control Amazon Echo devices (Alexa). This binding enables openHAB to control the volume, playing state, bluetooth connection of your amazon echo devices or allow to use it as TTS device. Michael Geramb - - Shows ID's in the channel drop downs which needed for setting the value from a rule + + Shows IDs in the channel drop downs which needed for setting the value from a rule false diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties index b488331982180..0093a1e94138f 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/i18n/amazonechocontrol_de.properties @@ -1,62 +1,51 @@ # binding binding.amazonechocontrol.name = Amazon Echo Steuerung Binding -binding.amazonechocontrol.description = Binding zum Steuern von Amazon Echo (Alexa). Diese Binding ermöglicht openHAB die Lautstärke, den Wiedergabe Status und die Bluetooth-Verbindung deines Amazon Echo Gerätes zu steuern. +binding.amazonechocontrol.description = Binding zum Steuern von Amazon Echo (Alexa). Dieses Binding ermöglicht openHAB die Lautstärke, den Wiedergabe Status und die Bluetooth-Verbindung des Amazon Echo Gerätes zu steuern oder als TTS Gerät zu benutzen. # thing types thing-type.amazonechocontrol.account.label = Amazon Konto -thing-type.amazonechocontrol.account.description = Amazon Konto bei dem dein Amazon Echo registriert ist. Hinweis: 2 Faktor Authentifizierung ist nicht unterstützt. +thing-type.amazonechocontrol.account.description = Amazon Konto bei dem die Amazon Echo Geräte registriert sind. Hinweis: 2 Faktor Authentifizierung ist nicht unterstützt. thing-type.config.amazonechocontrol.account.amazonSite.label = Amazon Seite -thing-type.config.amazonechocontrol.account.amazonSite.description = Wähle oder tippe die Seite ein bei der dein Amazon Konto erstellt wurde. Hinweis: 2 Faktor Authentifizierung ist nicht unterstützt. +thing-type.config.amazonechocontrol.account.amazonSite.description = Amazon Seite bei der das Amazon Konto erstellt wurde. Hinweis: 2 Faktor Authentifizierung ist nicht unterstützt. thing-type.config.amazonechocontrol.account.email.label = Amazon Konto E-Mail -thing-type.config.amazonechocontrol.account.email.description = E-Mail des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde eingeben. +thing-type.config.amazonechocontrol.account.email.description = E-Mail des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde. thing-type.config.amazonechocontrol.account.password.label = Amazon Konto Kennwort -thing-type.config.amazonechocontrol.account.password.description = Kennwort des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde eingeben. WICHTIG: Sollte das Account-Thing nicht Online gehen und einen Login-Fehler melden, öffne die URL YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in deinem Browser (Z.B.: http://openhab:8080/amazonechocontrol/account) und versuche dich anzumelden. +thing-type.config.amazonechocontrol.account.password.description = Kennwort des Amazon Konto welches für die Amazon Echo Geräte verwendet wurde. WICHTIG: Sollte das Account-Thing nicht Online gehen und einen Login-Fehler melden, die URL YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING im Browser (Z.B.: http://openhab:8080/amazonechocontrol/account) öffnen und anmelden versuchen. thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.label = Status-Aktualisierungs-Intervall thing-type.config.amazonechocontrol.account.pollingIntervalInSeconds.description = Aktualtisierungs-Intervall für den Status in Sekunden. Kleinere Zeiten verursachen höheren Netzwerkverkehr. -thing-type.config.amazonechocontrol.account.discoverSmartHomeDevices.label = Sucht Smart Home Geräte -thing-type.config.amazonechocontrol.account.discoverSmartHomeDevices.description = Sucht Smart Home Geräte die über einen Smart Home Alexa Skill verbunden sind. Der openHAB Alexa Skill wird ignoriert. - - thing-type.amazonechocontrol.echo.label = Amazon Echo thing-type.amazonechocontrol.echo.description = Amazon Echo Gerät (Amazon Echo, Amazon Echo Dot, Amazon Echo Plus...) thing-type.config.amazonechocontrol.echo.serialNumber.label = Seriennummer -thing-type.config.amazonechocontrol.echo.serialNumber.description = Die Seriennummer findest du in der Alexa App. +thing-type.config.amazonechocontrol.echo.serialNumber.description = Die Seriennummer des Geräts aus der Alexa App thing-type.amazonechocontrol.echospot.label = Amazon Echo Spot thing-type.amazonechocontrol.echospot.description = Amazon Echo Spot Gerät thing-type.config.amazonechocontrol.echospot.serialNumber.label = Seriennummer -thing-type.config.amazonechocontrol.echospot.serialNumber.description = Die Seriennummer findest du in der Alexa App. +thing-type.config.amazonechocontrol.echospot.serialNumber.description = Die Seriennummer des Geräts aus der Alexa App thing-type.amazonechocontrol.echoshow.label = Amazon Echo Show thing-type.amazonechocontrol.echoshow.description = Amazon Echo Show Gerät thing-type.config.amazonechocontrol.echoshow.serialNumber.label = Seriennummer -thing-type.config.amazonechocontrol.echoshow.serialNumber.description = Die Seriennummer findest du in der Alexa App. +thing-type.config.amazonechocontrol.echoshow.serialNumber.description = Die Seriennummer des Geräts aus der Alexa App thing-type.amazonechocontrol.wha.label = Amazon Multi-Raum Musik thing-type.amazonechocontrol.wha.description = Multi-Raum Musik Steuerung -thing-type.config.amazonechocontrol.wha.serialNumber.label = Seriennummer -thing-type.config.amazonechocontrol.wha.serialNumber.description = Die Seriennummer findest du in der Alexa App. - -thing-type.amazonechocontrol.unknown.label = Unbekanntes Echo Gerät oder unbekannte App -thing-type.amazonechocontrol.unknown.description = Unbekanntes Echo Gerät. Warnung: Möglicherweise werden nicht alle Kanäle vom Gerät unterstützt +thing-type.config.wha.echoshow.serialNumber.label = Seriennummer +thing-type.config.wha.echoshow.serialNumber.description = Die Seriennummer des Geräts aus der Alexa App thing-type.amazonechocontrol.flashbriefingprofile.label = Tägliche Zusammenfassungsprofile thing-type.amazonechocontrol.flashbriefingprofile.description = Speichert und läd eine Tägliches Zusammenfassungskonfiguration - -thing-type.config.amazonechocontrol.unknown.serialNumber.label = Seriennummer -thing-type.config.amazonechocontrol.unknown.serialNumber.description = Die Seriennummer findest du in der Alexa App. - # channel types channel-type.amazonechocontrol.bluetoothDeviceName.label = Bluetooth Gerät channel-type.amazonechocontrol.bluetoothDeviceName.description = Verbundenes Bluetoothgerät @@ -71,7 +60,7 @@ channel-type.amazonechocontrol.amazonMusic.label = Amazon Music channel-type.amazonechocontrol.amazonMusic.description = Amazon Music eingeschaltet channel-type.amazonechocontrol.amazonMusicPlayListId.label = Amazon Music Playlist ID -channel-type.amazonechocontrol.amazonMusicPlayListId.description = ID der Playlist auf Amazon Music (Nur schreiben, kein aktueller Status). Auswahl funktioniert derzeit nur in PaperUI. +channel-type.amazonechocontrol.amazonMusicPlayListId.description = ID der Playlist auf Amazon Music (Nur schreiben, kein aktueller Status). channel-type.amazonechocontrol.providerDisplayName.label = Anbieter Name channel-type.amazonechocontrol.providerDisplayName.description = Name des Musikanbieters @@ -89,7 +78,7 @@ channel-type.amazonechocontrol.playAlarmSound.label = Spielt Alarm Sound channel-type.amazonechocontrol.playAlarmSound.description = Spielt Alarm Sound ab (Nur schreiben) channel-type.amazonechocontrol.startRoutine.label = Started eine Routine -channel-type.amazonechocontrol.startRoutine.description = Tippen sie ein, was Sie normalerweise zu Alexa sagen um eine Routine zu starten, ohne "Alexa" vorangestellt (Nur schreiben) +channel-type.amazonechocontrol.startRoutine.description = Befehl der zu Alexa gesprochen werden muss um eine Routine zu starten, ohne "Alexa" vorangestellt (Nur schreiben) channel-type.amazonechocontrol.musicProviderId.label = Musikanbieter channel-type.amazonechocontrol.musicProviderId.description = Musikanbieter diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml index 6b8e4643a3038..b6757b89b0133 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/thing/thing-types.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="http://eclipse.org/smarthome/schemas/thing-description/v1.0.0 http://eclipse.org/smarthome/schemas/thing-description-1.0.0.xsd"> - Amazon Account where your amazon echo is registered. + Amazon Account where the amazon echo devices are registered. @@ -19,18 +19,16 @@ - Select or type in the site where your amazon account is created. + Site where the amazon account is created. - - Enter the email address of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! + Email address of the amazon account which is used for the amazon echo devices. Hint: 2 factor authentication is not supported! - password - Enter the password of the amazon account which is used for the amazon echo devices. IMPORTANT: If the account thing does not go online and reports and login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. + Password of the amazon account which is used for the amazon echo devices. IMPORTANT: If the account thing does not go online and reports and login error, open the url YOUR_OPENHAP/amazonechocontrol/ID_OF_THIS_THING in your browser (e.g. http://openhab:8080/amazonechocontrol/account) and try to login. 30 @@ -75,7 +73,7 @@ - You will find the serial number of your device in the Alexa app + The serial number of the device from the Alexa app @@ -114,7 +112,7 @@ - You will find the serial number of your device in the Alexa app + The serial number of the device from the Alexa app @@ -153,7 +151,7 @@ - You will find the serial number of your device in the Alexa app + The serial number of the device from the Alexa app @@ -181,7 +179,7 @@ - You will find the serial number of your device in the Alexa app + The serial number of the device from the Alexa app @@ -232,7 +230,7 @@ String - Type in what you normally say to Alexa without the preceding "Alexa," (Write Only) + The command which must be spoken to active the routing without the preceding "Alexa," (Write Only) String diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF index 5b5e997d80963..1b3d5364225f4 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF +++ b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF @@ -1,19 +1,18 @@ Manifest-Version: 1.0 Bundle-ActivationPolicy: lazy -Bundle-ClassPath: . +Bundle-ClassPath: .,lib/jsoup-1.10.1.jar Bundle-ManifestVersion: 2 -Bundle-Name: AmazonEchoControl Binding +Bundle-Name: Amazon Echo Control Binding Bundle-SymbolicName: org.openhab.binding.amazonechocontrol;singleton:=true Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-Vendor: openHAB Bundle-Version: 2.3.0.qualifier -Import-Package: com.google.gson;resolution:=optional, - com.google.gson.annotations, +Import-Package: com.google.gson, javax.servlet, javax.servlet.http, javax.ws.rs.core, org.apache.commons.lang, - org.eclipse.jdt.annotation;resolution:=optional, + org.eclipse.jdt.annotation, org.eclipse.smarthome.config.core, org.eclipse.smarthome.config.discovery, org.eclipse.smarthome.core.cache, @@ -27,8 +26,7 @@ Import-Package: com.google.gson;resolution:=optional, org.openhab.binding.amazonechocontrol, org.openhab.binding.amazonechocontrol.handler, org.osgi.framework, - org.osgi.service.component;version="1.3.0", - org.osgi.service.component.annotations;resolution:=optional, + org.osgi.service.component, org.osgi.service.http, org.slf4j Service-Component: OSGI-INF/*.xml @@ -36,3 +34,4 @@ Export-Package: org.openhab.binding.amazonechocontrol, org.openhab.binding.amazonechocontrol.handler + diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index c5645074460c5..8e70f83743d57 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -35,7 +35,7 @@ Some ideas what you can do in your home by using rules and other openHAB control - Have different flash briefing in the morning and evening - Let alexa say 'welcome' to you if you open the door -## Note ## +## Note This binding uses the same API as the Web-Browser-Based Alexa site (alexa.amazon.de). In other words, it simulates a user which is using the web page. @@ -43,17 +43,17 @@ Unfortunately, the binding can get broken if Amazon change the web site. The binding is tested with amazon.de, amazon.com and amazon.co.uk accounts, but should also work with all others. -## Warning ## +## Warning For the connection to the Amazon server, your password of the Amazon account is required, this will be stored in your openHAB thing device configuration. So you should be sure, that nobody other has access to your configuration! -## What else you should know ## +## What else you should know All the display options are updated by polling the amazon server. The polling time can be configured, but a minimum of 10 seconds is required. The default is 60 seconds, which means the it can take up to 60 seconds to see the correct state. -I do not know, if there is a limit implemented in the amazon server if the polling is too fast and maybe amazon will lock your account. 60 seconds seems to be safe. +It's not know, if there is a limit implemented in the amazon server if the polling is too fast and maybe amazon will lock your account. 30 seconds seems to be safe. ## Supported Things @@ -82,7 +82,7 @@ The configuration of your amazon account must be done in the 'Amazon Account' de ## Thing Configuration -The Amazon Account thing need the following configurations: +The Amazon Account thing needs the following configurations: | Configuration name | Description | |--------------------------|---------------------------------------------------------------------------| @@ -142,7 +142,7 @@ It will be configured at runtime by using the save channel to store the current ## Full Example -### amzonechocontrol.things +### amazonechocontrol.things ``` Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" [amazonSite="amazon.de", email="myaccountemail@myprovider.com", password="secure", pollingIntervalInSeconds=60] @@ -158,7 +158,7 @@ Bridge amazonechocontrol:account:account1 "Amazon Account" @ "Accounts" [amazonS You will find the serial number in the Alexa app. -### amzonechocontrol.items: +### amazonechocontrol.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. @@ -213,7 +213,7 @@ Switch FlashBriefing_LifeStyle_Active "Active" { channel="amazonechocontrol:flas String FlashBriefing_LifeStyle_Play "Play (Write only)" { channel="amazonechocontrol:flashbriefingprofile:account1:flashbriefing2:playOnDevice"} ``` -### amzonechocontrol.sitemap: +### amazonechocontrol.sitemap: ``` sitemap amzonechocontrol label="Echo Devices" @@ -247,7 +247,7 @@ sitemap amzonechocontrol label="Echo Devices" // Change the Place holder with the MAC address shown, if alexa is connected to the device Selection item=Echo_Living_Room_BluetoothMAC mappings=[ ''='Disconnected', ''='Bluetooth Device 1', ''='Bluetooth Device 2'] - // These are only view of the possible options. Enable ShowIDsInGUI in the binding configuration and look in drop-down-box of this channel in the paper UI Control section + // These are only view of the possible options. Enable ShowIDsInGUI in the binding configuration and look in drop-down-box of this channel in the Paper UI Control section Selection item=Echo_Living_Room_PlayAlarmSound mappings=[ ''='None', 'ECHO:system_alerts_soothing_01'='Adrift', 'ECHO:system_alerts_atonal_02'='Clangy'] Switch item=Echo_Living_Room_Bluetooth @@ -268,20 +268,20 @@ sitemap amzonechocontrol label="Echo Devices" } ``` -## How To Get ID's -Simple way to get the ID's required by the Selection element or an rule: +## How To Get IDs +Simple way to get the IDs required by the selection element or an rule: -1) Open the paper UI +1) Open the Paper UI 2) Navigate to the Configuration / Bindings section 3) Click on the edit button (Pencil) of the Amazon Echo Control Binding -4) Enable the 'Show ID's in the GUI' option and save it +4) Enable the 'Show IDs in the GUI' option and save it 5) Navigate to the Control section 6) Most of the channels which requires a ID show now a drop-down with the ID within []-brackets. If there are no drop downs, check if you have defined the channel and sometimes a browser refresh helps. ## Tutorials -**Let alexa speak a text from a rule:** +### Let alexa speak a text from a rule: 1) Create a rule with a trigger of your choice @@ -294,9 +294,9 @@ then end ``` -**Playing an alarm sound for 15 seconds with an openHAB rule if an door contact was opened:** +## Playing an alarm sound for 15 seconds with an openHAB rule if an door contact was opened: -1) Do get the ID of your sound, follow the steps in "How To Get ID's" +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: @@ -320,13 +320,13 @@ end ``` Note 1: Do not use a to short time for playing the sound, because alexa needs some time to start playing the sound. -I recommend, that you to not use a time below 10 seconds. +It's not recommended to use a time below 10 seconds. Note 2: The rule have no effect for your default alarm sound used in the alexa app. -**Play a spotify playlist if a switch was changed to on:** +### 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 ID's" +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: @@ -340,7 +340,36 @@ then end ``` -Note: I recommend, that you test the command send to play music command first with your voice on your alexa device. E.g. say 'Alexa, Playlist Party' +Note: It's recommended to test the command send to play music command first with the voice and the real alexa device. E.g. say 'Alexa, Playlist Party' + +### Start playing weather/traffic/etc: + +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: + +```php +rule "Start wheater info" +when + Item Spotify_Start_Wheater_Switch changed to ON +then + Echo_Living_Room_StartCommand.sendCommand('Weather') +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" +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: + +```php +rule "Start wheater info" +when + Item Spotify_Start_Wheater_Switch changed to ON +then + Echo_Living_Room_StartCommand.sendCommand('FlashBriefing.flashbriefing1') +end +``` ## Credits diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/build.properties b/addons/binding/org.openhab.binding.amazonechocontrol/build.properties index c67911aff5e9e..2b0da48f6f493 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/build.properties +++ b/addons/binding/org.openhab.binding.amazonechocontrol/build.properties @@ -4,4 +4,5 @@ bin.includes=META-INF/,\ .,\ OSGI-INF/,\ ESH-INF/,\ + lib/,\ about.html diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/lib/jsoup-1.10.1.jar b/addons/binding/org.openhab.binding.amazonechocontrol/lib/jsoup-1.10.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..6415d0c030e31ce4a0e563912fc3687b309bc10b GIT binary patch literal 345536 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�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

p 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

2M=SQ& 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.openhab.binding.amazonechocontrol 2.3.0-SNAPSHOT - AmazonEchoControl Binding + Amazon Echo Control Binding eclipse-plugin diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index d3b6802e8a9e0..e97a4e4d6fad6 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -36,6 +36,7 @@ import org.openhab.binding.amazonechocontrol.internal.AccountConfiguration; import org.openhab.binding.amazonechocontrol.internal.Connection; import org.openhab.binding.amazonechocontrol.internal.ConnectionException; +import org.openhab.binding.amazonechocontrol.internal.HttpException; import org.openhab.binding.amazonechocontrol.internal.LoginServlet; import org.openhab.binding.amazonechocontrol.internal.StateStorage; import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; @@ -73,6 +74,7 @@ public class AccountHandler extends BaseBridgeHandler implements IAmazonAccountH private final HttpService httpService; private final AmazonEchoDiscovery amazonEchoDiscovery; private @Nullable LoginServlet loginServlet; + private final Gson gson = new Gson(); public AccountHandler(Bridge bridge, HttpService httpService, AmazonEchoDiscovery amazonEchoDiscovery) { super(bridge); @@ -320,6 +322,9 @@ private void checkLogin() { this.stateStorage.storeState("sessionStorage", serializedStorage); } this.connection = currentConnection; + } catch (ConnectionException e) { + loginIsValid = false; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); } catch (UnknownHostException e) { loginIsValid = false; updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unknown host name '" @@ -338,8 +343,10 @@ private void checkLogin() { } } } - } catch (Exception e) { - logger.error("check login fails {}", e); + } catch (HttpException | JsonSyntaxException | ConnectionException e) { + logger.debug("check login fails {}", e); + } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail. + logger.error("check login fails with unexpected error {}", e); } } @@ -475,7 +482,6 @@ public void updateDeviceList(boolean manualScan) { public void setEnabledFlashBriefingsJson(String flashBriefingJson) { Connection currentConnection = connection; - Gson gson = new Gson(); JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class); if (currentConnection != null) { try { @@ -543,9 +549,8 @@ private void updateFlashBriefingProfiles(Connection currentConnection) { // Do not copy imageUrl here, because it will change forSerializer[i] = copy; } - Gson gson = new Gson(); this.currentFlashBriefingJson = gson.toJson(forSerializer); - } catch (JsonSyntaxException | IOException | URISyntaxException e) { + } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException e) { logger.warn("get flash briefing profiles fails {}", e); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 809eb921ce9d8..69121b47b21f2 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -96,7 +96,7 @@ public void initialize() { logger.debug("Amazon Echo Control Binding initialized"); if (this.connection != null) { - updateStatus(ThingStatus.ONLINE); + setDeviceAndUpdateThingState(this.device); } else { updateStatus(ThingStatus.UNKNOWN); Bridge bridge = this.getBridge(); @@ -112,8 +112,21 @@ public void initialize() { public void intialize(Connection connection, @Nullable Device deviceJson) { this.connection = connection; - this.device = deviceJson; + setDeviceAndUpdateThingState(deviceJson); + } + + boolean setDeviceAndUpdateThingState(@Nullable Device device) { + if (device == null) { + updateStatus(ThingStatus.UNKNOWN); + return false; + } + this.device = device; + if (!device.online) { + updateStatus(ThingStatus.OFFLINE); + return false; + } updateStatus(ThingStatus.ONLINE); + return true; } @Override @@ -543,16 +556,13 @@ public void updateState(@Nullable Device device, @Nullable BluetoothState blueto if (this.disableUpdate) { return; } - if (device == null) { - updateStatus(ThingStatus.UNKNOWN); + if (!setDeviceAndUpdateThingState(device)) { return; } - this.device = device; - if (!device.online) { - updateStatus(ThingStatus.OFFLINE); + if (device == null) { return; } - updateStatus(ThingStatus.ONLINE); + Connection connection = this.connection; if (connection == null) { return; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 7433d9339ed00..42b368b40b27a 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -38,6 +38,9 @@ import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAutomation; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAutomation.Payload; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonAutomation.Trigger; @@ -86,9 +89,11 @@ public class Connection { private @Nullable Date loginTime; private @Nullable Date verifyTime; + private final Gson gson = new Gson(); + private final Gson gsonWithNullSerialization; + public Connection(@Nullable String email, @Nullable String password, @Nullable String amazonSite, @Nullable String accountThingId) { - this.accountThingId = accountThingId != null ? accountThingId : ""; this.email = email != null ? email : ""; this.password = password != null ? password : ""; @@ -108,6 +113,9 @@ public Connection(@Nullable String email, @Nullable String password, @Nullable S } this.amazonSite = correctedAmazonSite; alexaServer = "https://alexa." + this.amazonSite; + + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonWithNullSerialization = gsonBuilder.create(); } public @Nullable Date tryGetLoginTime() { @@ -461,10 +469,23 @@ public void makeLogin() throws IOException, URISyntaxException { String postData = postDataBuilder.toString(); - if (postLoginData(queryParameters, postData) != null) { - throw new ConnectionException( - "Login fails. Check your credentials and try to login with your webbrowser to http(s):///amazonechocontrol/" - + accountThingId); + String response = postLoginData(queryParameters, postData); + if (response != null) { + Document htmlDocument = Jsoup.parse(response); + Element authWarningBoxElement = htmlDocument.getElementById("auth-warning-message-box"); + String error = null; + if (authWarningBoxElement != null) { + error = authWarningBoxElement.text(); + } + if (StringUtils.isNotEmpty(error)) { + throw new ConnectionException( + "Login fails. Check your credentials and try to login with your webbrowser to http(s):///amazonechocontrol/" + + accountThingId + System.lineSeparator() + "" + error); + } else { + throw new ConnectionException( + "Login fails. Check your credentials and try to login with your webbrowser to http(s):///amazonechocontrol/" + + accountThingId); + } } } catch (Exception e) { @@ -507,6 +528,7 @@ public void makeLogin() throws IOException, URISyntaxException { if (response.contains("Amazon Alexa")) { logger.debug("Response seems to be alexa app"); } else { + logger.info("Response maybe not valid"); } @@ -540,7 +562,6 @@ public void logout() { // parser private T parseJson(String json, Class type) throws JsonSyntaxException { try { - Gson gson = new Gson(); return gson.fromJson(json, type); } catch (JsonSyntaxException e) { logger.warn("Parsing json failed {}", e); @@ -558,7 +579,7 @@ public List getDeviceList() throws IOException, URISyntaxException { if (result == null) { return new ArrayList<>(); } - return new ArrayList(Arrays.asList(result)); + return new ArrayList<>(Arrays.asList(result)); } public String getDeviceListJson() throws IOException, URISyntaxException { @@ -668,8 +689,6 @@ public void textToSpeech(Device device, String text) throws IOException, URISynt // Alexa.SingASong.Play, Alexa.TellStory.Play, Alexa.Speak (textToSpeach) public void executeSequenceCommand(Device device, String command, @Nullable Map parameters) throws IOException, URISyntaxException { - Gson gson = new Gson(); - JsonObject operationPayload = new JsonObject(); operationPayload.addProperty("deviceType", device.deviceType); operationPayload.addProperty("deviceSerialNumber", device.serialNumber); @@ -731,7 +750,6 @@ public void startRoutine(Device device, String utterance) throws IOException, UR } } if (found != null) { - Gson gson = new Gson(); String sequenceJson = gson.toJson(found.sequence); JsonStartRoutineRequest request = new JsonStartRoutineRequest(); @@ -792,10 +810,7 @@ public JsonFeed[] getEnabledFlashBriefings() throws IOException, URISyntaxExcept public void setEnabledFlashBriefings(JsonFeed[] enabledFlashBriefing) throws IOException, URISyntaxException { JsonEnabledFeeds enabled = new JsonEnabledFeeds(); enabled.enabledFeeds = enabledFlashBriefing; - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.serializeNulls(); - Gson gson = gsonBuilder.create(); - String json = gson.toJson(enabled); + String json = gsonWithNullSerialization.toJson(enabled); makeRequest("POST", alexaServer + "/api/content-skills/enabled-feeds", json, true, true, null); } @@ -832,11 +847,7 @@ public JsonNotificationResponse notification(Device device, String type, @Nullab request.type = type; request.id = "create" + type; - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.serializeNulls(); - Gson gson = gsonBuilder.create(); - - String data = gson.toJson(request); + String data = gsonWithNullSerialization.toJson(request); String response = makeRequestAndReturnString("PUT", alexaServer + "/api/notifications/createReminder", data, true, null); JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); @@ -881,7 +892,6 @@ public void playMusicVoiceCommand(Device device, String providerId, String voice payload.musicProviderId = providerId; payload.searchPhrase = voiceCommand; - Gson gson = new Gson(); String playloadString = gson.toJson(payload); JsonObject postValidataionJson = new JsonObject(); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java index c8ab164f1456a..dc215f5d6748d 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -10,6 +10,8 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; +import java.io.IOException; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -31,6 +33,8 @@ import org.openhab.binding.amazonechocontrol.handler.EchoHandler; import org.openhab.binding.amazonechocontrol.handler.FlashBriefingProfileHandler; import org.openhab.binding.amazonechocontrol.internal.Connection; +import org.openhab.binding.amazonechocontrol.internal.ConnectionException; +import org.openhab.binding.amazonechocontrol.internal.HttpException; 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; @@ -45,6 +49,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.JsonSyntaxException; + /** * Dynamic channel state description provider. * Overrides the state description for the controls, which receive its configuration in the runtime. @@ -80,7 +86,7 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { return thing.getHandler(); } - StateOption CreateStateOption(@Nullable String id, @Nullable String displayValue, boolean showIdsInGUI) { + StateOption createStateOption(@Nullable String id, @Nullable String displayValue, boolean showIdsInGUI) { if (showIdsInGUI) { return new StateOption(id, String.format("%s [%s]", displayValue, id)); } else { @@ -112,14 +118,14 @@ StateOption CreateStateOption(@Nullable String id, @Nullable String displayValue return originalStateDescription; } - ArrayList options = new ArrayList(); + ArrayList options = new ArrayList<>(); options.add(new StateOption("", "")); for (PairedDevice device : pairedDeviceList) { if (device == null) { continue; } if (device.address != null && device.friendlyName != null) { - options.add(CreateStateOption(device.address, device.friendlyName, handler.getShowIdsInGUI())); + options.add(createStateOption(device.address, device.friendlyName, handler.getShowIdsInGUI())); } } StateDescription result = new StateDescription(originalStateDescription.getMinimum(), @@ -143,11 +149,11 @@ StateOption CreateStateOption(@Nullable String id, @Nullable String displayValue JsonPlaylists playLists; try { playLists = connection.getPlaylists(device); - } catch (Exception e) { + } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException e) { logger.warn("Get playlist failed: {}", e); return originalStateDescription; } - ArrayList options = new ArrayList(); + ArrayList options = new ArrayList<>(); options.add(new StateOption("", "")); @Nullable Map<@NonNull String, @Nullable PlayList @Nullable []> playlistMap = playLists.playlists; @@ -156,7 +162,7 @@ StateOption CreateStateOption(@Nullable String id, @Nullable String displayValue if (innerLists != null && innerLists.length > 0) { PlayList playList = innerLists[0]; if (playList.playlistId != null && playList.title != null) { - options.add(CreateStateOption(playList.playlistId, + options.add(createStateOption(playList.playlistId, String.format("%s (%d)", playList.title, playList.trackCount), handler.getShowIdsInGUI())); } @@ -184,18 +190,18 @@ StateOption CreateStateOption(@Nullable String id, @Nullable String displayValue JsonNotificationSound[] notificationSounds; try { notificationSounds = connection.getNotificationSounds(device); - } catch (Exception e) { + } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException e) { logger.warn("Get notification sounds failed: {}", e); return originalStateDescription; } - ArrayList options = new ArrayList(); + ArrayList 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(CreateStateOption(providerSoundId, notificationSound.displayName, + options.add(createStateOption(providerSoundId, notificationSound.displayName, handler.getShowIdsInGUI())); } } @@ -218,7 +224,7 @@ StateOption CreateStateOption(@Nullable String id, @Nullable String displayValue return originalStateDescription; } - ArrayList options = new ArrayList(); + ArrayList options = new ArrayList<>(); options.add(new StateOption("", "")); for (Device device : devices) { if (device.capabilities != null && Arrays.asList(device.capabilities).contains("FLASH_BRIEFING")) { @@ -240,7 +246,7 @@ StateOption CreateStateOption(@Nullable String id, @Nullable String displayValue } List musicProviders = connection.getMusicProviders(); - ArrayList options = new ArrayList(); + ArrayList options = new ArrayList<>(); for (JsonMusicProvider musicProvider : musicProviders) { @Nullable List<@Nullable String> properties = musicProvider.supportedProperties; @@ -250,7 +256,7 @@ StateOption CreateStateOption(@Nullable String id, @Nullable String displayValue && StringUtils.isNotEmpty(providerId) && StringUtils.equals(musicProvider.availability, "AVAILABLE") && StringUtils.isNotEmpty(displayName)) { - options.add(CreateStateOption(providerId, displayName, handler.getShowIdsInGUI())); + options.add(createStateOption(providerId, displayName, handler.getShowIdsInGUI())); } } StateDescription result = new StateDescription(originalStateDescription.getMinimum(), @@ -272,13 +278,13 @@ StateOption CreateStateOption(@Nullable String id, @Nullable String displayValue return originalStateDescription; } - ArrayList options = new ArrayList(); + ArrayList 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(CreateStateOption(value, displayName, handler.getShowIdsInGUI())); + options.add(createStateOption(value, displayName, handler.getShowIdsInGUI())); } StateDescription result = new StateDescription(originalStateDescription.getMinimum(), originalStateDescription.getMaximum(), originalStateDescription.getStep(), From 4d7c603a8d5a3f8b00f9bf512482a3fb2a9ac228 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Sun, 20 May 2018 17:44:42 +0200 Subject: [PATCH 47/56] [amazonechocontrol] Cleanup discovery. No more account discovery. Signed-off-by: Michael Geramb (github: mgeramb) --- .../META-INF/MANIFEST.MF | 2 +- .../handler/AccountHandler.java | 57 ++++----- .../handler/FlashBriefingProfileHandler.java | 12 +- .../AmazonEchoControlHandlerFactory.java | 57 +++++---- .../internal/Connection.java | 2 +- .../discovery/AmazonEchoDiscovery.java | 108 +++++++++--------- .../discovery/IAmazonAccountHandler.java | 18 --- 7 files changed, 111 insertions(+), 145 deletions(-) delete mode 100755 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonAccountHandler.java diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF index 1b3d5364225f4..954f381292474 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF +++ b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF @@ -12,7 +12,7 @@ Import-Package: com.google.gson, javax.servlet.http, javax.ws.rs.core, org.apache.commons.lang, - org.eclipse.jdt.annotation, + org.eclipse.jdt.annotation;resolution:=optional, org.eclipse.smarthome.config.core, org.eclipse.smarthome.config.discovery, org.eclipse.smarthome.core.cache, diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index e97a4e4d6fad6..ad85c25c00081 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -39,8 +39,6 @@ import org.openhab.binding.amazonechocontrol.internal.HttpException; import org.openhab.binding.amazonechocontrol.internal.LoginServlet; import org.openhab.binding.amazonechocontrol.internal.StateStorage; -import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; -import org.openhab.binding.amazonechocontrol.internal.discovery.IAmazonAccountHandler; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; @@ -58,7 +56,7 @@ * @author Michael Geramb - Initial Contribution */ @NonNullByDefault -public class AccountHandler extends BaseBridgeHandler implements IAmazonAccountHandler { +public class AccountHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(AccountHandler.class); private StateStorage stateStorage; @@ -72,15 +70,12 @@ public class AccountHandler extends BaseBridgeHandler implements IAmazonAccountH private boolean discoverFlashProfiles; private String currentFlashBriefingJson = ""; private final HttpService httpService; - private final AmazonEchoDiscovery amazonEchoDiscovery; private @Nullable LoginServlet loginServlet; private final Gson gson = new Gson(); - public AccountHandler(Bridge bridge, HttpService httpService, AmazonEchoDiscovery amazonEchoDiscovery) { + public AccountHandler(Bridge bridge, HttpService httpService) { super(bridge); this.httpService = httpService; - this.amazonEchoDiscovery = amazonEchoDiscovery; - this.amazonEchoDiscovery.resetDiscoverAccount(); stateStorage = new StateStorage(bridge); } @@ -229,7 +224,6 @@ public void dispose() { loginServlet.dispose(); } this.loginServlet = null; - this.amazonEchoDiscovery.removeAccountHandler(this); cleanup(); super.dispose(); } @@ -351,9 +345,9 @@ private void checkLogin() { } private void handleValidLogin() { - updateDeviceList(false); + updateDeviceList(); + updateFlashBriefingHandlers(); updateStatus(ThingStatus.ONLINE); - this.amazonEchoDiscovery.addAccountHandler(this); } // used to set a valid connection from the web proxy login @@ -383,7 +377,8 @@ private void refreshData() { } // get all devices registered in the account - updateDeviceList(false); + updateDeviceList(); + updateFlashBriefingHandlers(); // update bluetooth states JsonBluetoothStates states = null; @@ -441,15 +436,11 @@ private void refreshData() { return null; } - @Override - public void updateDeviceList(boolean manualScan) { - if (manualScan) { - discoverFlashProfiles = true; - } + public List updateDeviceList() { Connection currentConnection = connection; if (currentConnection == null) { - return; + return new ArrayList(); } List devices = null; @@ -469,15 +460,16 @@ public void updateDeviceList(boolean manualScan) { } } jsonSerialNumberDeviceMapping = newJsonSerialDeviceMapping; - amazonEchoDiscovery.setDevices(getThing().getUID(), devices); } synchronized (echoHandlers) { for (EchoHandler child : echoHandlers) { initializeEchoHandler(child, currentConnection); } } - - updateFlashBriefingHandlers(currentConnection); + if (devices != null) { + return devices; + } + return new ArrayList(); } public void setEnabledFlashBriefingsJson(String flashBriefingJson) { @@ -493,33 +485,32 @@ public void setEnabledFlashBriefingsJson(String flashBriefingJson) { updateFlashBriefingHandlers(); } - public void updateFlashBriefingHandlers() { + public String getNewCurrentFlashbriefingConfiguration() { + discoverFlashProfiles = true; + return updateFlashBriefingHandlers(); + } + + public String updateFlashBriefingHandlers() { Connection currentConnection = connection; if (currentConnection != null) { - updateFlashBriefingHandlers(currentConnection); + return updateFlashBriefingHandlers(currentConnection); } + return ""; } - private void updateFlashBriefingHandlers(Connection currentConnection) { + private String updateFlashBriefingHandlers(Connection currentConnection) { synchronized (flashBriefingProfileHandlers) { if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) { updateFlashBriefingProfiles(currentConnection); } - boolean flashBriefingProfileFound = false; for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) { flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson); } - if (flashBriefingProfileHandlers.isEmpty()) { - discoverFlashProfiles = true; // discover at least one device - } - if (discoverFlashProfiles) { - discoverFlashProfiles = false; - if (!flashBriefingProfileFound) { - amazonEchoDiscovery.discoverFlashBriefingProfiles(getThing().getUID(), - this.currentFlashBriefingJson, this.flashBriefingProfileHandlers.size() + 1); - } + if (flashBriefingProfileFound) { + return ""; } + return this.currentFlashBriefingJson; } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java index d0e10fa04152f..226a4ba022c05 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java @@ -29,7 +29,6 @@ import org.eclipse.smarthome.core.types.RefreshType; import org.openhab.binding.amazonechocontrol.internal.Connection; import org.openhab.binding.amazonechocontrol.internal.StateStorage; -import org.openhab.binding.amazonechocontrol.internal.discovery.AmazonEchoDiscovery; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,11 +49,9 @@ public class FlashBriefingProfileHandler extends BaseThingHandler { boolean updatePlayOnDevice = true; String currentConfigurationJson = ""; private @Nullable ScheduledFuture updateStateJob; - private final AmazonEchoDiscovery amazonEchoDiscovery; - public FlashBriefingProfileHandler(Thing thing, AmazonEchoDiscovery amazonEchoDiscovery) { + public FlashBriefingProfileHandler(Thing thing) { super(thing); - this.amazonEchoDiscovery = amazonEchoDiscovery; stateStorage = new StateStorage(thing); } @@ -87,7 +84,6 @@ public void dispose() { if (updateStateJob != null) { updateStateJob.cancel(false); } - removeFromDiscovery(); super.dispose(); } @@ -175,7 +171,6 @@ public boolean initialize(AccountHandler handler, String currentConfigurationJso if (configurationJson == null || configurationJson.isEmpty()) { this.currentConfigurationJson = saveCurrentProfile(handler); } else { - removeFromDiscovery(); this.currentConfigurationJson = configurationJson; } if (!this.currentConfigurationJson.isEmpty()) { @@ -196,15 +191,10 @@ public boolean initialize(AccountHandler handler, String currentConfigurationJso private String saveCurrentProfile(AccountHandler connection) { String configurationJson = ""; configurationJson = connection.getEnabledFlashBriefingsJson(); - removeFromDiscovery(); this.currentConfigurationJson = configurationJson; if (!configurationJson.isEmpty()) { this.stateStorage.storeState("configurationJson", configurationJson); } return configurationJson; } - - private void removeFromDiscovery() { - amazonEchoDiscovery.removeExistingFlashBriefingProfile(this.currentConfigurationJson); - } } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index c978dafe1e630..2b48701d076cd 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -10,7 +10,9 @@ import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; +import java.util.HashMap; import java.util.Hashtable; +import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -18,6 +20,7 @@ import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; @@ -43,10 +46,10 @@ @NonNullByDefault public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { + private final Map> discoveryServiceRegistrations = new HashMap<>(); + @Nullable HttpService httpService; - @Nullable - AmazonEchoDiscovery amazonEchoDiscovery; boolean showIdsInGUI; @Nullable @@ -68,17 +71,6 @@ protected void activate(ComponentContext componentContext) { @Override protected void deactivate(ComponentContext componentContext) { super.deactivate(componentContext); - AmazonEchoDiscovery amazonEchoDiscovery = this.amazonEchoDiscovery; - if (amazonEchoDiscovery != null) { - amazonEchoDiscovery.deactivate(); - } - this.amazonEchoDiscovery = null; - @Nullable - ServiceRegistration discoverServiceRegistration = this.discoverServiceRegistration; - if (discoverServiceRegistration != null) { - discoverServiceRegistration.unregister(); - this.discoverServiceRegistration = null; - } } @Override @@ -89,22 +81,15 @@ protected void deactivate(ComponentContext componentContext) { if (httpService == null) { return null; } - AmazonEchoDiscovery amazonEchoDiscovery = this.amazonEchoDiscovery; - if (amazonEchoDiscovery == null) { - amazonEchoDiscovery = new AmazonEchoDiscovery(); - discoverServiceRegistration = bundleContext.registerService(DiscoveryService.class.getName(), - amazonEchoDiscovery, new Hashtable()); - amazonEchoDiscovery.activate(); - this.amazonEchoDiscovery = amazonEchoDiscovery; - - } if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { - AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService, amazonEchoDiscovery); + + AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService); + registerDiscoveryService(bridgeHandler); return bridgeHandler; } if (thingTypeUID.equals(THING_TYPE_FLASH_BRIEFING_PROFILE)) { - return new FlashBriefingProfileHandler(thing, amazonEchoDiscovery); + return new FlashBriefingProfileHandler(thing); } if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { return new EchoHandler(thing, showIdsInGUI); @@ -112,6 +97,30 @@ protected void deactivate(ComponentContext componentContext) { return null; } + private synchronized void registerDiscoveryService(AccountHandler bridgeHandler) { + AmazonEchoDiscovery discoveryService = new AmazonEchoDiscovery(bridgeHandler); + discoveryService.activate(); + this.discoveryServiceRegistrations.put(bridgeHandler.getThing().getUID(), bundleContext + .registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable())); + } + + @Override + protected synchronized void removeHandler(ThingHandler thingHandler) { + if (thingHandler instanceof AccountHandler) { + ServiceRegistration serviceReg = this.discoveryServiceRegistrations + .get(thingHandler.getThing().getUID()); + if (serviceReg != null) { + // remove discovery service, if bridge handler is removed + AmazonEchoDiscovery service = (AmazonEchoDiscovery) bundleContext.getService(serviceReg.getReference()); + if (service != null) { + service.deactivate(); + } + serviceReg.unregister(); + discoveryServiceRegistrations.remove(thingHandler.getThing().getUID()); + } + } + } + @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC) protected void setHttpService(HttpService httpService) { this.httpService = httpService; diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 42b368b40b27a..c419c407efc62 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -300,7 +300,7 @@ public HttpsURLConnection makeRequest(String verb, String url, @Nullable String connection = (HttpsURLConnection) new URL(currentUrl).openConnection(); connection.setRequestMethod(verb); connection.setRequestProperty("Accept-Language", "en-US"); - connection.setRequestProperty("User-Agent", "Mozilla/5.0"); + connection.setRequestProperty("User-Agent", "Mozilla/5.0 openHAB/1.0.0.0"); connection.setRequestProperty("DNT", "1"); connection.setRequestProperty("Upgrade-Insecure-Requests", "1"); if (customHeaders != null) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java index 4cc3bcc05489c..5f510be9aa349 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/AmazonEchoDiscovery.java @@ -15,8 +15,6 @@ import java.util.Hashtable; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.UUID; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -29,6 +27,8 @@ import org.eclipse.smarthome.config.discovery.ExtendedDiscoveryService; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; +import org.openhab.binding.amazonechocontrol.handler.AccountHandler; +import org.openhab.binding.amazonechocontrol.internal.Connection; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; import org.osgi.service.component.annotations.Activate; import org.slf4j.Logger; @@ -43,9 +43,7 @@ @NonNullByDefault public class AmazonEchoDiscovery extends AbstractDiscoveryService implements ExtendedDiscoveryService { - private boolean discoverAccount = true; - private final Set discoveryServices = new HashSet<>(); - + AccountHandler accountHandler; private final Logger logger = LoggerFactory.getLogger(AmazonEchoDiscovery.class); private final HashSet discoverdFlashBriefings = new HashSet(); @@ -60,28 +58,13 @@ public void setDiscoveryServiceCallback(DiscoveryServiceCallback discoveryServic this.discoveryServiceCallback = discoveryServiceCallback; } - public void resetDiscoverAccount() { - this.discoverAccount = false; - } - - public void addAccountHandler(IAmazonAccountHandler discoveryService) { - synchronized (discoveryServices) { - discoveryServices.add(discoveryService); - } - } - - public void removeAccountHandler(IAmazonAccountHandler discoveryService) { - synchronized (discoveryServices) { - discoveryServices.remove(discoveryService); - } - } - - public AmazonEchoDiscovery() { + public AmazonEchoDiscovery(AccountHandler accountHandler) { super(SUPPORTED_THING_TYPES_UIDS, 10); + this.accountHandler = accountHandler; } public void activate() { - super.activate(new Hashtable()); + activate(new Hashtable()); } @Override @@ -91,42 +74,39 @@ public void deactivate() { @Override protected void startScan() { - startScan(true); - } - - protected void startAutomaticScan() { - startScan(false); - } - - void startScan(boolean manual) { stopScanJob(); removeOlderResults(activateTimeStamp); - if (discoverAccount) { - discoverAccount = false; - // No accounts created yet, create one - ThingUID thingUID = new ThingUID(THING_TYPE_ACCOUNT, "account1"); + setDevices(accountHandler.updateDeviceList()); - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("Amazon Account").build(); - logger.debug("Device [Amazon Account] found."); - thingDiscovered(result); - } + String currentFlashBriefingConfiguration = accountHandler.getNewCurrentFlashbriefingConfiguration(); + discoverFlashBriefingProfiles(currentFlashBriefingConfiguration); + } - IAmazonAccountHandler[] accounts; - synchronized (discoveryServices) { - accounts = new IAmazonAccountHandler[discoveryServices.size()]; - accounts = discoveryServices.toArray(accounts); + protected void startAutomaticScan() { + if (!this.accountHandler.getThing().getThings().isEmpty()) { + stopScanJob(); + return; } - - for (IAmazonAccountHandler discovery : accounts) { - discovery.updateDeviceList(manual); + 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(); } @Override protected void startBackgroundDiscovery() { stopScanJob(); - startScanStateJob = scheduler.schedule(this::startAutomaticScan, 3000, TimeUnit.MILLISECONDS); + startScanStateJob = scheduler.scheduleWithFixedDelay(this::startAutomaticScan, 3000, 1000, + TimeUnit.MILLISECONDS); } @Override @@ -153,7 +133,7 @@ public void activate(@Nullable Map config) { activateTimeStamp = new Date().getTime(); }; - public synchronized void setDevices(ThingUID brigdeThingUID, List deviceList) { + synchronized void setDevices(List deviceList) { DiscoveryServiceCallback discoveryServiceCallback = this.discoveryServiceCallback; if (discoveryServiceCallback == null) { return; @@ -177,6 +157,7 @@ public synchronized void setDevices(ThingUID brigdeThingUID, List device continue; } + ThingUID brigdeThingUID = this.accountHandler.getThing().getUID(); ThingUID thingUID = new ThingUID(thingTypeId, brigdeThingUID, serialNumber); if (discoveryServiceCallback.getExistingDiscoveryResult(thingUID) != null) { continue; @@ -199,25 +180,38 @@ public synchronized void setDevices(ThingUID brigdeThingUID, List device } } - public synchronized void discoverFlashBriefingProfiles(ThingUID brigdeThingUID, String currentFlashBriefingJson, - int number) { + public synchronized void discoverFlashBriefingProfiles(String currentFlashBriefingJson) { if (currentFlashBriefingJson.isEmpty()) { return; } - if (discoverdFlashBriefings.contains(currentFlashBriefingJson)) { + DiscoveryServiceCallback discoveryServiceCallback = this.discoveryServiceCallback; + if (discoveryServiceCallback == null) { return; } if (!discoverdFlashBriefings.contains(currentFlashBriefingJson)) { - String id = UUID.randomUUID().toString(); - ThingUID thingUID = new ThingUID(THING_TYPE_FLASH_BRIEFING_PROFILE, brigdeThingUID, id); - - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("FlashBriefing " + number) + ThingUID freeThingUID = null; + int freeIndex = 0; + for (int i = 1; i < 1000; i++) { + String id = Integer.toString(i); + ThingUID brigdeThingUID = this.accountHandler.getThing().getUID(); + ThingUID thingUID = new ThingUID(THING_TYPE_FLASH_BRIEFING_PROFILE, brigdeThingUID, id); + if (discoveryServiceCallback.getExistingThing(thingUID) == null + && discoveryServiceCallback.getExistingDiscoveryResult(thingUID) == null) { + freeThingUID = thingUID; + freeIndex = i; + break; + } + } + if (freeThingUID == null) { + logger.debug("No more free flashbriefing thing ID found"); + return; + } + DiscoveryResult result = DiscoveryResultBuilder.create(freeThingUID).withLabel("FlashBriefing " + freeIndex) .withProperty(DEVICE_PROPERTY_FLASH_BRIEFING_PROFILE, currentFlashBriefingJson) - .withBridge(brigdeThingUID).build(); + .withBridge(accountHandler.getThing().getUID()).build(); logger.debug("Flash Briefing {} discovered", currentFlashBriefingJson); - thingDiscovered(result); discoverdFlashBriefings.add(currentFlashBriefingJson); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonAccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonAccountHandler.java deleted file mode 100755 index 97979ebfb0bd8..0000000000000 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/IAmazonAccountHandler.java +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2010-2018 by the respective copyright holders. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.amazonechocontrol.internal.discovery; - -/** - * The {@link IAmazonAccountHandler} is responsible connection between account and discovery service - * - * @author Michael Geramb - Initial contribution - */ -public interface IAmazonAccountHandler { - void updateDeviceList(boolean manual); -} From e53330e2c3b92ca3d02267759336c76ccef0703f Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Tue, 22 May 2018 20:02:58 +0200 Subject: [PATCH 48/56] [amazonechocontrol] Binding configuration for showIdinGUI removed, Ids shown in servlet Signed-off-by: Michael Geramb (github: mgeramb) --- .../ESH-INF/binding/binding.xml | 8 - .../README.md | 16 +- .../AmazonEchoControlBindingConstants.java | 1 + .../handler/AccountHandler.java | 31 +- .../handler/EchoHandler.java | 8 +- .../internal/AccountServlet.java | 511 ++++++++++++++++++ .../AmazonEchoControlHandlerFactory.java | 27 +- .../internal/BindingServlet.java | 126 +++++ .../internal/LoginServlet.java | 261 --------- ...onEchoDynamicStateDescriptionProvider.java | 22 +- 10 files changed, 690 insertions(+), 321 deletions(-) create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java create mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java delete mode 100644 addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml index 429a4fd36c2bd..81910f326e731 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml +++ b/addons/binding/org.openhab.binding.amazonechocontrol/ESH-INF/binding/binding.xml @@ -6,12 +6,4 @@ Binding to control Amazon Echo devices (Alexa). This binding enables openHAB to control the volume, playing state, bluetooth connection of your amazon echo devices or allow to use it as TTS device. Michael Geramb - - - - Shows IDs in the channel drop downs which needed for setting the value from a rule - false - - - diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index 8e70f83743d57..c2e34a2fc4058 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -93,7 +93,7 @@ The Amazon Account thing needs the following configurations: 2 factor authentication is not supported! -** HINT ** IMPORTANT: If the Account thing does not go online and reports a login error, open the url YOUR_OPENHAB/amazonechocontrol/ID_OF_ACCOUNT_THING (Replace YOUR_OPENHAB and ID_OF_ACCOUNT_THING with your configuration) in your browser (e.g. http://openhab:8080/amazonechocontrol/account1) and try to login. +** HINT ** IMPORTANT: If the Account thing does not go online and reports a login error, open the url YOUR_OPENHAB/amazonechocontrol in your browser, click the link for your account thing (e.g. http://openhab:8080/amazonechocontrol/) and try to login. ### Amazon Devices @@ -269,15 +269,11 @@ sitemap amzonechocontrol label="Echo Devices" ``` ## How To Get IDs -Simple way to get the IDs required by the selection element or an rule: - -1) Open the Paper UI -2) Navigate to the Configuration / Bindings section -3) Click on the edit button (Pencil) of the Amazon Echo Control Binding -4) Enable the 'Show IDs in the GUI' option and save it -5) Navigate to the Control section -6) Most of the channels which requires a ID show now a drop-down with the ID within []-brackets. -If there are no drop downs, check if you have defined the channel and sometimes a browser refresh helps. + +1) Open the url YOUR_OPENHAB/amazonechocontrol in your browser (e.g. http://openhab:8080/amazonechocontrol/) +2) Click on the name of the account thing +3) Click on the name of the echo thing +4) Scroll to the channel and copy the required ID ## Tutorials diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java index d930b53b02f82..2fddadc81bfc2 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/AmazonEchoControlBindingConstants.java @@ -26,6 +26,7 @@ public class AmazonEchoControlBindingConstants { public static final String BINDING_ID = "amazonechocontrol"; + public static final String BINDING_NAME = "Amazon Echo Control"; // List of all Thing Type UIDs public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account"); diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index ad85c25c00081..919e6d182d166 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -34,10 +34,10 @@ import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; import org.openhab.binding.amazonechocontrol.internal.AccountConfiguration; +import org.openhab.binding.amazonechocontrol.internal.AccountServlet; import org.openhab.binding.amazonechocontrol.internal.Connection; import org.openhab.binding.amazonechocontrol.internal.ConnectionException; import org.openhab.binding.amazonechocontrol.internal.HttpException; -import org.openhab.binding.amazonechocontrol.internal.LoginServlet; import org.openhab.binding.amazonechocontrol.internal.StateStorage; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; @@ -67,10 +67,9 @@ public class AccountHandler extends BaseBridgeHandler { private Map jsonSerialNumberDeviceMapping = new HashMap<>(); private @Nullable ScheduledFuture refreshJob; private @Nullable ScheduledFuture refreshLogin; - private boolean discoverFlashProfiles; private String currentFlashBriefingJson = ""; private final HttpService httpService; - private @Nullable LoginServlet loginServlet; + private @Nullable AccountServlet accountServlet; private final Gson gson = new Gson(); public AccountHandler(Bridge bridge, HttpService httpService) { @@ -116,8 +115,8 @@ public void initialize() { this.connection = new Connection(email, password, amazonSite, this.getThing().getUID().getId()); } } - if (this.loginServlet == null) { - this.loginServlet = new LoginServlet(httpService, this.getThing().getUID().getId(), this, config); + if (this.accountServlet == null) { + this.accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, config); } updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login"); @@ -154,6 +153,17 @@ public void addEchoHandler(EchoHandler echoHandler) { } } + public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) { + synchronized (echoHandlers) { + for (EchoHandler echoHandler : echoHandlers) { + if (StringUtils.equals(echoHandler.findSerialNumber(), deviceSerialNumber)) { + return echoHandler.getThing(); + } + } + } + return null; + } + public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) { synchronized (flashBriefingProfileHandlers) { flashBriefingProfileHandlers.add(flashBriefingProfileHandler); @@ -206,24 +216,22 @@ public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) { echoHandlers.remove(childHandler); } } - // check for flash briefing profile handler if (childHandler instanceof FlashBriefingProfileHandler) { synchronized (flashBriefingProfileHandlers) { flashBriefingProfileHandlers.remove(childHandler); } } - super.childHandlerDisposed(childHandler, childThing); } @Override public void dispose() { - LoginServlet loginServlet = this.loginServlet; - if (loginServlet != null) { - loginServlet.dispose(); + AccountServlet accountServlet = this.accountServlet; + if (accountServlet != null) { + accountServlet.dispose(); } - this.loginServlet = null; + this.accountServlet = null; cleanup(); super.dispose(); } @@ -486,7 +494,6 @@ public void setEnabledFlashBriefingsJson(String flashBriefingJson) { } public String getNewCurrentFlashbriefingConfiguration() { - discoverFlashProfiles = true; return updateFlashBriefingHandlers(); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java index 69121b47b21f2..74e2b2f873d95 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/EchoHandler.java @@ -82,13 +82,11 @@ public class EchoHandler extends BaseThingHandler { private boolean updateRoutine = true; private boolean updatePlayMusicVoiceCommand = true; private boolean updateStartCommand = true; - private boolean showIdsInGUI = false; private @Nullable JsonNotificationResponse currentNotification; private @Nullable ScheduledFuture currentNotifcationUpdateTimer; - public EchoHandler(Thing thing, boolean showIdsInGUI) { + public EchoHandler(Thing thing) { super(thing); - this.showIdsInGUI = showIdsInGUI; } @Override @@ -140,10 +138,6 @@ public void dispose() { super.dispose(); } - public boolean getShowIdsInGUI() { - return this.showIdsInGUI; - } - public @Nullable BluetoothState findBluetoothState() { return this.bluetoothState; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java new file mode 100644 index 0000000000000..232105ad8b60e --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java @@ -0,0 +1,511 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal; + +import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Thing; +import org.openhab.binding.amazonechocontrol.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.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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 final Logger logger = LoggerFactory.getLogger(AccountServlet.class); + + HttpService httpService; + String servletUrlWithoutRoot; + String servletUrl; + AccountHandler account; + AccountConfiguration configuration; + String id; + Connection connection; + + public AccountServlet(HttpService httpService, String id, AccountHandler account, + AccountConfiguration configuration) { + this.httpService = httpService; + this.account = account; + this.id = id; + this.configuration = configuration; + this.connection = reCreateConnection(); + try { + servletUrlWithoutRoot = "amazonechocontrol/" + URLEncoder.encode(id, "UTF8"); + } catch (UnsupportedEncodingException e) { + servletUrlWithoutRoot = ""; + servletUrl = ""; + logger.warn("Register servlet fails {}", e); + return; + } + servletUrl = "/" + servletUrlWithoutRoot; + try { + httpService.registerServlet(servletUrl, this, null, httpService.createDefaultHttpContext()); + } catch (ServletException e) { + logger.warn("Register servlet fails {}", e); + } catch (NamespaceException e) { + logger.warn("Register servlet fails {}", e); + } + } + + private Connection reCreateConnection() { + return new Connection(configuration.email, configuration.password, configuration.amazonSite, this.id); + } + + public void dispose() { + httpService.unregister(servletUrl); + } + + @Override + protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) + throws ServletException, IOException { + if (req == null) { + return; + } + if (resp == null) { + return; + } + resp.addHeader("content-type", "text/html;charset=UTF-8"); + + Map 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 = map.get(name)[0]; + postDataBuilder.append(URLEncoder.encode(value, "UTF-8")); + if (name.equals("email") && !value.equalsIgnoreCase(configuration.email)) { + returnError(resp, + "Email must match the configured email of your thing. Change your configuration or retype your email."); + return; + } + + if (name.equals("password") && !value.equals(configuration.password)) { + returnError(resp, + "Password must match the configured password of your thing. Change your configuration or retype your password."); + return; + } + } + + String uri = req.getRequestURI(); + if (!uri.startsWith(servletUrl)) { + returnError(resp, "Invalid request uri '" + uri + "'"); + return; + } + String relativeUrl = uri.substring(servletUrl.length()).replace(FORWARD_URI_PART, "/"); + + String postUrl = "https://www." + connection.getAmazonSite() + relativeUrl; + String queryString = req.getQueryString(); + if (queryString != null && queryString.length() > 0) { + postUrl += "?" + queryString; + } + String referer = "https://www." + connection.getAmazonSite(); + String postData = postDataBuilder.toString(); + HandleProxyRequest(resp, "POST", postUrl, referer, postData); + } + + @Override + protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) + throws ServletException, IOException { + if (req == null) { + return; + } + if (resp == null) { + return; + } + String baseUrl = req.getRequestURI().substring(servletUrl.length()); + String uri = baseUrl; + String queryString = req.getQueryString(); + if (queryString != null && queryString.length() > 0) { + uri += "?" + queryString; + } + logger.debug("doGet {}", uri); + try { + if (uri.startsWith(FORWARD_URI_PART)) { + + String getUrl = "https://www." + connection.getAmazonSite() + "/" + + uri.substring(FORWARD_URI_PART.length()); + + this.HandleProxyRequest(resp, "GET", getUrl, null, null); + return; + } + + Connection connection = this.account.findConnection(); + if (connection != null && connection.verifyLogin()) { + + // handle diagnostic commands + if (baseUrl.equals("/devices") || baseUrl.equals("/devices/")) { + handleDevices(resp, connection); + return; + } + if (baseUrl.equals("/ids") || baseUrl.equals("/ids/")) { + String serialNumber = getQueryMap(queryString).get("serialNumber"); + Device device = account.findDeviceJson(serialNumber); + if (device != null) { + Thing thing = account.findThingBySerialNumber(device.serialNumber); + if (thing != null) { + handleIds(resp, connection, device, thing); + } + return; + } + } + // return hint that everything is ok + handleDefaultPageResult(resp, "The Account is already logged in."); + return; + } + + if (!uri.equals("/")) { + String newUri = req.getServletPath() + "/"; + resp.sendRedirect(newUri); + return; + } + + String html = this.connection.getLoginPage(); + returnHtml(resp, html); + } catch (URISyntaxException e) { + logger.warn("get failed with uri syntax error {}", e); + } + } + + public Map getQueryMap(@Nullable String query) { + Map map = new HashMap(); + if (query != null) { + String[] params = query.split("&"); + for (String param : params) { + String[] elements = param.split("="); + if (elements.length == 2) { + String name = elements[0]; + String value = ""; + try { + value = URLDecoder.decode(elements[1], "UTF8"); + } catch (UnsupportedEncodingException e) { + logger.info("Unsupported encoding {}", e); + } + map.put(name, value); + } + } + } + return map; + } + + private void handleDefaultPageResult(HttpServletResponse resp, String message) throws IOException { + StringBuilder html = createPageStart("Index"); + html.append(StringEscapeUtils.escapeHtml(message + " The account thing should be online.")); + html.append("
"); + html.append(StringEscapeUtils.escapeHtml("Check Thing in Paper UI")); + html.append("

"); + + html.append( + ""); + for (Device device : this.account.getLastKnownDevices()) { + + html.append(""); + } + html.append("
DeviceSerial NumberStateThingTypeFamily
"); + html.append(StringEscapeUtils.escapeHtml(nullReplacement(device.accountName))); + html.append(""); + html.append(StringEscapeUtils.escapeHtml(nullReplacement(device.serialNumber))); + html.append(""); + html.append(StringEscapeUtils.escapeHtml(device.online ? "Online" : "Offline")); + html.append(""); + Thing accountHandler = account.findThingBySerialNumber(device.serialNumber); + if (accountHandler != null) { + html.append("" + + StringEscapeUtils.escapeHtml(accountHandler.getLabel()) + ""); + } else { + html.append("Not defined"); + } + html.append(""); + html.append(StringEscapeUtils.escapeHtml(nullReplacement(device.deviceFamily))); + html.append(""); + html.append(StringEscapeUtils.escapeHtml(nullReplacement(device.deviceType))); + html.append(""); + html.append("
"); + createPageEndAndSent(resp, html); + } + + private void handleDevices(HttpServletResponse resp, Connection connection) throws IOException, URISyntaxException { + returnHtml(resp, "" + StringEscapeUtils.escapeHtml(connection.getDeviceListJson()) + ""); + } + + private String nullReplacement(@Nullable String text) { + if (text == null) { + return ""; + } + return text; + } + + StringBuilder createPageStart(String title) { + StringBuilder html = new StringBuilder(); + html.append("" + + StringEscapeUtils + .escapeHtml(BINDING_NAME + " - " + this.account.getThing().getLabel() + " - " + title) + + ""); + html.append("

" + StringEscapeUtils + .escapeHtml(BINDING_NAME + " - " + this.account.getThing().getLabel() + " - " + title) + "

"); + return html; + } + + private void createPageEndAndSent(HttpServletResponse resp, StringBuilder html) { + html.append(""); + resp.addHeader("content-type", "text/html;charset=UTF-8"); + try { + resp.getWriter().write(html.toString()); + } catch (IOException e) { + logger.warn("return html failed with IO error {}", e); + } + } + + private void handleIds(HttpServletResponse resp, Connection connection, Device device, Thing thing) + throws IOException, URISyntaxException { + StringBuilder html = createPageStart("Channel Options - " + thing.getLabel()); + + renderBluetoothMacChannel(connection, device, html); + renderAmazonMusicPlaylistIdChannel(connection, device, html); + renderPlayAlarmSoundChannel(connection, device, html); + renderMusicProviderIdChannel(connection, html); + + createPageEndAndSent(resp, html); + } + + private void renderMusicProviderIdChannel(Connection connection, StringBuilder html) { + html.append("

" + StringEscapeUtils.escapeHtml("Channel " + CHANNEL_MUSIC_PROVIDER_ID) + "

"); + html.append(""); + List musicProviders = connection.getMusicProviders(); + for (JsonMusicProvider musicProvider : musicProviders) { + @Nullable + List<@Nullable String> properties = musicProvider.supportedProperties; + String providerId = musicProvider.id; + String displayName = musicProvider.displayName; + if (properties != null && properties.contains("Alexa.Music.PlaySearchPhrase") + && StringUtils.isNotEmpty(providerId) && StringUtils.equals(musicProvider.availability, "AVAILABLE") + && StringUtils.isNotEmpty(displayName)) { + html.append(""); + } + } + html.append("
NameValue
"); + html.append(StringEscapeUtils.escapeHtml(displayName)); + html.append(""); + html.append(StringEscapeUtils.escapeHtml(providerId)); + html.append("
"); + } + + private void renderPlayAlarmSoundChannel(Connection connection, Device device, StringBuilder html) { + html.append("

" + StringEscapeUtils.escapeHtml("Channel " + CHANNEL_PLAY_ALARM_SOUND) + "

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

" + StringEscapeUtils.escapeHtml("Channel " + CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID) + "

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

" + StringEscapeUtils.escapeHtml("Channel " + CHANNEL_BLUETOOTH_MAC) + "

"); + JsonBluetoothStates bluetoothStates = connection.getBluetoothConnectionStates(); + BluetoothState[] innerStates = bluetoothStates.bluetoothStates; + if (innerStates != null) { + for (BluetoothState state : innerStates) { + if (StringUtils.equals(state.deviceSerialNumber, device.serialNumber)) { + PairedDevice[] pairedDeviceList = state.pairedDeviceList; + if (pairedDeviceList != null && pairedDeviceList.length > 0) { + html.append(""); + for (PairedDevice pairedDevice : pairedDeviceList) { + html.append(""); + } + html.append("
NameValue
"); + html.append(StringEscapeUtils.escapeHtml(nullReplacement(pairedDevice.friendlyName))); + html.append(""); + html.append(StringEscapeUtils.escapeHtml(nullReplacement(pairedDevice.address))); + html.append("
"); + } else { + html.append(StringEscapeUtils.escapeHtml("No bluetooth devices paired")); + } + } + } + } + } + + void HandleProxyRequest(HttpServletResponse resp, String verb, String url, @Nullable String referer, + @Nullable String postData) throws IOException { + HttpsURLConnection urlConnection; + try { + Map headers = null; + if (referer != null) { + headers = new HashMap(); + headers.put("Referer", referer); + } + + urlConnection = connection.makeRequest(verb, url, postData, false, false, headers); + if (urlConnection.getResponseCode() == 302) { + { + String location = urlConnection.getHeaderField("location"); + if (location.contains("//alexa.")) { + if (connection.verifyLogin()) { + handleDefaultPageResult(resp, "Login succeeded"); + account.setConnection(this.connection); + this.connection = reCreateConnection(); + return; + } + } + String startString = "https://www." + connection.getAmazonSite() + "/"; + String newLocation = null; + 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.addHeader("location", newLocation); + resp.sendError(302); + return; + } + returnError(resp, "Invalid redirect to '" + location + "'"); + return; + } + } + } catch (URISyntaxException e) { + returnError(resp, e.getLocalizedMessage()); + return; + } + String response = connection.convertStream(urlConnection.getInputStream()); + returnHtml(resp, response); + } + + private void returnHtml(HttpServletResponse resp, String html) { + String resultHtml = html.replace("https://www." + connection.getAmazonSite() + "/", 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, String errorMessage) { + try { + resp.getWriter().write("" + StringEscapeUtils.escapeHtml(errorMessage) + "
Try again"); + } catch (IOException e) { + logger.info("Returning error message failed {}", e); + } + } +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 2b48701d076cd..92a83909128a2 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -50,10 +50,10 @@ public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { @Nullable HttpService httpService; - - boolean showIdsInGUI; @Nullable ServiceRegistration discoverServiceRegistration; + @Nullable + BindingServlet bindingServlet; @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { @@ -63,13 +63,19 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { @Override protected void activate(ComponentContext componentContext) { super.activate(componentContext); - Object configShowIdsInGui = componentContext.getProperties().get("showIdsInGUI"); - showIdsInGUI = (configShowIdsInGui instanceof Boolean) ? (Boolean) configShowIdsInGui : false; - + HttpService httpService = this.httpService; + if (bindingServlet == null && httpService != null) { + bindingServlet = new BindingServlet(httpService); + } } @Override protected void deactivate(ComponentContext componentContext) { + BindingServlet bindingServlet = this.bindingServlet; + this.bindingServlet = null; + if (bindingServlet != null) { + bindingServlet.dispose(); + } super.deactivate(componentContext); } @@ -83,16 +89,18 @@ protected void deactivate(ComponentContext componentContext) { } if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { - AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService); registerDiscoveryService(bridgeHandler); + if (bindingServlet != null) { + bindingServlet.addAccountThing(thing); + } return bridgeHandler; } if (thingTypeUID.equals(THING_TYPE_FLASH_BRIEFING_PROFILE)) { return new FlashBriefingProfileHandler(thing); } if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new EchoHandler(thing, showIdsInGUI); + return new EchoHandler(thing); } return null; } @@ -107,6 +115,11 @@ private synchronized void registerDiscoveryService(AccountHandler bridgeHandler) @Override protected synchronized void removeHandler(ThingHandler thingHandler) { if (thingHandler instanceof AccountHandler) { + BindingServlet bindingServlet = this.bindingServlet; + if (bindingServlet != null) { + bindingServlet.removeAccountThing(thingHandler.getThing()); + } + ServiceRegistration serviceReg = this.discoveryServiceRegistrations .get(thingHandler.getThing().getUID()); if (serviceReg != null) { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java new file mode 100644 index 0000000000000..ddbd31691206d --- /dev/null +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.amazonechocontrol.internal; + +import static org.openhab.binding.amazonechocontrol.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.apache.commons.lang.StringEscapeUtils; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Thing; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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(AccountServlet.class); + + String servletUrlWithoutRoot; + String servletUrl; + HttpService httpService; + + List accountHandlers = new ArrayList(); + + public BindingServlet(HttpService httpService) { + this.httpService = httpService; + servletUrlWithoutRoot = "amazonechocontrol"; + servletUrl = "/" + servletUrlWithoutRoot; + try { + httpService.registerServlet(servletUrl, this, null, httpService.createDefaultHttpContext()); + } catch (ServletException e) { + logger.warn("Register servlet fails {}", e); + } catch (NamespaceException 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 uri = req.getRequestURI().substring(servletUrl.length()); + String queryString = req.getQueryString(); + if (queryString != null && queryString.length() > 0) { + uri += "?" + queryString; + } + logger.debug("doGet {}", uri); + + if (!uri.equals("/")) { + String newUri = req.getServletPath() + "/"; + resp.sendRedirect(newUri); + return; + } + + StringBuilder html = new StringBuilder(); + html.append("" + StringEscapeUtils.escapeHtml(BINDING_NAME) + ""); + html.append("

" + StringEscapeUtils.escapeHtml(BINDING_NAME) + "

"); + + synchronized (accountHandlers) { + if (accountHandlers.isEmpty()) { + html.append("No Account thing created."); + } else { + for (Thing accountHandler : accountHandlers) { + String url = URLEncoder.encode(accountHandler.getUID().getId(), "UTF8"); + html.append("" + StringEscapeUtils.escapeHtml(accountHandler.getLabel()) + + "
"); + } + } + } + html.append(""); + + resp.addHeader("content-type", "text/html;charset=UTF-8"); + try { + resp.getWriter().write(html.toString()); + } catch (IOException e) { + logger.warn("return html failed with uri syntax error {}", e); + } + } + +} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java deleted file mode 100644 index 12ec7e731f174..0000000000000 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/LoginServlet.java +++ /dev/null @@ -1,261 +0,0 @@ -/** - * Copyright (c) 2010-2018 by the respective copyright holders. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.amazonechocontrol.internal; - -import static org.openhab.binding.amazonechocontrol.AmazonEchoControlBindingConstants.*; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.util.HashMap; -import java.util.Map; - -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.apache.commons.lang.StringEscapeUtils; -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.amazonechocontrol.handler.AccountHandler; -import org.osgi.service.http.HttpService; -import org.osgi.service.http.NamespaceException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * 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 - * - * @author Michael Geramb - Initial Contribution - */ -@NonNullByDefault -public class LoginServlet extends HttpServlet { - - private static final long serialVersionUID = -1453738923337413163L; - private static final String FORWARD_URI_PART = "/FORWARD/"; - - private final Logger logger = LoggerFactory.getLogger(LoginServlet.class); - - HttpService httpService; - String servletUrlWithoutRoot; - String servletUrl; - AccountHandler account; - AccountConfiguration configuration; - String id; - Connection connection; - - public LoginServlet(HttpService httpService, String id, AccountHandler account, - AccountConfiguration configuration) { - this.httpService = httpService; - this.account = account; - this.id = id; - this.configuration = configuration; - this.connection = reCreateConnection(); - servletUrlWithoutRoot = "amazonechocontrol/" + id; - servletUrl = "/" + servletUrlWithoutRoot; - try { - httpService.registerServlet(servletUrl, this, null, httpService.createDefaultHttpContext()); - } catch (ServletException e) { - logger.warn("Register servlet fails {}", e); - } catch (NamespaceException e) { - logger.warn("Register servlet fails {}", e); - } - } - - private Connection reCreateConnection() { - return new Connection(configuration.email, configuration.password, configuration.amazonSite, this.id); - } - - public void dispose() { - httpService.unregister(servletUrl); - } - - @Override - protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) - throws ServletException, IOException { - if (req == null) { - return; - } - if (resp == null) { - return; - } - resp.addHeader("content-type", "text/html;charset=UTF-8"); - - Map 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 = map.get(name)[0]; - postDataBuilder.append(URLEncoder.encode(value, "UTF-8")); - if (name.equals("email") && !value.equalsIgnoreCase(configuration.email)) { - returnError(resp, - "Email must match the configured email of your thing. Change your configuration or retype your email."); - return; - } - - if (name.equals("password") && !value.equals(configuration.password)) { - returnError(resp, - "Password must match the configured password of your thing. Change your configuration or retype your password."); - return; - } - } - - String uri = req.getRequestURI(); - if (!uri.startsWith(servletUrl)) { - returnError(resp, "Invalid request uri '" + uri + "'"); - return; - } - String relativeUrl = uri.substring(servletUrl.length()).replace(FORWARD_URI_PART, "/"); - - String postUrl = "https://www." + connection.getAmazonSite() + relativeUrl; - String queryString = req.getQueryString(); - if (queryString != null && queryString.length() > 0) { - postUrl += "?" + queryString; - } - String referer = "https://www." + connection.getAmazonSite(); - String postData = postDataBuilder.toString(); - HandleProxyRequest(resp, "POST", postUrl, referer, postData); - } - - @Override - protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) - throws ServletException, IOException { - if (req == null) { - return; - } - if (resp == null) { - return; - } - String uri = req.getRequestURI().substring(servletUrl.length()); - String queryString = req.getQueryString(); - if (queryString != null && queryString.length() > 0) { - uri += "?" + queryString; - } - logger.debug("doGet {}", uri); - try { - if (uri.startsWith(FORWARD_URI_PART)) { - - String getUrl = "https://www." + connection.getAmazonSite() + "/" - + uri.substring(FORWARD_URI_PART.length()); - - this.HandleProxyRequest(resp, "GET", getUrl, null, null); - return; - } - - Connection connection = this.account.findConnection(); - if (connection != null && connection.verifyLogin()) { - - // handle diagnostic commands - if (uri.equals("/devices") || uri.equals("/devices/")) { - returnHtml(resp, - "" + StringEscapeUtils.escapeHtml(connection.getDeviceListJson()) + ""); - return; - } - - // return hint that everything is ok - resp.getWriter().write( - "The Account is already logged in. The account thing should be online.
Check Thing in Paper UI"); - return; - } - - if (!uri.equals("/")) { - String newUri = req.getServletPath() + "/"; - resp.sendRedirect(newUri); - return; - } - - String html = this.connection.getLoginPage(); - returnHtml(resp, html); - } catch (URISyntaxException e) { - logger.warn("get failed with uri syntax error {}", e); - } - } - - void HandleProxyRequest(HttpServletResponse resp, String verb, String url, @Nullable String referer, - @Nullable String postData) throws IOException { - HttpsURLConnection urlConnection; - try { - Map headers = null; - if (referer != null) { - headers = new HashMap(); - headers.put("Referer", referer); - } - - urlConnection = connection.makeRequest(verb, url, postData, false, false, headers); - if (urlConnection.getResponseCode() == 302) { - { - String location = urlConnection.getHeaderField("location"); - if (location.contains("//alexa.")) { - if (connection.verifyLogin()) { - resp.getWriter().write( - "Login succeeded. The account thing should now be online.
Check Thing in Paper UI"); - account.setConnection(this.connection); - this.connection = reCreateConnection(); - return; - } - } - String startString = "https://www." + connection.getAmazonSite() + "/"; - String newLocation = null; - 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.addHeader("location", newLocation); - resp.sendError(302); - return; - } - returnError(resp, "Invalid redirect to '" + location + "'"); - return; - } - } - } catch (URISyntaxException e) { - returnError(resp, e.getLocalizedMessage()); - return; - } - - String response = connection.convertStream(urlConnection.getInputStream()); - returnHtml(resp, response); - } - - private void returnHtml(HttpServletResponse resp, String html) { - String resultHtml = html.replace("https://www." + connection.getAmazonSite() + "/", servletUrl + "/"); - resp.addHeader("content-type", "text/html;charset=UTF-8"); - try { - resp.getWriter().write(resultHtml); - } catch (IOException e) { - logger.warn("return html failed with uri syntax error {}", e); - } - } - - void returnError(HttpServletResponse resp, String errorMessage) { - try { - resp.getWriter().write("" + StringEscapeUtils.escapeHtml(errorMessage) + "
Try again"); - } catch (IOException e) { - logger.info("Returning error message failed {}", e); - } - } -} diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java index dc215f5d6748d..5fbe6bfceeead 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/statedescription/AmazonEchoDynamicStateDescriptionProvider.java @@ -86,14 +86,6 @@ protected void unsetThingRegistry(ThingRegistry thingRegistry) { return thing.getHandler(); } - StateOption createStateOption(@Nullable String id, @Nullable String displayValue, boolean showIdsInGUI) { - if (showIdsInGUI) { - return new StateOption(id, String.format("%s [%s]", displayValue, id)); - } else { - return new StateOption(id, displayValue); - } - } - @Override public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { @@ -125,7 +117,7 @@ StateOption createStateOption(@Nullable String id, @Nullable String displayValue continue; } if (device.address != null && device.friendlyName != null) { - options.add(createStateOption(device.address, device.friendlyName, handler.getShowIdsInGUI())); + options.add(new StateOption(device.address, device.friendlyName)); } } StateDescription result = new StateDescription(originalStateDescription.getMinimum(), @@ -162,9 +154,8 @@ StateOption createStateOption(@Nullable String id, @Nullable String displayValue if (innerLists != null && innerLists.length > 0) { PlayList playList = innerLists[0]; if (playList.playlistId != null && playList.title != null) { - options.add(createStateOption(playList.playlistId, - String.format("%s (%d)", playList.title, playList.trackCount), - handler.getShowIdsInGUI())); + options.add(new StateOption(playList.playlistId, + String.format("%s (%d)", playList.title, playList.trackCount))); } } } @@ -201,8 +192,7 @@ StateOption createStateOption(@Nullable String id, @Nullable String displayValue if (notificationSound.folder == null && notificationSound.providerId != null && notificationSound.id != null && notificationSound.displayName != null) { String providerSoundId = notificationSound.providerId + ":" + notificationSound.id; - options.add(createStateOption(providerSoundId, notificationSound.displayName, - handler.getShowIdsInGUI())); + options.add(new StateOption(providerSoundId, notificationSound.displayName)); } } StateDescription result = new StateDescription(originalStateDescription.getMinimum(), @@ -256,7 +246,7 @@ StateOption createStateOption(@Nullable String id, @Nullable String displayValue && StringUtils.isNotEmpty(providerId) && StringUtils.equals(musicProvider.availability, "AVAILABLE") && StringUtils.isNotEmpty(displayName)) { - options.add(createStateOption(providerId, displayName, handler.getShowIdsInGUI())); + options.add(new StateOption(providerId, displayName)); } } StateDescription result = new StateDescription(originalStateDescription.getMinimum(), @@ -284,7 +274,7 @@ StateOption createStateOption(@Nullable String id, @Nullable String displayValue for (FlashBriefingProfileHandler flashBriefing : flashbriefings) { String value = FLASH_BRIEFING_COMMAND_PREFIX + flashBriefing.getThing().getUID().getId(); String displayName = flashBriefing.getThing().getLabel(); - options.add(createStateOption(value, displayName, handler.getShowIdsInGUI())); + options.add(new StateOption(value, displayName)); } StateDescription result = new StateDescription(originalStateDescription.getMinimum(), originalStateDescription.getMaximum(), originalStateDescription.getStep(), From 8c3913c3025e07b0740a7100be7ffe70e95df2bf Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Tue, 22 May 2018 20:08:36 +0200 Subject: [PATCH 49/56] [amazonechocontrol] error handler added Signed-off-by: Michael Geramb (github: mgeramb) --- .../binding/amazonechocontrol/handler/AccountHandler.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index 919e6d182d166..cb483632bd312 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -408,8 +408,10 @@ private void refreshData() { updateStatus(ThingStatus.ONLINE); logger.debug("refresh data {} finished", getThing().getUID().getAsString()); - } catch (Exception e) { - logger.error("refresh data failed: {}", e); + } catch (HttpException | JsonSyntaxException | ConnectionException e) { + logger.debug("refresh data fails {}", e); + } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail. + logger.error("refresh data fails with unexpected error {}", e); } } From 296dbf4f25ef905e7cd57c5a7320a6318ee9390d Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Tue, 22 May 2018 21:06:36 +0200 Subject: [PATCH 50/56] [amazonechocontrol] Update readme Signed-off-by: Michael Geramb (github: mgeramb) --- .../README.md | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/README.md b/addons/binding/org.openhab.binding.amazonechocontrol/README.md index c2e34a2fc4058..9154a0f8a8c7d 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/README.md +++ b/addons/binding/org.openhab.binding.amazonechocontrol/README.md @@ -48,7 +48,7 @@ The binding is tested with amazon.de, amazon.com and amazon.co.uk accounts, but For the connection to the Amazon server, your password of the Amazon account is required, this will be stored in your openHAB thing device configuration. So you should be sure, that nobody other has access to your configuration! -## What else you should know +## What Else You Should Know All the display options are updated by polling the amazon server. The polling time can be configured, but a minimum of 10 seconds is required. @@ -66,15 +66,23 @@ It's not know, if there is a limit implemented in the amazon server if the polli | wha | Amazon Echo Whole House Audio Control | | flashbriefingprofile | Flash briefing profile | +## First Steps + +1) Create an 'Amazon Account' thing +2) Configure your credentials in the account thing (2 factor authentication is not supported!) +3) After confirmation: +a) the 'Account Thing' goes Online -> continue with 4) +b) the 'Account Thing' stays offline: +open the url YOUR_OPENHAB/amazonechocontrol in your browser (e.g. http://openhab:8080/amazonechocontrol/), click the link for your account thing and try to login. +4) The echo device things get automatically discovered and can be accepted + ## Discovery -The first 'Amazon Account' thing will be automatically discovered. -After configuration of the thing with the account data, a 'Amazon ' thing will be discovered for each registered device. +After configuration of the account thing with the login data, the echo devices registered in the amazon account, get discovered. If the device type is not known by the binding, the device will not be discovered. But you can define any device listed in your alexa app with the best matching existing device (e.g. echo). You will find the required serial number in settings of the device in the alexa app. - ## Binding Configuration The binding does not have any configuration. @@ -91,9 +99,7 @@ The Amazon Account thing needs the following configurations: | password | Password of your amazon account | | pollingIntervalInSeconds | Polling interval for the device state in seconds. Default 30, minimum 10 | -2 factor authentication is not supported! - -** HINT ** IMPORTANT: If the Account thing does not go online and reports a login error, open the url YOUR_OPENHAB/amazonechocontrol in your browser, click the link for your account thing (e.g. http://openhab:8080/amazonechocontrol/) and try to login. +IMPORTANT: If the Account thing does not go online and reports a login error, read the instructions in "First Steps" above. ### Amazon Devices From 7103e628cc0f5cf4628a1c1e6ce8d7a8a4ef7b4c Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 23 May 2018 17:53:07 +0200 Subject: [PATCH 51/56] [amazonechocontrol] Resource leak in state storage fixed, unused field removed in factory Signed-off-by: Michael Geramb (github: mgeramb) --- .../internal/AmazonEchoControlHandlerFactory.java | 2 -- .../amazonechocontrol/internal/StateStorage.java | 14 ++++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 92a83909128a2..5188005f3cdd7 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -51,8 +51,6 @@ public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { @Nullable HttpService httpService; @Nullable - ServiceRegistration discoverServiceRegistration; - @Nullable BindingServlet bindingServlet; @Override diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java index f5097625ff462..ec4c85b6b04df 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java @@ -30,7 +30,6 @@ */ @NonNullByDefault public class StateStorage { - private final Logger logger = LoggerFactory.getLogger(StateStorage.class); File propertyFile; @@ -90,10 +89,9 @@ private void saveProperties() { if (!directory.exists()) { directory.mkdirs(); } - - FileWriter fileWriter = new FileWriter(propertyFile); - properties.store(fileWriter, "Save properties"); - fileWriter.close(); + try (FileWriter fileWriter = new FileWriter(propertyFile)) { + properties.store(fileWriter, "Save properties"); + } } catch (IOException e) { logger.error("Saving properties failed {}", e); } @@ -105,9 +103,9 @@ private Properties initProperties() { result = new Properties(); if (propertyFile.exists()) { try { - FileReader fileReader = new FileReader(propertyFile); - result.load(fileReader); - fileReader.close(); + try (FileReader fileReader = new FileReader(propertyFile)) { + result.load(fileReader); + } } catch (IOException e) { logger.error("Error occured on writing the property file.", e); } From 19a430f87f82e4d43be5bfc3fb9906f586409386 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 23 May 2018 17:59:04 +0200 Subject: [PATCH 52/56] [amazonechocontrol] Fixed try syntax in StateStorage Signed-off-by: Michael Geramb (github: mgeramb) --- .../internal/StateStorage.java | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java index ec4c85b6b04df..15c0785ec4dd6 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/StateStorage.java @@ -81,17 +81,15 @@ public void storeState(@Nullable String key, @Nullable String value) { } private void saveProperties() { - try { - Properties properties = initProperties(); - logger.debug("Create file {}.", propertyFile); - String directoryName = propertyFile.getParent(); - File directory = new File(directoryName); - if (!directory.exists()) { - directory.mkdirs(); - } - try (FileWriter fileWriter = new FileWriter(propertyFile)) { - properties.store(fileWriter, "Save properties"); - } + Properties properties = initProperties(); + logger.debug("Create file {}.", propertyFile); + String directoryName = propertyFile.getParent(); + File directory = new File(directoryName); + if (!directory.exists()) { + directory.mkdirs(); + } + try (FileWriter fileWriter = new FileWriter(propertyFile)) { + properties.store(fileWriter, "Save properties"); } catch (IOException e) { logger.error("Saving properties failed {}", e); } @@ -102,10 +100,8 @@ private Properties initProperties() { if (result == null) { result = new Properties(); if (propertyFile.exists()) { - try { - try (FileReader fileReader = new FileReader(propertyFile)) { - result.load(fileReader); - } + try (FileReader fileReader = new FileReader(propertyFile)) { + result.load(fileReader); } catch (IOException e) { logger.error("Error occured on writing the property file.", e); } From c93df728e1bed09f577a6101bbf98a30cd4c7b1b Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 23 May 2018 20:14:22 +0200 Subject: [PATCH 53/56] [amazonechocontrol] Upgrade version for storage replacement Signed-off-by: Michael Geramb (github: mgeramb) --- .../META-INF/MANIFEST.MF | 1 + .../handler/AccountHandler.java | 21 ++++++++++------ .../handler/FlashBriefingProfileHandler.java | 17 +++++++++---- .../AmazonEchoControlHandlerFactory.java | 25 +++++++++++++++++-- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF index 954f381292474..d2190d145686a 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF +++ b/addons/binding/org.openhab.binding.amazonechocontrol/META-INF/MANIFEST.MF @@ -18,6 +18,7 @@ Import-Package: com.google.gson, org.eclipse.smarthome.core.cache, org.eclipse.smarthome.core.i18n, org.eclipse.smarthome.core.library.types, + org.eclipse.smarthome.core.storage, org.eclipse.smarthome.core.thing, org.eclipse.smarthome.core.thing.binding, org.eclipse.smarthome.core.thing.binding.builder, diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index cb483632bd312..50b6782f86077 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -24,6 +24,7 @@ import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.storage.Storage; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; @@ -59,7 +60,7 @@ public class AccountHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(AccountHandler.class); - private StateStorage stateStorage; + private Storage stateStorage; private @Nullable Connection connection; private final Set echoHandlers = new HashSet<>(); private final Set flashBriefingProfileHandlers = new HashSet<>(); @@ -72,10 +73,16 @@ public class AccountHandler extends BaseBridgeHandler { private @Nullable AccountServlet accountServlet; private final Gson gson = new Gson(); - public AccountHandler(Bridge bridge, HttpService httpService) { + public AccountHandler(Bridge bridge, HttpService httpService, Storage stateStorage) { super(bridge); this.httpService = httpService; - stateStorage = new StateStorage(bridge); + this.stateStorage = stateStorage; + StateStorage oldStorage = new StateStorage(thing); + String sessionStorage = oldStorage.findState("sessionStorage"); + if (sessionStorage != null) { + this.stateStorage.put("sessionStorage", sessionStorage); + oldStorage.storeState("sessionStorage", null); + } } @Override @@ -284,7 +291,7 @@ private void checkLogin() { if (loginTime != null && currentTime - loginTime.getTime() > 86400000 * 5) // 5 days { // Recreate session - this.stateStorage.storeState("sessionStorage", ""); + this.stateStorage.put("sessionStorage", ""); currentConnection = new Connection(currentConnection.getEmail(), currentConnection.getPassword(), currentConnection.getAmazonSite(), this.getThing().getUID().getId()); } @@ -293,7 +300,7 @@ private void checkLogin() { try { // read session data from property - String sessionStore = this.stateStorage.findState("sessionStorage"); + String sessionStore = this.stateStorage.get("sessionStorage"); // try use the session data if (!currentConnection.tryRestoreLogin(sessionStore)) { @@ -321,7 +328,7 @@ private void checkLogin() { } // store session data in property String serializedStorage = currentConnection.serializeLoginData(); - this.stateStorage.storeState("sessionStorage", serializedStorage); + this.stateStorage.put("sessionStorage", serializedStorage); } this.connection = currentConnection; } catch (ConnectionException e) { @@ -362,7 +369,7 @@ private void handleValidLogin() { public void setConnection(Connection connection) { this.connection = connection; String serializedStorage = connection.serializeLoginData(); - this.stateStorage.storeState("sessionStorage", serializedStorage); + this.stateStorage.put("sessionStorage", serializedStorage); handleValidLogin(); } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java index 226a4ba022c05..26e8950f085d9 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java @@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.storage.Storage; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; @@ -45,14 +46,20 @@ public class FlashBriefingProfileHandler extends BaseThingHandler { @Nullable AccountHandler accountHandler; - StateStorage stateStorage; + Storage stateStorage; boolean updatePlayOnDevice = true; String currentConfigurationJson = ""; private @Nullable ScheduledFuture updateStateJob; - public FlashBriefingProfileHandler(Thing thing) { + public FlashBriefingProfileHandler(Thing thing, Storage storage) { super(thing); - stateStorage = new StateStorage(thing); + this.stateStorage = storage; + StateStorage oldStorage = new StateStorage(thing); + String configurationJson = oldStorage.findState("configurationJson"); + if (configurationJson != null) { + this.stateStorage.put("configurationJson", configurationJson); + oldStorage.storeState("configurationJson", null); + } } public @Nullable AccountHandler findAccountHandler() { @@ -167,7 +174,7 @@ public boolean initialize(AccountHandler handler, String currentConfigurationJso } if (this.accountHandler != handler) { this.accountHandler = handler; - String configurationJson = this.stateStorage.findState("configurationJson"); + String configurationJson = this.stateStorage.get("configurationJson"); if (configurationJson == null || configurationJson.isEmpty()) { this.currentConfigurationJson = saveCurrentProfile(handler); } else { @@ -193,7 +200,7 @@ private String saveCurrentProfile(AccountHandler connection) { configurationJson = connection.getEnabledFlashBriefingsJson(); this.currentConfigurationJson = configurationJson; if (!configurationJson.isEmpty()) { - this.stateStorage.storeState("configurationJson", configurationJson); + this.stateStorage.put("configurationJson", configurationJson); } return configurationJson; } diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 5188005f3cdd7..86686c3a7b391 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -17,6 +17,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.storage.Storage; +import org.eclipse.smarthome.core.storage.StorageService; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingTypeUID; @@ -51,6 +53,8 @@ public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { @Nullable HttpService httpService; @Nullable + StorageService storageService; + @Nullable BindingServlet bindingServlet; @Override @@ -85,9 +89,15 @@ protected void deactivate(ComponentContext componentContext) { if (httpService == null) { return null; } + StorageService storageService = this.storageService; + if (storageService == null) { + return null; + } if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { - AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService); + Storage storage = storageService.getStorage(thing.getUID().toString(), + String.class.getClassLoader()); + AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService, storage); registerDiscoveryService(bridgeHandler); if (bindingServlet != null) { bindingServlet.addAccountThing(thing); @@ -95,7 +105,9 @@ protected void deactivate(ComponentContext componentContext) { return bridgeHandler; } if (thingTypeUID.equals(THING_TYPE_FLASH_BRIEFING_PROFILE)) { - return new FlashBriefingProfileHandler(thing); + Storage storage = storageService.getStorage(thing.getUID().toString(), + String.class.getClassLoader()); + return new FlashBriefingProfileHandler(thing, storage); } if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { return new EchoHandler(thing); @@ -140,4 +152,13 @@ protected void setHttpService(HttpService httpService) { protected void unsetHttpService(HttpService httpService) { this.httpService = null; } + + @Reference + protected void setStorageService(StorageService storageService) { + this.storageService = storageService; + } + + protected void unsetStorageService(StorageService storageService) { + this.storageService = null; + } } From 22bdc2708fb9fe8decbde61bbd98f23d3c54f86a Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 23 May 2018 20:28:00 +0200 Subject: [PATCH 54/56] [amazonechocontrol] Upgrade version for storage replacement with RC3 hint Signed-off-by: Michael Geramb (github: mgeramb) --- .../binding/amazonechocontrol/internal/BindingServlet.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java index ddbd31691206d..55c1bf10d9d52 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java @@ -31,7 +31,7 @@ /** * This servlet provides the base navigation page, with hyperlinks for the defined account things - * + * * @author Michael Geramb - Initial Contribution */ @NonNullByDefault @@ -99,8 +99,9 @@ protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResp } StringBuilder html = new StringBuilder(); - html.append("" + StringEscapeUtils.escapeHtml(BINDING_NAME) + ""); - html.append("

" + StringEscapeUtils.escapeHtml(BINDING_NAME) + "

"); + html.append( + "" + StringEscapeUtils.escapeHtml(BINDING_NAME + " RC3") + ""); + html.append("

" + StringEscapeUtils.escapeHtml(BINDING_NAME + " RC3") + "

"); synchronized (accountHandlers) { if (accountHandlers.isEmpty()) { From eede1decc7bfcc0d49bf05c6477d4fbe6cd460a8 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 23 May 2018 20:59:07 +0200 Subject: [PATCH 55/56] [amazonechocontrol] StateStorage class removed Signed-off-by: Michael Geramb (github: mgeramb) --- .../binding/amazonechocontrol/handler/AccountHandler.java | 7 ------- .../handler/FlashBriefingProfileHandler.java | 7 ------- .../binding/amazonechocontrol/internal/BindingServlet.java | 5 ++--- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java index 50b6782f86077..394500036e123 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/AccountHandler.java @@ -39,7 +39,6 @@ import org.openhab.binding.amazonechocontrol.internal.Connection; import org.openhab.binding.amazonechocontrol.internal.ConnectionException; import org.openhab.binding.amazonechocontrol.internal.HttpException; -import org.openhab.binding.amazonechocontrol.internal.StateStorage; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; @@ -77,12 +76,6 @@ public AccountHandler(Bridge bridge, HttpService httpService, Storage st super(bridge); this.httpService = httpService; this.stateStorage = stateStorage; - StateStorage oldStorage = new StateStorage(thing); - String sessionStorage = oldStorage.findState("sessionStorage"); - if (sessionStorage != null) { - this.stateStorage.put("sessionStorage", sessionStorage); - oldStorage.storeState("sessionStorage", null); - } } @Override diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java index 26e8950f085d9..e842dcd721c76 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/handler/FlashBriefingProfileHandler.java @@ -29,7 +29,6 @@ import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; import org.openhab.binding.amazonechocontrol.internal.Connection; -import org.openhab.binding.amazonechocontrol.internal.StateStorage; import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,12 +53,6 @@ public class FlashBriefingProfileHandler extends BaseThingHandler { public FlashBriefingProfileHandler(Thing thing, Storage storage) { super(thing); this.stateStorage = storage; - StateStorage oldStorage = new StateStorage(thing); - String configurationJson = oldStorage.findState("configurationJson"); - if (configurationJson != null) { - this.stateStorage.put("configurationJson", configurationJson); - oldStorage.storeState("configurationJson", null); - } } public @Nullable AccountHandler findAccountHandler() { diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java index 55c1bf10d9d52..ad5db34ed80c6 100644 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java @@ -99,9 +99,8 @@ protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResp } StringBuilder html = new StringBuilder(); - html.append( - "" + StringEscapeUtils.escapeHtml(BINDING_NAME + " RC3") + ""); - html.append("

" + StringEscapeUtils.escapeHtml(BINDING_NAME + " RC3") + "

"); + html.append("" + StringEscapeUtils.escapeHtml(BINDING_NAME) + ""); + html.append("

" + StringEscapeUtils.escapeHtml(BINDING_NAME) + "

"); synchronized (accountHandlers) { if (accountHandlers.isEmpty()) { From d03fdb89e8cabca2c29c2378102c435b5410ae48 Mon Sep 17 00:00:00 2001 From: Michael Geramb Date: Wed, 23 May 2018 21:15:21 +0200 Subject: [PATCH 56/56] [amazonechocontrol] Jenkins null pointer access warning fixed Signed-off-by: Michael Geramb (github: mgeramb) --- .../internal/AmazonEchoControlHandlerFactory.java | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 86686c3a7b391..824f912fe48b1 100755 --- a/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/addons/binding/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -99,6 +99,7 @@ protected void deactivate(ComponentContext componentContext) { String.class.getClassLoader()); AccountHandler bridgeHandler = new AccountHandler((Bridge) thing, httpService, storage); registerDiscoveryService(bridgeHandler); + BindingServlet bindingServlet = this.bindingServlet; if (bindingServlet != null) { bindingServlet.addAccountThing(thing); }