[somfytahoma] added support for the control over the LAN (local mode) (#13411)

* [somfytahoma] added support for the control over the LAN (local mode)

Signed-off-by: Ondrej Pecta <opecta@gmail.com>
This commit is contained in:
Ondrej Pecta 2022-12-11 12:07:25 +01:00 committed by GitHub
parent 48b471a313
commit 45052a810c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 646 additions and 103 deletions

View File

@ -45,7 +45,7 @@ Any home automation system based on the OverKiz API is potentially supported.
- water heater system (monitor and control)
- Yutaki heat pump consisting of heat pump, heating control and hot water tank (controls and a lot of states; it is tested with components RAS-4WHNPE, RWM-4.ONE, DHWT-300S-3.0H2E)
Both Somfy Tahoma and Somfy Connexoon gateways have been confirmed working.
Both Somfy Tahoma and Somfy Connexoon gateways have been confirmed working in the cloud mode.
## Discovery
@ -59,7 +59,41 @@ If you are missing some device, check the debug log during the discovery and cre
## Thing Configuration
To retrieve thing configuration and url parameter, just add the automatically discovered device from your inbox and copy its values from thing edit page. (the url parameter is visible on edit page only)
### bridge
| Parameter | Parameter ID | Required/Optional | Description |
|----------------|---------------|-------------------|--------------------------------------------------------------------------------------|
| Cloud portal | cloudPortal | Optional | Cloud portal to connect to |
| Email address | email | Required | Email address for the portal |
| Password | password | Required | Password for the portal |
| Refresh | refresh | Optional | Refresh time for polling events (in seconds) |
| Status timeout | statusTimeout | Optional | Reconciliation timeout after which the status is refreshed (in seconds) |
| Retries | retries | Optional | Specifies the number of retries when command execution |
| Retry delay | retryDelay | Optional | Delay in milliseconds between subsequent retries after a command failure |
| Developer mode | devMode | Optional | Enables the direct control of your devices over the lan using the local API endpoint |
| Gateway IP | ip | Optional | Local IP address of gateway, relevant only if developer mode is enabled |
| Gateway PIN | pin | Optional | Gateway PIN in format ABCD-EFGH-IJKL, relevant only if developer mode is enabled |
| Local token | token | Optional | Token for local communication, relevant only if developer mode is enabled |
For more information about the developer mode please see https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode.
If the gateway ip or pin are not provided, the binding tries to detect it automatically and saves it into the configuration.
If the local token is not provided, the binding creates the local token automatically and saves it into the configuration.
Please note that the action groups (scenarios) control does not work in local mode due to missing support in the gateway firmware.
The gateway support for the developer mode is limited as well, so far Connexoon gateways do not support the developer mode.
### gateway
| Parameter | Parameter ID | Required/Optional | Description |
|------------|--------------|-------------------|-------------------------------------------|
| Gateway id | id | Required | ID of your gateway (sometimes called pin) |
### other devices
| Parameter | Parameter ID | Required/Optional | Description |
|------------|--------------|-------------------|------------------------------|
| Device URL | url | Required | The identifier of the device |
To retrieve the url parameter or gateway id, just add the automatically discovered device from your inbox and copy its values from thing edit page. (the url parameter is visible on edit page only)
Please see the example below.
## Channels
@ -73,7 +107,7 @@ Please see the example below.
| gate | gate_state | get state of your gate (open, closed, pedestrian) |
| gate | gate_position | get position (0-100%) of your gate (where supported) |
| roller shutter, shutter, screen, ven. blind, garage door, awning, pergola, curtain | control | device controller which reacts to commands UP/DOWN/ON/OFF/OPEN/CLOSE/MY/STOP + closure 0-100 |
| roller shutter | moving | Indicates if the device is currently operating a command |
| roller shutter | moving | Indicates if the device is currently operating a command |
| window | control | device controller which reacts to commands UP/DOWN/ON/OFF/OPEN/CLOSE/STOP + closure 0-100 |
| silent roller shutter | silent_control | similar to control channel but in silent mode |
| venetian blind, adjustable slats roller shutter, bioclimatic pergola | orientation | percentual orientation of the blind's slats, it can have value 0-100. For IO Homecontrol devices only (non RTS) |
@ -150,7 +184,7 @@ Please see the example below.
| hitachi (yutaki) air to water heating zone | zone_mode | sets the zone mode (Auto, Manual) |
| hitachi (yutaki) air to water heating zone | thermostat_setting_zone1 | controls the thermostat setting for the zone 1 |
| hitachi (yutaki) air to water heating zone | wh_setting_temp_zone1 | controls the water heating setting temperature for the zone 1 |
| hitachi (yutaki) air to water heating zone | room_ambient_temp_zone1 | controls the room ambient temperature for the zone 1 |
| hitachi (yutaki) air to water heating zone | room_ambient_temp_zone1 | controls the room ambient temperature for the zone 1 |
| hitachi (yutaki) domestic hot water | anti_legionella | controls the anti legionella mode (Run, Stop) |
| hitachi (yutaki) domestic hot water | anti_legionella_temp | controls the anti legionella temperature |
| hitachi (yutaki) domestic hot water | target_boost_mode | controls the boost mode (No request, Enabled, Disabled) |

View File

@ -291,7 +291,6 @@ public class SomfyTahomaBindingConstants {
public static final String POWER_HEAT_PUMP = "power_heatpump";
public static final String POWER_HEAT_ELEC = "power_heatelec";
public static final String WATER_HEATER_MODE = "mode";
public static final String WATER_TEMPERATURE = "water_temperature";
public static final String ELECTRIC_BOOSTER_OPERATING_TIME = "electric_booster_operating_time";
public static final String SHOWERS = "showers";
@ -367,12 +366,15 @@ public class SomfyTahomaBindingConstants {
public static final String API_BASE_URL = "/enduser-mobile-web/enduserAPI/";
public static final String EVENTS_URL = "events/";
public static final String SETUP_URL = "setup/";
public static final String CONFIG_URL = "config/";
public static final String GATEWAYS_URL = SETUP_URL + "gateways/";
public static final String DEVICES_URL = SETUP_URL + "devices/";
public static final String REFRESH_URL = DEVICES_URL + "states/refresh";
public static final String EXEC_URL = "exec/";
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 LOCAL_TOKENS_URL = "/local/tokens/";
public static final String TAHOMA_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.82 Safari/537.36";
public static final int TAHOMA_TIMEOUT = 5;
public static final String UNAUTHORIZED = "Not logged in";
public static final int TYPE_NONE = 0;
@ -381,9 +383,13 @@ public class SomfyTahomaBindingConstants {
public static final int TYPE_STRING = 3;
public static final int TYPE_BOOLEAN = 6;
public static final String UNAVAILABLE = "unavailable";
public static final String AUTHENTICATION_CHALLENGE = "HTTP protocol violation: Authentication challenge without WWW-Authenticate header";
public static final String AUTHENTICATION_OAUTH_GRANT_ERROR = "Provided Authorization Grant is invalid.";
public static final String TEMPORARILY_BANNED = "Too many attempts with an invalid token, temporarily banned.";
public static final String TOO_MANY_REQUESTS = "Too many requests, try again later";
public static final String EVENT_LISTENER_TIMEOUT = "No registered event listener";
public static final String AUTHENTICATION_OAUTH_GRANT_ERROR = "Provided Authorization Grant is invalid.";
public static final String AUTHENTICATION_OAUTH_INVALID_GRANT = "error.invalid.grant";
public static final String OPENHAB_TOKEN = "openHAB token";
public static final int SUSPEND_TIME = 120;
public static final int RECONCILIATION_TIME = 600;
@ -449,6 +455,7 @@ public class SomfyTahomaBindingConstants {
public static final String RADIO_PART_BATTERY_STATE = "io:MaintenanceRadioPartBatteryState";
public static final String SENSOR_PART_BATTERY_STATE = "io:MaintenanceSensorPartBatteryState";
public static final String ZWAVE_SET_POINT_TYPE_STATE = "zwave:SetPointTypeState";
public static final String LUMINANCE_STATE = "core:LuminanceState";
// supported uiClasses
public static final String CLASS_ROLLER_SHUTTER = "RollerShutter";

View File

@ -31,6 +31,10 @@ public class SomfyTahomaConfig {
private int statusTimeout = 300;
private int retries = 10;
private int retryDelay = 1000;
private boolean devMode = false;
private String pin = "";
private String ip = "";
private String token = "";
public String getCloudPortal() {
return cloudPortal;
@ -60,6 +64,22 @@ public class SomfyTahomaConfig {
return retryDelay;
}
public boolean isDevMode() {
return devMode;
}
public String getPin() {
return pin;
}
public String getIp() {
return ip;
}
public String getToken() {
return token;
}
public void setCloudPortal(String cloudPortal) {
this.cloudPortal = cloudPortal;
}
@ -79,4 +99,16 @@ public class SomfyTahomaConfig {
public void setRetryDelay(int retryDelay) {
this.retryDelay = retryDelay;
}
public void setPin(String pin) {
this.pin = pin;
}
public void setIp(String ip) {
this.ip = ip;
}
public void setToken(String token) {
this.token = token;
}
}

View File

@ -104,7 +104,7 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
protected void stopBackgroundDiscovery() {
logger.debug("Stopping SomfyTahoma background discovery");
ScheduledFuture<?> localDiscoveryJob = discoveryJob;
if (localDiscoveryJob != null && !localDiscoveryJob.isCancelled()) {
if (localDiscoveryJob != null) {
localDiscoveryJob.cancel(true);
}
}
@ -137,14 +137,17 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
gatewayDiscovered(gw);
}
List<SomfyTahomaActionGroup> actions = localBridgeHandler.listActionGroups();
// local mode does not have action groups
if (!localBridgeHandler.isDevModeReady()) {
List<SomfyTahomaActionGroup> actions = localBridgeHandler.listActionGroups();
for (SomfyTahomaActionGroup group : actions) {
String oid = group.getOid();
String label = group.getLabel();
for (SomfyTahomaActionGroup group : actions) {
String oid = group.getOid();
String label = group.getLabel();
// actiongroups use oid as deviceURL
actionGroupDiscovered(label, oid, oid);
// actiongroups use oid as deviceURL
actionGroupDiscovered(label, oid);
}
}
} else {
logger.debug("Cannot start discovery since the bridge is not online!");
@ -154,7 +157,8 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
private void discoverDevice(SomfyTahomaDevice device, SomfyTahomaSetup setup) {
logger.debug("url: {}", device.getDeviceURL());
String place = getPlaceLabel(setup, device.getPlaceOID());
switch (device.getUiClass()) {
String widget = device.getDefinition().getWidgetName();
switch (device.getDefinition().getUiClass()) {
case CLASS_AWNING:
// widget: PositionableHorizontalAwning
// widget: DynamicAwning
@ -180,7 +184,7 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
deviceDiscovered(device, THING_TYPE_GARAGEDOOR, place);
break;
case CLASS_LIGHT:
if ("DimmerLight".equals(device.getWidget()) || "DynamicLight".equals(device.getWidget())) {
if ("DimmerLight".equals(widget) || "DynamicLight".equals(widget)) {
// widget: DimmerLight
// widget: DynamicLight
deviceDiscovered(device, THING_TYPE_DIMMER_LIGHT, place);
@ -243,7 +247,7 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
if (device.getDeviceURL().startsWith("internal:")) {
// widget: TSKAlarmController
deviceDiscovered(device, THING_TYPE_INTERNAL_ALARM, place);
} else if ("MyFoxAlarmController".equals(device.getWidget())) {
} else if ("MyFoxAlarmController".equals(widget)) {
// widget: MyFoxAlarmController
deviceDiscovered(device, THING_TYPE_MYFOX_ALARM, place);
} else {
@ -256,9 +260,9 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
}
break;
case CLASS_HEATING_SYSTEM:
if ("SomfyThermostat".equals(device.getWidget())) {
if ("SomfyThermostat".equals(widget)) {
deviceDiscovered(device, THING_TYPE_THERMOSTAT, place);
} else if ("ValveHeatingTemperatureInterface".equals(device.getWidget())) {
} else if ("ValveHeatingTemperatureInterface".equals(widget)) {
deviceDiscovered(device, THING_TYPE_VALVE_HEATING_SYSTEM, place);
} else if (isOnOffHeatingSystem(device)) {
deviceDiscovered(device, THING_TYPE_ONOFF_HEATING_SYSTEM, place);
@ -269,7 +273,7 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
}
break;
case CLASS_EXTERIOR_HEATING_SYSTEM:
if ("DimmerExteriorHeating".equals(device.getWidget())) {
if ("DimmerExteriorHeating".equals(widget)) {
// widget: DimmerExteriorHeating
deviceDiscovered(device, THING_TYPE_EXTERIOR_HEATING_SYSTEM, place);
} else {
@ -288,7 +292,7 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
deviceDiscovered(device, THING_TYPE_DOOR_LOCK, place);
break;
case CLASS_PERGOLA:
if ("BioclimaticPergola".equals(device.getWidget())) {
if ("BioclimaticPergola".equals(widget)) {
// widget: BioclimaticPergola
deviceDiscovered(device, THING_TYPE_BIOCLIMATIC_PERGOLA, place);
} else {
@ -315,7 +319,7 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
break;
case CLASS_WATER_HEATING_SYSTEM:
// widget: DomesticHotWaterProduction
if ("DomesticHotWaterProduction".equals(device.getWidget())) {
if ("DomesticHotWaterProduction".equals(widget)) {
deviceDiscovered(device, THING_TYPE_WATERHEATINGSYSTEM, place);
} else {
logUnsupportedDevice(device);
@ -340,13 +344,13 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
}
break;
case CLASS_HITACHI_HEATING_SYSTEM:
if ("HitachiAirToWaterHeatingZone".equals(device.getWidget())) {
if ("HitachiAirToWaterHeatingZone".equals(widget)) {
// widget: HitachiAirToWaterHeatingZone
deviceDiscovered(device, THING_TYPE_HITACHI_ATWHZ, place);
} else if ("HitachiAirToWaterMainComponent".equals(device.getWidget())) {
} else if ("HitachiAirToWaterMainComponent".equals(widget)) {
// widget: HitachiAirToWaterMainComponent
deviceDiscovered(device, THING_TYPE_HITACHI_ATWMC, place);
} else if ("HitachiDHW".equals(device.getWidget())) {
} else if ("HitachiDHW".equals(widget)) {
// widget: HitachiDHW
deviceDiscovered(device, THING_TYPE_HITACHI_DHW, place);
} else {
@ -354,7 +358,7 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
}
break;
case CLASS_RAIN_SENSOR:
if ("RainSensor".equals(device.getWidget())) {
if ("RainSensor".equals(widget)) {
// widget: RainSensor
deviceDiscovered(device, THING_TYPE_RAINSENSOR, place);
} else {
@ -391,8 +395,8 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
private void logUnsupportedDevice(SomfyTahomaDevice device) {
if (!isStateLess(device)) {
logger.debug("Detected a new unsupported device: {} with widgetName: {}", device.getUiClass(),
device.getWidget());
logger.debug("Detected a new unsupported device: {} with widgetName: {}",
device.getDefinition().getUiClass(), device.getDefinition().getWidgetName());
logger.debug("If you want to add the support, please create a new issue and attach the information below");
logger.debug("Device definition:\n{}", device.getDefinition());
@ -417,11 +421,11 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
}
private boolean isSilentRollerShutter(SomfyTahomaDevice device) {
return "PositionableRollerShutterWithLowSpeedManagement".equals(device.getWidget());
return "PositionableRollerShutterWithLowSpeedManagement".equals(device.getDefinition().getWidgetName());
}
private boolean isUnoRollerShutter(SomfyTahomaDevice device) {
return "PositionableRollerShutterUno".equals(device.getWidget());
return "PositionableRollerShutterUno".equals(device.getDefinition().getWidgetName());
}
private boolean isOnOffHeatingSystem(SomfyTahomaDevice device) {
@ -441,11 +445,10 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
if (place != null && !place.isBlank()) {
label += " (" + place + ")";
}
deviceDiscovered(label, device.getDeviceURL(), device.getOid(), thingTypeUID,
hasState(device, RSSI_LEVEL_STATE));
deviceDiscovered(label, device.getDeviceURL(), thingTypeUID, hasState(device, RSSI_LEVEL_STATE));
}
private void deviceDiscovered(String label, String deviceURL, String oid, ThingTypeUID thingTypeUID, boolean rssi) {
private void deviceDiscovered(String label, String deviceURL, ThingTypeUID thingTypeUID, boolean rssi) {
Map<String, Object> properties = new HashMap<>();
properties.put("url", deviceURL);
properties.put(NAME_STATE, label);
@ -455,17 +458,18 @@ public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
SomfyTahomaBridgeHandler localBridgeHandler = bridgeHandler;
if (localBridgeHandler != null) {
ThingUID thingUID = new ThingUID(thingTypeUID, localBridgeHandler.getThing().getUID(), oid);
ThingUID thingUID = new ThingUID(thingTypeUID, localBridgeHandler.getThing().getUID(),
deviceURL.replaceAll("[^a-zA-Z0-9_]", ""));
logger.debug("Detected a/an {} - label: {} oid: {}", thingTypeUID.getId(), label, oid);
logger.debug("Detected a/an {} - label: {} device URL: {}", thingTypeUID.getId(), label, deviceURL);
thingDiscovered(DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
.withProperties(properties).withRepresentationProperty("url").withLabel(label)
.withBridge(localBridgeHandler.getThing().getUID()).build());
}
}
private void actionGroupDiscovered(String label, String deviceURL, String oid) {
deviceDiscovered(label, deviceURL, oid, THING_TYPE_ACTIONGROUP, false);
private void actionGroupDiscovered(String label, String deviceURL) {
deviceDiscovered(label, deviceURL, THING_TYPE_ACTIONGROUP, false);
}
private void gatewayDiscovered(SomfyTahomaGateway gw) {

View File

@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.somfytahoma.internal.discovery;
import java.util.Enumeration;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.somfytahoma.internal.handler.SomfyTahomaBridgeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SomfyTahomaMDNSDiscoveryListener} represents a mDNS listener
* for a mDNS discovery.
*
* @author Ondrej Pecta - Initial contribution
*/
@NonNullByDefault
public class SomfyTahomaMDNSDiscoveryListener implements ServiceListener {
private final Logger logger = LoggerFactory.getLogger(SomfyTahomaMDNSDiscoveryListener.class);
private final SomfyTahomaBridgeHandler handler;
public SomfyTahomaMDNSDiscoveryListener(SomfyTahomaBridgeHandler handler) {
this.handler = handler;
}
@Override
public void serviceAdded(@Nullable ServiceEvent event) {
if (event != null) {
logger.trace("Service added: {}", event.getInfo());
}
}
@Override
public void serviceRemoved(@Nullable ServiceEvent event) {
if (event != null) {
logger.trace("Service removed: {}", event.getInfo());
}
}
@Override
public void serviceResolved(@Nullable ServiceEvent event) {
if (event == null || event.getInfo() == null) {
logger.debug("Null event received");
return;
}
ServiceInfo info = event.getInfo();
logger.trace("Service resolved: {}", info);
if (info.getInet4Addresses().length > 0) {
logger.debug("Server address: {}", info.getInet4Addresses()[0].getHostAddress());
handler.setGatewayIPAddress(info.getInet4Addresses()[0].getHostAddress());
}
Enumeration<String> e = info.getPropertyNames();
if (e != null) {
while (e.hasMoreElements()) {
String name = e.nextElement();
if ("gateway_pin".equals(name)) {
String pin = info.getPropertyString(name);
logger.debug("Gateway PIN: {}", pin);
handler.setGatewayPin(pin);
handler.updateConfiguration();
break;
}
}
}
}
}

View File

@ -14,6 +14,8 @@ package org.openhab.binding.somfytahoma.internal.handler;
import static org.openhab.binding.somfytahoma.internal.SomfyTahomaBindingConstants.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
@ -28,23 +30,29 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.jmdns.JmDNS;
import javax.ws.rs.core.MediaType;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.WWWAuthenticationProtocolHandler;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.somfytahoma.internal.config.SomfyTahomaConfig;
import org.openhab.binding.somfytahoma.internal.discovery.SomfyTahomaItemDiscoveryService;
import org.openhab.binding.somfytahoma.internal.discovery.SomfyTahomaMDNSDiscoveryListener;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaAction;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaActionGroup;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaApplyResponse;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaDevice;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaError;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaEvent;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaLocalToken;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaLoginResponse;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaOauth2Error;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaOauth2Reponse;
@ -53,7 +61,9 @@ import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaSetup;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaState;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaStatus;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaStatusResponse;
import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaTokenReponse;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@ -86,7 +96,7 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
/**
* The shared HttpClient
*/
private final HttpClient httpClient;
private @Nullable HttpClient httpClient;
/**
* Future to poll for updates
@ -103,6 +113,11 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
*/
private @Nullable ScheduledFuture<?> reconciliationFuture;
/**
* Future for postponed login
*/
private @Nullable ScheduledFuture<?> loginFuture;
// List of futures used for command retries
private Collection<ScheduledFuture<?>> retryFutures = new ConcurrentLinkedQueue<ScheduledFuture<?>>();
@ -120,6 +135,9 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
// Reconciliation flag
private boolean reconciliation = false;
// Cloud fallback
private boolean cloudFallback = false;
/**
* Our configuration
*/
@ -130,6 +148,8 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
*/
private String eventsId = "";
private String localToken = "";
private Map<String, SomfyTahomaDevice> devicePlaces = new HashMap<>();
private ExpiringCache<List<SomfyTahomaDevice>> cachedDevices = new ExpiringCache<>(Duration.ofSeconds(30),
@ -138,9 +158,11 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
// Gson & parser
private final Gson gson = new Gson();
private final HttpClientFactory httpClientFactory;
public SomfyTahomaBridgeHandler(Bridge thing, HttpClientFactory httpClientFactory) {
super(thing);
this.httpClient = httpClientFactory.createHttpClient("somfy_" + thing.getUID().getId());
this.httpClientFactory = httpClientFactory;
}
@Override
@ -149,7 +171,24 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
thingConfig = getConfigAs(SomfyTahomaConfig.class);
createHttpClient();
scheduler.execute(() -> {
login();
initPolling();
logger.debug("Initialize done...");
});
}
private void createHttpClient() {
// let's create the right http client
if (thingConfig.isDevMode()) {
this.httpClient = new HttpClient(new SslContextFactory.Client(true));
} else {
this.httpClient = httpClientFactory.createHttpClient("somfy_" + thing.getUID().getId());
}
try {
httpClient.start();
@ -157,12 +196,8 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
logger.debug("Cannot start http client for: {}", thing.getBridgeUID().getId(), e);
return;
}
scheduler.execute(() -> {
login();
initPolling();
logger.debug("Initialize done...");
});
// Remove the WWWAuth protocol handler since Tahoma is not fully compliant
httpClient.getProtocolHandlers().remove(WWWAuthenticationProtocolHandler.NAME);
}
/**
@ -214,9 +249,9 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
}
reLoginNeeded = false;
cloudFallback = false;
try {
String urlParameters = "";
// if cozytouch, must use oauth server
@ -239,31 +274,37 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
SomfyTahomaLoginResponse data = gson.fromJson(response.getContentAsString(),
SomfyTahomaLoginResponse.class);
if (data == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Received invalid data (login)");
} else if (data.isSuccess()) {
logger.debug("SomfyTahoma version: {}", data.getVersion());
} else if (!data.getErrorCode().isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, data.getError());
if (data.getError().startsWith(TOO_MANY_REQUESTS)) {
setTooManyRequests();
}
} else {
if (thingConfig.isDevMode()) {
initializeLocalMode();
}
String id = registerEvents();
if (id != null && !UNAUTHORIZED.equals(id)) {
eventsId = id;
logger.debug("Events id: {}", eventsId);
updateStatus(ThingStatus.ONLINE);
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE,
isDevModeReady() ? "LAN mode" : cloudFallback ? "Cloud mode fallback" : "Cloud mode");
} else {
logger.debug("Events id error: {}", id);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Error logging in: " + data.getError());
if (data.getError().startsWith(TOO_MANY_REQUESTS)) {
setTooManyRequests();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"unable to register events");
}
}
} catch (JsonSyntaxException e) {
logger.debug("Received invalid data (login)", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data (login)");
} catch (ExecutionException e) {
if (isAuthenticationChallenge(e) || isOAuthGrantError(e)) {
if (isOAuthGrantError(e)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Error logging in (check your credentials)");
setTooManyRequests();
@ -280,11 +321,106 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
}
}
public boolean isDevModeReady() {
return thingConfig.isDevMode() && !localToken.isEmpty() && !cloudFallback;
}
private void initializeLocalMode() {
if (thingConfig.getIp().isEmpty() || thingConfig.getPin().isEmpty()) {
discoverGateway();
}
if (!thingConfig.getIp().isEmpty() && !thingConfig.getPin().isEmpty()) {
try {
if (thingConfig.getToken().isEmpty()) {
localToken = getNewLocalToken();
logger.debug("Local token retrieved");
activateLocalToken();
updateConfiguration();
} else {
localToken = thingConfig.getToken();
activateLocalToken();
}
logger.debug("Local mode initialized, waiting for cloud sync");
Thread.sleep(3000);
} catch (InterruptedException ex) {
logger.debug("Interruption during local mode initialization, falling back to cloud mode", ex);
Thread.currentThread().interrupt();
} catch (ExecutionException | TimeoutException ex) {
logger.debug("Exception during local mode initialization, falling back to cloud mode", ex);
cloudFallback = true;
}
} else {
logger.debug("Cannot switch to developer mode - gateway not found on LAN");
cloudFallback = true;
}
}
private String getNewLocalToken() throws ExecutionException, InterruptedException, TimeoutException {
// Get list of local tokens
SomfyTahomaLocalToken[] tokens = invokeCallToURL(
CONFIG_URL + thingConfig.getPin() + LOCAL_TOKENS_URL + "devmode", "", HttpMethod.GET,
SomfyTahomaLocalToken[].class);
// Delete old OH tokens
for (SomfyTahomaLocalToken token : tokens) {
if (OPENHAB_TOKEN.equals(token.getLabel())) {
logger.debug("Deleting token: {}", token.getUuid());
sendDeleteToTahomaWithCookie(CONFIG_URL + thingConfig.getPin() + LOCAL_TOKENS_URL + token.getUuid());
}
}
// Generate a new token
SomfyTahomaTokenReponse tokenResponse = invokeCallToURL(
CONFIG_URL + thingConfig.getPin() + LOCAL_TOKENS_URL + "generate", "", HttpMethod.GET,
SomfyTahomaTokenReponse.class);
return tokenResponse.getToken();
}
private void discoverGateway() {
logger.debug("Starting mDNS discovery...");
JmDNS jmdns = null;
try {
// Create a JmDNS instance
jmdns = JmDNS.create(InetAddress.getLocalHost());
jmdns.addServiceListener("_kizboxdev._tcp.local.", new SomfyTahomaMDNSDiscoveryListener(this));
// Wait a bit
Thread.sleep(TAHOMA_TIMEOUT * 1000);
} catch (InterruptedException e) {
logger.debug("mDNS discovery interrupted", e);
Thread.currentThread().interrupt();
} catch (IOException e) {
logger.debug("Exception during mDNS discovery", e);
}
if (jmdns != null) {
jmdns.unregisterAllServices();
try {
jmdns.close();
} catch (IOException e) {
// ignore
}
}
}
private void activateLocalToken() throws ExecutionException, InterruptedException, TimeoutException {
String param = "{\"label\" : \"" + OPENHAB_TOKEN + "\",\"token\" : \"" + localToken
+ "\",\"scope\" : \"devmode\"}";
String response = sendPostToTahomaWithCookie(CONFIG_URL + thingConfig.getPin() + "/local/tokens", param);
logger.trace("Local token activation: {}", response);
}
private void setTooManyRequests() {
logger.debug("Too many requests or bad credentials for the cloud portal, suspending activity for {} seconds",
SUSPEND_TIME);
tooManyRequests = true;
scheduler.schedule(this::enableLogin, SUSPEND_TIME, TimeUnit.SECONDS);
if (!tooManyRequests) {
logger.debug(
"Too many requests or bad credentials for the cloud portal, suspending activity for {} seconds",
SUSPEND_TIME);
tooManyRequests = true;
loginFuture = scheduler.schedule(this::enableLogin, SUSPEND_TIME, TimeUnit.SECONDS);
}
}
private @Nullable String registerEvents() {
@ -302,6 +438,10 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
}
private List<SomfyTahomaEvent> getEvents() {
if (eventsId.isEmpty()) {
return List.of();
}
SomfyTahomaEvent[] response = invokeCallToURL(EVENTS_URL + eventsId + "/fetch", "", HttpMethod.POST,
SomfyTahomaEvent[].class);
return response != null ? List.of(response) : List.of();
@ -331,11 +471,24 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
// cancel all scheduled retries
retryFutures.forEach(x -> x.cancel(false));
try {
httpClient.stop();
} catch (Exception e) {
logger.debug("Error during http client stopping", e);
ScheduledFuture<?> localLoginFuture = loginFuture;
if (localLoginFuture != null) {
localLoginFuture.cancel(true);
loginFuture = null;
}
HttpClient localHttpClient = httpClient;
if (localHttpClient != null) {
try {
localHttpClient.stop();
} catch (Exception e) {
logger.debug("Error during http client stopping", e);
}
httpClient = null;
}
// Clean access data
localToken = "";
}
@Override
@ -351,16 +504,19 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
*/
private void stopPolling() {
ScheduledFuture<?> localPollFuture = pollFuture;
if (localPollFuture != null && !localPollFuture.isCancelled()) {
if (localPollFuture != null) {
localPollFuture.cancel(true);
pollFuture = null;
}
ScheduledFuture<?> localStatusFuture = statusFuture;
if (localStatusFuture != null && !localStatusFuture.isCancelled()) {
if (localStatusFuture != null) {
localStatusFuture.cancel(true);
statusFuture = null;
}
ScheduledFuture<?> localReconciliationFuture = reconciliationFuture;
if (localReconciliationFuture != null && !localReconciliationFuture.isCancelled()) {
if (localReconciliationFuture != null) {
localReconciliationFuture.cancel(true);
reconciliationFuture = null;
}
}
@ -404,7 +560,7 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
if (!device.getPlaceOID().isEmpty()) {
SomfyTahomaDevice newDevice = new SomfyTahomaDevice();
newDevice.setPlaceOID(device.getPlaceOID());
newDevice.setWidget(device.getWidget());
newDevice.getDefinition().setWidgetName(device.getDefinition().getWidgetName());
devicePlaces.put(device.getDeviceURL(), newDevice);
}
}
@ -637,10 +793,10 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
private String sendMethodToTahomaWithCookie(String url, HttpMethod method, String urlParameters)
throws InterruptedException, ExecutionException, TimeoutException {
logger.trace("Sending {} to url: {} with data: {}", method.asString(), getApiFullUrl(url), urlParameters);
logger.debug("Sending {} to url: {} with data: {}", method.asString(), getApiFullUrl(url), urlParameters);
Request request = sendRequestBuilder(url, method);
if (!urlParameters.isEmpty()) {
request = request.content(new StringContentProvider(urlParameters), "application/json;charset=UTF-8");
request = request.content(new StringContentProvider(urlParameters), "application/json");
}
ContentResponse response = request.send();
@ -651,17 +807,44 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
if (response.getStatus() < 200 || response.getStatus() >= 300) {
logger.debug("Received unexpected status code: {}", response.getStatus());
if (response.getHeaders().contains(HttpHeader.CONTENT_TYPE)) {
if (response.getHeaders().getField(HttpHeader.CONTENT_TYPE).getValue()
.equalsIgnoreCase(MediaType.APPLICATION_JSON)) {
try {
SomfyTahomaError error = gson.fromJson(response.getContentAsString(), SomfyTahomaError.class);
throw new ExecutionException(error.getError(), null);
} catch (JsonSyntaxException e) {
}
}
}
throw new ExecutionException(
"Unknown http error " + response.getStatus() + " while attempting to send a message.", null);
}
return response.getContentAsString();
}
private Request sendRequestBuilder(String subUrl, HttpMethod method) {
return isLocalRequest(subUrl) ? sendRequestBuilderLocal(subUrl, method)
: sendRequestBuilderCloud(subUrl, method);
}
private boolean isLocalRequest(String subUrl) {
return isDevModeReady() && !subUrl.startsWith(CONFIG_URL);
}
private Request sendRequestBuilderCloud(String subUrl, HttpMethod method) {
return httpClient.newRequest(getApiFullUrl(subUrl)).method(method)
.header(HttpHeader.ACCEPT_LANGUAGE, "en-US,en").header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate")
.header("X-Requested-With", "XMLHttpRequest").timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS)
.agent(TAHOMA_AGENT);
}
private Request sendRequestBuilderLocal(String subUrl, HttpMethod method) {
return httpClient.newRequest(getApiFullUrl(subUrl)).method(method).accept("application/json")
.header(HttpHeader.AUTHORIZATION, "Bearer " + localToken);
}
/**
* Performs the login for Cozytouch using OAUTH2 authorization.
*
@ -720,7 +903,9 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
}
private String getApiFullUrl(String subUrl) {
return "https://" + thingConfig.getCloudPortal() + API_BASE_URL + subUrl;
return isLocalRequest(subUrl)
? "https://" + thingConfig.getIp() + ":8443/enduser-mobile-web/1/enduserAPI/" + subUrl
: "https://" + thingConfig.getCloudPortal() + API_BASE_URL + subUrl;
}
public void sendCommand(String io, String command, String params, String url) {
@ -781,7 +966,7 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
if (device != null && !device.getPlaceOID().isEmpty()) {
devicePlaces.forEach((deviceUrl, devicePlace) -> {
if (device.getPlaceOID().equals(devicePlace.getPlaceOID())
&& device.getWidget().equals(devicePlace.getWidget())) {
&& device.getDefinition().getWidgetName().equals(devicePlace.getDefinition().getWidgetName())) {
sendCommand(deviceUrl, command, params, url);
}
});
@ -832,6 +1017,7 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
private boolean reLogin() {
logger.debug("Doing relogin");
reLoginNeeded = true;
localToken = "";
login();
return ThingStatus.OFFLINE != thing.getStatus();
}
@ -850,28 +1036,53 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
}
public void forceGatewaySync() {
invokeCallToURL(REFRESH_URL, "", HttpMethod.PUT, null);
// refresh is valid only if in a cloud mode
if (!thingConfig.isDevMode() || localToken.isEmpty()) {
invokeCallToURL(REFRESH_URL, "", HttpMethod.PUT, null);
}
}
public SomfyTahomaStatus getTahomaStatus(String gatewayId) {
SomfyTahomaStatusResponse data = invokeCallToURL(GATEWAYS_URL + gatewayId, "", HttpMethod.GET,
SomfyTahomaStatusResponse.class);
if (data != null) {
logger.debug("Tahoma status: {}", data.getConnectivity().getStatus());
logger.debug("Tahoma protocol version: {}", data.getConnectivity().getProtocolVersion());
return data.getConnectivity();
SomfyTahomaStatusResponse status = null;
if (isDevModeReady()) {
// Local endpoint does not have a method for specific gateway
SomfyTahomaStatusResponse[] data = invokeCallToURL(GATEWAYS_URL, "", HttpMethod.GET,
SomfyTahomaStatusResponse[].class);
if (data != null) {
for (SomfyTahomaStatusResponse gatewayStatus : data) {
if (gatewayStatus.getGatewayId().equals(gatewayId)) {
status = gatewayStatus;
break;
}
}
}
} else {
status = invokeCallToURL(GATEWAYS_URL + gatewayId, "", HttpMethod.GET, SomfyTahomaStatusResponse.class);
}
if (status != null) {
logger.debug("Tahoma status: {}", status.getConnectivity().getStatus());
logger.debug("Tahoma protocol version: {}", status.getConnectivity().getProtocolVersion());
return status.getConnectivity();
}
return new SomfyTahomaStatus();
}
private boolean isAuthenticationChallenge(Exception ex) {
private boolean isTempBanned(Exception ex) {
String msg = ex.getMessage();
return msg != null && msg.contains(AUTHENTICATION_CHALLENGE);
return msg != null && msg.contains(TEMPORARILY_BANNED);
}
private boolean isEventListenerTimeout(Exception ex) {
String msg = ex.getMessage();
return msg != null && msg.contains(EVENT_LISTENER_TIMEOUT);
}
private boolean isOAuthGrantError(Exception ex) {
String msg = ex.getMessage();
return msg != null && msg.contains(AUTHENTICATION_OAUTH_GRANT_ERROR);
return msg != null
&& (msg.contains(AUTHENTICATION_OAUTH_GRANT_ERROR) || msg.contains(AUTHENTICATION_OAUTH_INVALID_GRANT));
}
@Override
@ -915,7 +1126,10 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
logger.debug("Received data: {} is not JSON", response, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
} catch (ExecutionException e) {
if (isAuthenticationChallenge(e)) {
if (isTempBanned(e)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Temporarily banned");
setTooManyRequests();
} else if (isEventListenerTimeout(e)) {
reLogin();
} else {
logger.debug("Cannot call url: {} with params: {}!", getApiFullUrl(url), urlParameters, e);
@ -930,4 +1144,22 @@ public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
}
return null;
}
public void setGatewayIPAddress(String gatewayIPAddress) {
thingConfig.setIp(gatewayIPAddress);
}
public void setGatewayPin(String gatewayPin) {
thingConfig.setPin(gatewayPin);
}
public void updateConfiguration() {
Configuration config = editConfiguration();
config.put("ip", thingConfig.getIp());
config.put("pin", thingConfig.getPin());
if (!localToken.isEmpty()) {
config.put("token", localToken);
}
updateConfiguration(config);
}
}

View File

@ -28,7 +28,9 @@ public class SomfyTahomaLightSensorHandler extends SomfyTahomaBaseThingHandler {
public SomfyTahomaLightSensorHandler(Thing thing) {
super(thing);
stateNames.put(LUMINANCE, "core:LuminanceState");
stateNames.put(LUMINANCE, LUMINANCE_STATE);
stateNames.put(SENSOR_DEFECT, SENSOR_DEFECT_STATE);
// override state type because the local server sends luminance in percent
cacheStateType(LUMINANCE_STATE, TYPE_DECIMAL);
}
}

View File

@ -12,8 +12,7 @@
*/
package org.openhab.binding.somfytahoma.internal.handler;
import static org.openhab.binding.somfytahoma.internal.SomfyTahomaBindingConstants.TEMPERATURE;
import static org.openhab.binding.somfytahoma.internal.SomfyTahomaBindingConstants.TYPE_DECIMAL;
import static org.openhab.binding.somfytahoma.internal.SomfyTahomaBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Thing;
@ -29,9 +28,9 @@ public class SomfyTahomaTemperatureSensorHandler extends SomfyTahomaBaseThingHan
public SomfyTahomaTemperatureSensorHandler(Thing thing) {
super(thing);
stateNames.put(TEMPERATURE, "core:TemperatureState");
stateNames.put(TEMPERATURE, TEMPERATURE_STATE);
// override state type because the cloud sends both percent & decimal
cacheStateType("core:TemperatureState", TYPE_DECIMAL);
cacheStateType(TEMPERATURE_STATE, TYPE_DECIMAL);
}
}

View File

@ -27,8 +27,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
@NonNullByDefault
public class SomfyTahomaDevice {
private String uiClass = "";
private String widget = "";
private String deviceURL = "";
private String label = "";
private String oid = "";
@ -49,18 +47,6 @@ public class SomfyTahomaDevice {
return oid;
}
public String getUiClass() {
return uiClass;
}
public String getWidget() {
return widget;
}
public void setWidget(String widget) {
this.widget = widget;
}
public SomfyTahomaDeviceDefinition getDefinition() {
return definition;
}

View File

@ -36,6 +36,22 @@ public class SomfyTahomaDeviceDefinition {
return states;
}
private String widgetName = "";
private String uiClass = "";
public String getWidgetName() {
return widgetName;
}
public String getUiClass() {
return uiClass;
}
public void setWidgetName(String widgetName) {
this.widgetName = widgetName;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.somfytahoma.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SomfyTahomaError} is used to parse error from API server.
*
* @author Ondrej Pecta - Initial contribution
*/
@NonNullByDefault
public class SomfyTahomaError {
private String error = "";
private String errorCode = "";
public String getError() {
return error;
}
public String getErrorCode() {
return errorCode;
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.somfytahoma.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SomfyTahomaLocalToken} is used to parse a local token.
*
* @author Ondrej Pecta - Initial contribution
*/
@NonNullByDefault
public class SomfyTahomaLocalToken {
String uuid = "";
String label = "";
public String getUuid() {
return uuid;
}
public String getLabel() {
return label;
}
}

View File

@ -27,6 +27,8 @@ public class SomfyTahomaLoginResponse {
private String version = "";
private String error = "";
private String errorCode = "";
public boolean isSuccess() {
return success;
}
@ -38,4 +40,8 @@ public class SomfyTahomaLoginResponse {
public String getError() {
return error;
}
public String getErrorCode() {
return errorCode;
}
}

View File

@ -23,9 +23,14 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
@NonNullByDefault
public class SomfyTahomaStatusResponse {
private String gatewayId = "";
private SomfyTahomaStatus connectivity = new SomfyTahomaStatus();
public SomfyTahomaStatus getConnectivity() {
return connectivity;
}
public String getGatewayId() {
return gatewayId;
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.somfytahoma.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SomfyTahomaTokenReponse} holds information about generated token
*
* @author Ondrej Pecta - Initial contribution
*/
@NonNullByDefault
public class SomfyTahomaTokenReponse {
private String token = "";
public String getToken() {
return token;
}
}

View File

@ -69,5 +69,35 @@
<description>Specifies the delay in milliseconds between subsequent retries after a command failure</description>
<default>1000</default>
</parameter>
<parameter name="devMode" type="boolean" required="false">
<label>Developer mode</label>
<description>Enables the direct control of your devices over the lan using the local API endpoint. See
https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="ip" type="text" required="false">
<label>Gateway IP</label>
<description>Local IP address of gateway. Relevant only if developer mode is enabled. If not provided, the binding
will try to autodetect it</description>
<advanced>true</advanced>
</parameter>
<parameter name="pin" type="text" required="false">
<label>Gateway PIN</label>
<description>Gateway PIN in format ABCD-EFGH-IJKL. Relevant only if developer mode is enabled. If not provided, the
binding will try to autodetect it</description>
<advanced>true</advanced>
</parameter>
<parameter name="token" type="text" required="false">
<label>Local token</label>
<description>Local token. Relevant only if developer mode is enabled. If not provided, the binding
will try to
generate it using the gateway IP and PIN</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -80,6 +80,14 @@ bridge-type.config.somfytahoma.bridge.retryDelay.label = Retry delay
bridge-type.config.somfytahoma.bridge.retryDelay.description = Specifies the delay in milliseconds between subsequent retries after a command failure
bridge-type.config.somfytahoma.bridge.statusTimeout.label = Status Timeout
bridge-type.config.somfytahoma.bridge.statusTimeout.description = Specifies the timeout in seconds after which the status is got from the cloud
bridge-type.config.somfytahoma.bridge.devMode.label = Developer mode
bridge-type.config.somfytahoma.bridge.devMode.description = Enables the direct control of your devices over the lan using the local API endpoint. See https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode
bridge-type.config.somfytahoma.bridge.ip.label = Gateway IP
bridge-type.config.somfytahoma.bridge.ip.description = Local IP address of gateway. Relevant only if developer mode is enabled. If not provided, the binding will try to autodetect it
bridge-type.config.somfytahoma.bridge.pin.label = Gateway PIN
bridge-type.config.somfytahoma.bridge.pin.description = Gateway PIN in format ABCD-EFGH-IJKL. Relevant only if developer mode is enabled. If not provided, the binding will try to autodetect it
bridge-type.config.somfytahoma.bridge.token.label = Local token
bridge-type.config.somfytahoma.bridge.token.description = Local token. Relevant only if developer mode is enabled. If not provided, the binding will try to generate it using the gateway IP and PIN
thing-type.config.somfytahoma.device.url.label = Somfy Tahoma Device URL
thing-type.config.somfytahoma.device.url.description = The identifier of this Somfy device
thing-type.config.somfytahoma.gateway.id.label = Somfy Tahoma Gateway ID