mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
48b471a313
commit
45052a810c
@ -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) |
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user