[somfytahoma] Open to other portals (#10611)

* [somfytahoma] Open to other portals

Signed-off-by: Laurent Garnier <lg.hc@free.fr>

* Review comment: suppress the advanced setting for cookie handling

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
lolodomo 2021-05-09 20:16:14 +02:00 committed by GitHub
parent 22eebc797a
commit 892221ccad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 80 additions and 44 deletions

View File

@ -5,13 +5,14 @@ This binding integrates the
and and
[Somfy Connexoon](https://www.somfy.fr/produits/1811429/) [Somfy Connexoon](https://www.somfy.fr/produits/1811429/)
home automation systems. home automation systems.
Any home automation system based on the OverKiz API is potentially supported.
## Supported Things ## Supported Things
Currently these things are supported: Currently these things are supported:
- bridge (Somfy Tahoma bridge, which can discover gateways, roller shutters, awnings, switches and action groups) - bridge (cloud bridge, which can discover gateways, roller shutters, awnings, switches and action groups)
- gateways (Somfy Tahoma gateway - gateway status) - gateways (gateway status)
- gates (control gate, get state) - gates (control gate, get state)
- roller shutters (UP, DOWN, STOP control of a roller shutter). IO Homecontrol devices are allowed to set exact position of a shutter (0-100%) - roller shutters (UP, DOWN, STOP control of a roller shutter). IO Homecontrol devices are allowed to set exact position of a shutter (0-100%)
- blinds (UP, DOWN, STOP control of a blind). IO Homecontrol devices are allowed to set exact position of a blinds (0-100%) as well as orientation of slats (0-100%) - blinds (UP, DOWN, STOP control of a blind). IO Homecontrol devices are allowed to set exact position of a blinds (0-100%) as well as orientation of slats (0-100%)
@ -46,10 +47,10 @@ Both Somfy Tahoma and Somfy Connexoon gateways have been confirmed working.
To start a discovery, just To start a discovery, just
- Add a new Somfy Tahoma bridge thing. - Add a new bridge thing.
- Configure the bridge with your email (login) and password to the TahomaLink cloud portal. - Configure the bridge selecting your cloud portal (www.tahomalink.com by default) and setting your email (login) and password to the cloud portal.
If the supplied TahomaLink credentials are correct, the automatic discovery can be used to scan and detect roller shutters, awnings, switches and action groups that will appear in your Inbox. If the supplied credentials are correct, the automatic discovery can be used to scan and detect roller shutters, awnings, switches and action groups that will appear in your Inbox.
## Thing Configuration ## Thing Configuration
@ -61,7 +62,7 @@ Please see the example below.
| Thing | Channel | Note | | Thing | Channel | Note |
|-------------------------------------------------------------------------------|------------------------------|-----------------------------------------------------------------------------------------------------------------------------| |-------------------------------------------------------------------------------|------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| bridge | N.A | bridge does not expose any channel | | bridge | N.A | bridge does not expose any channel |
| gateway | status | status of your Tahoma gateway | | gateway | status | status of your gateway |
| gateway | scenarios | used to run the scenarios defined in the cloud portal | | gateway | scenarios | used to run the scenarios defined in the cloud portal |
| gate | gate_command | used for controlling your gate (open, close, stop, pedestrian) | | gate | gate_command | used for controlling your gate (open, close, stop, pedestrian) |
| gate | gate_state | get state of your gate (open, closed, pedestrian) | | gate | gate_state | get state of your gate (open, closed, pedestrian) |

View File

@ -26,6 +26,7 @@ import org.openhab.core.thing.ThingTypeUID;
* used across the whole binding. * used across the whole binding.
* *
* @author Ondrej Pecta - Initial contribution * @author Ondrej Pecta - Initial contribution
* @author Laurent Garnier - Other portals integration
*/ */
@NonNullByDefault @NonNullByDefault
public class SomfyTahomaBindingConstants { public class SomfyTahomaBindingConstants {
@ -280,13 +281,14 @@ public class SomfyTahomaBindingConstants {
public static final String SHUTTER = "shutter"; public static final String SHUTTER = "shutter";
// Constants // Constants
public static final String TAHOMA_API_URL = "https://www.tahomalink.com/enduser-mobile-web/enduserAPI/"; public static final String TAHOMA_PORTAL = "www.tahomalink.com";
public static final String TAHOMA_EVENTS_URL = TAHOMA_API_URL + "events/"; public static final String API_BASE_URL = "/enduser-mobile-web/enduserAPI/";
public static final String SETUP_URL = TAHOMA_API_URL + "setup/"; public static final String EVENTS_URL = "events/";
public static final String SETUP_URL = "setup/";
public static final String GATEWAYS_URL = SETUP_URL + "gateways/"; public static final String GATEWAYS_URL = SETUP_URL + "gateways/";
public static final String DEVICES_URL = SETUP_URL + "devices/"; public static final String DEVICES_URL = SETUP_URL + "devices/";
public static final String REFRESH_URL = DEVICES_URL + "states/refresh"; public static final String REFRESH_URL = DEVICES_URL + "states/refresh";
public static final String EXEC_URL = TAHOMA_API_URL + "exec/"; public static final String EXEC_URL = "exec/";
public static final String DELETE_URL = EXEC_URL + "current/setup/"; public static final String DELETE_URL = EXEC_URL + "current/setup/";
public static final String TAHOMA_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"; public static final String TAHOMA_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36";
public static final int TAHOMA_TIMEOUT = 5; public static final int TAHOMA_TIMEOUT = 5;
@ -415,6 +417,7 @@ public class SomfyTahomaBindingConstants {
put(29, "TAHOMA_V2"); put(29, "TAHOMA_V2");
put(30, "KIZBOX_V2_3H"); put(30, "KIZBOX_V2_3H");
put(31, "KIZBOX_V2_2H"); put(31, "KIZBOX_V2_2H");
put(32, "COZYTOUCH");
put(34, "CONNEXOON"); put(34, "CONNEXOON");
put(35, "JSW_CAMERA"); put(35, "JSW_CAMERA");
put(37, "KIZBOX_MINI_DAUGHTERBOARD"); put(37, "KIZBOX_MINI_DAUGHTERBOARD");

View File

@ -13,15 +13,18 @@
package org.openhab.binding.somfytahoma.internal.config; package org.openhab.binding.somfytahoma.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.somfytahoma.internal.SomfyTahomaBindingConstants;
/** /**
* The {@link SomfyTahomaConfig} is is the base class for configuration * The {@link SomfyTahomaConfig} is is the base class for configuration
* information held by devices and modules. * information held by devices and modules.
* *
* @author Ondrej Pecta - Initial contribution * @author Ondrej Pecta - Initial contribution
* @author Laurent Garnier - new parameter portalUrl
*/ */
@NonNullByDefault @NonNullByDefault
public class SomfyTahomaConfig { public class SomfyTahomaConfig {
private String cloudPortal = SomfyTahomaBindingConstants.TAHOMA_PORTAL;
private String email = ""; private String email = "";
private String password = ""; private String password = "";
private int refresh = 30; private int refresh = 30;
@ -29,6 +32,10 @@ public class SomfyTahomaConfig {
private int retries = 10; private int retries = 10;
private int retryDelay = 1000; private int retryDelay = 1000;
public String getCloudPortal() {
return cloudPortal;
}
public String getEmail() { public String getEmail() {
return email; return email;
} }
@ -53,6 +60,10 @@ public class SomfyTahomaConfig {
return retryDelay; return retryDelay;
} }
public void setCloudPortal(String cloudPortal) {
this.cloudPortal = cloudPortal;
}
public void setEmail(String email) { public void setEmail(String email) {
this.email = email; this.email = email;
} }

View File

@ -73,6 +73,7 @@ import com.google.gson.JsonSyntaxException;
* sent to one of the channels. * sent to one of the channels.
* *
* @author Ondrej Pecta - Initial contribution * @author Ondrej Pecta - Initial contribution
* @author Laurent Garnier - Other portals integration
*/ */
@NonNullByDefault @NonNullByDefault
public class SomfyTahomaBridgeHandler extends BaseBridgeHandler { public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
@ -193,8 +194,6 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
} }
public synchronized void login() { public synchronized void login() {
String url;
if (thingConfig.getEmail().isEmpty() || thingConfig.getPassword().isEmpty()) { if (thingConfig.getEmail().isEmpty() || thingConfig.getPassword().isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Can not access device as username and/or password are null"); "Can not access device as username and/or password are null");
@ -214,11 +213,10 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
reLoginNeeded = false; reLoginNeeded = false;
try { try {
url = TAHOMA_API_URL + "login";
String urlParameters = "userId=" + urlEncode(thingConfig.getEmail()) + "&userPassword=" String urlParameters = "userId=" + urlEncode(thingConfig.getEmail()) + "&userPassword="
+ urlEncode(thingConfig.getPassword()); + urlEncode(thingConfig.getPassword());
ContentResponse response = sendRequestBuilder(url, HttpMethod.POST) ContentResponse response = sendRequestBuilder("login", HttpMethod.POST)
.content(new StringContentProvider(urlParameters), .content(new StringContentProvider(urlParameters),
"application/x-www-form-urlencoded; charset=UTF-8") "application/x-www-form-urlencoded; charset=UTF-8")
.send(); .send();
@ -235,7 +233,7 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
} else if (data.isSuccess()) { } else if (data.isSuccess()) {
logger.debug("SomfyTahoma version: {}", data.getVersion()); logger.debug("SomfyTahoma version: {}", data.getVersion());
String id = registerEvents(); String id = registerEvents();
if (id != null && !id.equals(UNAUTHORIZED)) { if (id != null && !UNAUTHORIZED.equals(id)) {
eventsId = id; eventsId = id;
logger.debug("Events id: {}", eventsId); logger.debug("Events id: {}", eventsId);
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
@ -254,7 +252,8 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data (login)"); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data (login)");
} catch (ExecutionException e) { } catch (ExecutionException e) {
if (isAuthenticationChallenge(e)) { if (isAuthenticationChallenge(e)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Authentication challenge"); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Error logging in (check your credentials)");
setTooManyRequests(); setTooManyRequests();
} else { } else {
logger.debug("Cannot get login cookie", e); logger.debug("Cannot get login cookie", e);
@ -270,14 +269,15 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
} }
private void setTooManyRequests() { private void setTooManyRequests() {
logger.debug("Too many requests error, suspending activity for {} seconds", SUSPEND_TIME); logger.debug("Too many requests or bad credentials for the cloud portal, suspending activity for {} seconds",
SUSPEND_TIME);
tooManyRequests = true; tooManyRequests = true;
scheduler.schedule(this::enableLogin, SUSPEND_TIME, TimeUnit.SECONDS); scheduler.schedule(this::enableLogin, SUSPEND_TIME, TimeUnit.SECONDS);
} }
private @Nullable String registerEvents() { private @Nullable String registerEvents() {
SomfyTahomaRegisterEventsResponse response = invokeCallToURL(TAHOMA_EVENTS_URL + "register", "", SomfyTahomaRegisterEventsResponse response = invokeCallToURL(EVENTS_URL + "register", "", HttpMethod.POST,
HttpMethod.POST, SomfyTahomaRegisterEventsResponse.class); SomfyTahomaRegisterEventsResponse.class);
return response != null ? response.getId() : null; return response != null ? response.getId() : null;
} }
@ -294,8 +294,8 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
} }
private List<SomfyTahomaEvent> getEvents() { private List<SomfyTahomaEvent> getEvents() {
SomfyTahomaEvent[] response = invokeCallToURL(TAHOMA_API_URL + "events/" + eventsId + "/fetch", "", SomfyTahomaEvent[] response = invokeCallToURL(EVENTS_URL + eventsId + "/fetch", "", HttpMethod.POST,
HttpMethod.POST, SomfyTahomaEvent[].class); SomfyTahomaEvent[].class);
return response != null ? List.of(response) : List.of(); return response != null ? List.of(response) : List.of();
} }
@ -357,13 +357,13 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
} }
public List<SomfyTahomaActionGroup> listActionGroups() { public List<SomfyTahomaActionGroup> listActionGroups() {
SomfyTahomaActionGroup[] list = invokeCallToURL(TAHOMA_API_URL + "actionGroups", "", HttpMethod.GET, SomfyTahomaActionGroup[] list = invokeCallToURL("actionGroups", "", HttpMethod.GET,
SomfyTahomaActionGroup[].class); SomfyTahomaActionGroup[].class);
return list != null ? List.of(list) : List.of(); return list != null ? List.of(list) : List.of();
} }
public @Nullable SomfyTahomaSetup getSetup() { public @Nullable SomfyTahomaSetup getSetup() {
SomfyTahomaSetup setup = invokeCallToURL(TAHOMA_API_URL + "setup", "", HttpMethod.GET, SomfyTahomaSetup.class); SomfyTahomaSetup setup = invokeCallToURL("setup", "", HttpMethod.GET, SomfyTahomaSetup.class);
if (setup != null) { if (setup != null) {
saveDevicePlaces(setup.getDevices()); saveDevicePlaces(setup.getDevices());
} }
@ -591,7 +591,7 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
private void logout() { private void logout() {
try { try {
eventsId = ""; eventsId = "";
sendGetToTahomaWithCookie(TAHOMA_API_URL + "logout"); sendGetToTahomaWithCookie("logout");
} catch (ExecutionException | TimeoutException e) { } catch (ExecutionException | TimeoutException e) {
logger.debug("Cannot send logout command!", e); logger.debug("Cannot send logout command!", e);
} catch (InterruptedException e) { } catch (InterruptedException e) {
@ -626,7 +626,7 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
private String sendMethodToTahomaWithCookie(String url, HttpMethod method, String urlParameters) private String sendMethodToTahomaWithCookie(String url, HttpMethod method, String urlParameters)
throws InterruptedException, ExecutionException, TimeoutException { throws InterruptedException, ExecutionException, TimeoutException {
logger.trace("Sending {} to url: {} with data: {}", method.asString(), url, urlParameters); logger.trace("Sending {} to url: {} with data: {}", method.asString(), getApiFullUrl(url), urlParameters);
Request request = sendRequestBuilder(url, method); Request request = sendRequestBuilder(url, method);
if (!urlParameters.isEmpty()) { if (!urlParameters.isEmpty()) {
request = request.content(new StringContentProvider(urlParameters), "application/json;charset=UTF-8"); request = request.content(new StringContentProvider(urlParameters), "application/json;charset=UTF-8");
@ -644,10 +644,15 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
return response.getContentAsString(); return response.getContentAsString();
} }
private Request sendRequestBuilder(String url, HttpMethod method) { private Request sendRequestBuilder(String subUrl, HttpMethod method) {
return httpClient.newRequest(url).method(method).header(HttpHeader.ACCEPT_LANGUAGE, "en-US,en") return httpClient.newRequest(getApiFullUrl(subUrl)).method(method)
.header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").header("X-Requested-With", "XMLHttpRequest") .header(HttpHeader.ACCEPT_LANGUAGE, "en-US,en").header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate")
.timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS).agent(TAHOMA_AGENT); .header("X-Requested-With", "XMLHttpRequest").timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS)
.agent(TAHOMA_AGENT);
}
private String getApiFullUrl(String subUrl) {
return "https://" + thingConfig.getCloudPortal() + API_BASE_URL + subUrl;
} }
public void sendCommand(String io, String command, String params, String url) { public void sendCommand(String io, String command, String params, String url) {
@ -672,7 +677,7 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
} }
private boolean sendCommandInternal(String io, String command, String params, String url) { private boolean sendCommandInternal(String io, String command, String params, String url) {
String value = params.equals("[]") ? command : command + " " + params.replace("\"", ""); String value = "[]".equals(params) ? command : command + " " + params.replace("\"", "");
String urlParameters = "{\"label\":\"" + getThingLabelByURL(io) + " - " + value String urlParameters = "{\"label\":\"" + getThingLabelByURL(io) + " - " + value
+ " - openHAB\",\"actions\":[{\"deviceURL\":\"" + io + "\",\"commands\":[{\"name\":\"" + command + " - openHAB\",\"actions\":[{\"deviceURL\":\"" + io + "\",\"commands\":[{\"name\":\"" + command
+ "\",\"parameters\":" + params + "}]}]}"; + "\",\"parameters\":" + params + "}]}]}";
@ -799,11 +804,10 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
@Override @Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) { public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
super.handleConfigurationUpdate(configurationParameters); super.handleConfigurationUpdate(configurationParameters);
if (configurationParameters.containsKey("email")) { if (configurationParameters.containsKey("email") || configurationParameters.containsKey("password")
thingConfig.setEmail(configurationParameters.get("email").toString()); || configurationParameters.containsKey("portalUrl")) {
} reLoginNeeded = true;
if (configurationParameters.containsKey("password")) { tooManyRequests = false;
thingConfig.setPassword(configurationParameters.get("password").toString());
} }
} }
@ -841,11 +845,11 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
if (isAuthenticationChallenge(e)) { if (isAuthenticationChallenge(e)) {
reLogin(); reLogin();
} else { } else {
logger.debug("Cannot call url: {} with params: {}!", url, urlParameters, e); logger.debug("Cannot call url: {} with params: {}!", getApiFullUrl(url), urlParameters, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
} }
} catch (TimeoutException e) { } catch (TimeoutException e) {
logger.debug("Timeout when calling url: {} with params: {}!", url, urlParameters, e); logger.debug("Timeout when calling url: {} with params: {}!", getApiFullUrl(url), urlParameters, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
} catch (InterruptedException e) { } catch (InterruptedException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);

View File

@ -4,6 +4,7 @@
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd"> xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>SomfyTahoma Binding</name> <name>SomfyTahoma Binding</name>
<description>This is the binding for SomfyTahoma.</description> <description>This is the binding for Somfy Tahoma and Somfy Connexoon home automation systems and for any other system
based on the OverKiz API.</description>
</binding:binding> </binding:binding>

View File

@ -19,26 +19,42 @@
</config-description> </config-description>
<config-description uri="bridge-type:somfytahoma:bridge"> <config-description uri="bridge-type:somfytahoma:bridge">
<parameter name="cloudPortal" type="text" required="false">
<label>Cloud Portal</label>
<description>Cloud portal to connect to</description>
<options>
<option value="www.tahomalink.com">Somfy TaHoma / Somfy Connexoon IO / Somfy (Europe)</option>
<option value="ha201-1.overkiz.com">Somfy Connexoon RTS / Somfy (Australia)</option>
<option value="ha401-1.overkiz.com">Somfy (North America)</option>
<option value="ha110-1.overkiz.com">Cozytouch</option>
<option value="ha101-1.overkiz.com">eedomus</option>
<option value="ha117-1.overkiz.com">Hi Kumo</option>
<option value="ha112-1.overkiz.com">Rexel Energeasy Connect</option>
</options>
<default>www.tahomalink.com</default>
<limitToOptions>false</limitToOptions>
</parameter>
<parameter name="email" type="text" required="true"> <parameter name="email" type="text" required="true">
<label>Email Address</label> <label>Email Address</label>
<description>Email address for TahomaLink portal</description> <description>Email address for the portal</description>
</parameter> </parameter>
<parameter name="password" type="text" required="true"> <parameter name="password" type="text" required="true">
<context>password</context> <context>password</context>
<label>Password</label> <label>Password</label>
<description>Password for TahomaLink portal</description> <description>Password for the portal</description>
</parameter> </parameter>
<parameter name="refresh" type="integer" required="false" min="10"> <parameter name="refresh" type="integer" required="false" min="10">
<label>Refresh</label> <label>Refresh</label>
<description>Specifies the refresh time in seconds for polling events from Tahoma cloud</description> <description>Specifies the refresh time in seconds for polling events from the cloud</description>
<default>30</default> <default>30</default>
</parameter> </parameter>
<parameter name="statusTimeout" type="integer" required="false" min="60"> <parameter name="statusTimeout" type="integer" required="false" min="60">
<label>Status Timeout</label> <label>Status Timeout</label>
<description>Specifies the timeout in seconds after which the status is got from Tahoma cloud</description> <description>Specifies the timeout in seconds after which the status is got from the cloud</description>
<default>300</default> <default>300</default>
</parameter> </parameter>

View File

@ -6,8 +6,8 @@
<!-- Bridge --> <!-- Bridge -->
<bridge-type id="bridge"> <bridge-type id="bridge">
<label>Somfy Tahoma Bridge</label> <label>Bridge</label>
<description>Somfy Tahoma bridge enabling communication with Somfy devices</description> <description>Bridge enabling communication with devices through a cloud portal</description>
<config-description-ref uri="bridge-type:somfytahoma:bridge"/> <config-description-ref uri="bridge-type:somfytahoma:bridge"/>
</bridge-type> </bridge-type>