mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[shelly] Add support for Range Extender feature (#16419)
* Add support for Shelly Range Extender mode (Plus/Pro series only) * Check for secondary devices also when manual scan is triggered Signed-off-by: Markus Michels <markus7017@gmail.com>
This commit is contained in:
parent
d2050f43a7
commit
afc6d949e8
@ -215,6 +215,27 @@ In this case the binding could directly access the device to retrieve the requir
|
||||
Otherwise a Thing of type shellyprotected is created in the Inbox and you could set the credentials while adding the Thing.
|
||||
In this case the credentials are persisted as part of the Thing configuration.
|
||||
|
||||
### Range Extender Mode
|
||||
|
||||
The Plus/Pro devices support the so-called Range Extender Mode (not available for Gen1).
|
||||
This allows connect Shellys, which are normally no reachable, because of a lack of WiFi signal.
|
||||
Once enabled the Shelly acts as a hub to the linked devices, like a WiFi repeater.
|
||||
The hub device enables the access point, which can be seen by the linked device.
|
||||
The binding could then get access to the secondary device using <ub shelly ip>:<special port>.
|
||||
A special port on the hub device will be created for every linked device so one hub device could supported multiple linked devices.
|
||||
|
||||
|
||||
The binding communicates with the Shelly hub device, which then forwards the request to the secondary device.
|
||||
Once the thing for the primary Shelly goes online the binding detects the enabled range extender mode and adds all connected secondary devices to the Inbox.
|
||||
This means: The primary Shelly has to complete initialization before linked secondary devices are discovered.
|
||||
|
||||
- Discover primary/hub Shelly
|
||||
- Add thing and wait until it goes ONLINE
|
||||
- Check Inbox to find the secondary/linked devices
|
||||
- Add secondary device as usual
|
||||
|
||||
If you are adding another secondary device to the same hub device you need to suspend and resume the primary thing, this will run a new initialization and adds the new secondary device to the Inbox.
|
||||
|
||||
### Dynamic creation of channels
|
||||
|
||||
The Shelly series of devices has many combinations of relays, meters (different versions), sensors etc.
|
||||
|
@ -17,6 +17,7 @@ import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyMotionSettings;
|
||||
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2APClientList;
|
||||
import org.openhab.core.thing.CommonTriggerEvents;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
@ -594,6 +595,7 @@ public class Shelly1ApiJsonDTO {
|
||||
public Boolean wifiRecoveryReboot; // FW 1.10+
|
||||
@SerializedName("ap_roaming")
|
||||
public ShellyApRoaming apRoaming; // FW 1.10+
|
||||
public Boolean rangeExtender; // Gen2: Range extender
|
||||
|
||||
public ShellySettingsMqtt mqtt = new ShellySettingsMqtt();
|
||||
public ShellySettingsSntp sntp = new ShellySettingsSntp();
|
||||
@ -742,6 +744,7 @@ public class Shelly1ApiJsonDTO {
|
||||
// /settings/sta for details
|
||||
public ShellyStatusCloud cloud = new ShellyStatusCloud();
|
||||
public ShellyStatusMqtt mqtt = new ShellyStatusMqtt();
|
||||
public Shelly2APClientList rangeExtender;
|
||||
|
||||
public String time;
|
||||
public Integer serial = -1;
|
||||
|
@ -55,6 +55,7 @@ public class Shelly2ApiJsonDTO {
|
||||
public static final String SHELLYRPC_METHOD_LED_SETCONFIG = "WD_UI.SetConfig";
|
||||
public static final String SHELLYRPC_METHOD_WIFIGETCONG = "Wifi.GetConfig";
|
||||
public static final String SHELLYRPC_METHOD_WIFISETCONG = "Wifi.SetConfig";
|
||||
public static final String SHELLYRPC_METHOD_WIFILISTAPCLIENTS = "WiFi.ListAPClients";
|
||||
public static final String SHELLYRPC_METHOD_ETHGETCONG = "Eth.GetConfig";
|
||||
public static final String SHELLYRPC_METHOD_ETHSETCONG = "Eth.SetConfig";
|
||||
public static final String SHELLYRPC_METHOD_BLEGETCONG = "BLE.GetConfig";
|
||||
@ -520,6 +521,21 @@ public class Shelly2ApiJsonDTO {
|
||||
public Shelly2GetConfigResult result;
|
||||
}
|
||||
|
||||
public static class Shelly2APClientList {
|
||||
public static class Shelly2APClient {
|
||||
public String mac;
|
||||
public String ip;
|
||||
@SerializedName("ip_static")
|
||||
public Boolean staticIP;
|
||||
public Integer mport;
|
||||
public Long since;
|
||||
}
|
||||
|
||||
public Long ts;
|
||||
@SerializedName("ap_clients")
|
||||
public ArrayList<Shelly2APClient> apClients;
|
||||
}
|
||||
|
||||
public static class Shelly2DeviceStatus {
|
||||
public class Shelly2InputCounts {
|
||||
public Integer total;
|
||||
|
@ -55,6 +55,7 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortSta
|
||||
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
|
||||
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
|
||||
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
|
||||
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2APClientList;
|
||||
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthChallenge;
|
||||
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2ConfigParms;
|
||||
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DeviceConfigSta;
|
||||
@ -148,7 +149,9 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
|
||||
@Override
|
||||
public void startScan() {
|
||||
try {
|
||||
installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway);
|
||||
if (getProfile().isBlu) {
|
||||
installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway);
|
||||
}
|
||||
} catch (ShellyApiException e) {
|
||||
}
|
||||
}
|
||||
@ -222,6 +225,9 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
|
||||
profile.settings.wifiSta1 = new ShellySettingsWiFiNetwork();
|
||||
fillWiFiSta(dc.wifi.sta, profile.settings.wifiSta);
|
||||
fillWiFiSta(dc.wifi.sta1, profile.settings.wifiSta1);
|
||||
if (dc.wifi.ap != null && dc.wifi.ap.rangeExtender != null) {
|
||||
profile.settings.rangeExtender = getBool(dc.wifi.ap.rangeExtender.enable);
|
||||
}
|
||||
|
||||
profile.numMeters = 0;
|
||||
if (profile.hasRelays) {
|
||||
@ -797,6 +803,19 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
|
||||
}
|
||||
|
||||
fillDeviceStatus(status, ds, false);
|
||||
if (getBool(profile.settings.rangeExtender)) {
|
||||
try {
|
||||
// Get List of AP clients
|
||||
profile.status.rangeExtender = apiRequest(SHELLYRPC_METHOD_WIFILISTAPCLIENTS, null,
|
||||
Shelly2APClientList.class);
|
||||
logger.debug("{}: Range extender is enabled, {} clients connected", thingName,
|
||||
profile.status.rangeExtender.apClients.size());
|
||||
} catch (ShellyApiException e) {
|
||||
logger.debug("{}: Range extender is enabled, but unable to read AP client list", thingName, e);
|
||||
profile.settings.rangeExtender = false;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
@ -45,4 +45,14 @@ public class ShellyThingConfiguration {
|
||||
public String serviceName = "";
|
||||
|
||||
public Boolean enableBluGateway = false;
|
||||
public Boolean enableRangeExtender = true;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Device address=" + deviceAddress + ", HTTP user/password=" + userId + "/"
|
||||
+ (password.isEmpty() ? "<none>" : "***") + ", update interval=" + updateInterval + "\n"
|
||||
+ "Events: Button: " + eventsButton + ", Switch (on/off): " + eventsSwitch + ", Push: " + eventsPush
|
||||
+ ", Roller: " + eventsRoller + "Sensor: " + eventsSensorReport + ", CoIoT: " + eventsCoIoT + "\n"
|
||||
+ "Blu Gateway=" + enableBluGateway + ", Range Extender: " + enableRangeExtender;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.shelly.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
|
||||
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
|
||||
import static org.openhab.core.thing.Thing.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.shelly.internal.api.ShellyApiException;
|
||||
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
|
||||
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
|
||||
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
|
||||
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
|
||||
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
|
||||
import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
|
||||
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
|
||||
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
|
||||
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
|
||||
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
|
||||
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Device discovery creates a thing in the inbox for each vehicle
|
||||
* found in the data received from {@link ShellyBasicDiscoveryService}.
|
||||
*
|
||||
* @author Markus Michels - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShellyBasicDiscoveryService extends AbstractDiscoveryService {
|
||||
private final Logger logger = LoggerFactory.getLogger(ShellyBasicDiscoveryService.class);
|
||||
|
||||
private final BundleContext bundleContext;
|
||||
private final ShellyThingTable thingTable;
|
||||
private static final int TIMEOUT = 10;
|
||||
private @Nullable ServiceRegistration<?> discoveryService;
|
||||
|
||||
public ShellyBasicDiscoveryService(BundleContext bundleContext, ShellyThingTable thingTable) {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT);
|
||||
this.bundleContext = bundleContext;
|
||||
this.thingTable = thingTable;
|
||||
}
|
||||
|
||||
public void registerDeviceDiscoveryService() {
|
||||
if (discoveryService == null) {
|
||||
discoveryService = bundleContext.registerService(DiscoveryService.class.getName(), this, new Hashtable<>());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Starting BLU Discovery");
|
||||
thingTable.startScan();
|
||||
}
|
||||
|
||||
public void discoveredResult(ThingTypeUID tuid, String model, String serviceName, String address,
|
||||
Map<String, Object> properties) {
|
||||
ThingUID uid = ShellyThingCreator.getThingUID(serviceName, model, "", true);
|
||||
logger.debug("Adding discovered thing with id {}", uid.toString());
|
||||
properties.put(PROPERTY_MAC_ADDRESS, address);
|
||||
String thingLabel = "Shelly BLU " + model + " (" + serviceName + ")";
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
|
||||
.withRepresentationProperty(PROPERTY_DEV_NAME).withLabel(thingLabel).build();
|
||||
thingDiscovered(result);
|
||||
}
|
||||
|
||||
public void discoveredResult(DiscoveryResult result) {
|
||||
thingDiscovered(result);
|
||||
}
|
||||
|
||||
public void unregisterDeviceDiscoveryService() {
|
||||
if (discoveryService != null) {
|
||||
discoveryService.unregister();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
super.deactivate();
|
||||
unregisterDeviceDiscoveryService();
|
||||
}
|
||||
|
||||
public static @Nullable DiscoveryResult createResult(boolean gen2, String hostname, String ipAddress,
|
||||
ShellyBindingConfiguration bindingConfig, HttpClient httpClient, ShellyTranslationProvider messages) {
|
||||
Logger logger = LoggerFactory.getLogger(ShellyBasicDiscoveryService.class);
|
||||
ThingUID thingUID = null;
|
||||
ShellyDeviceProfile profile;
|
||||
ShellySettingsDevice devInfo;
|
||||
ShellyApiInterface api = null;
|
||||
boolean auth = false;
|
||||
String mac = "";
|
||||
String model = "";
|
||||
String mode = "";
|
||||
String name = hostname;
|
||||
String deviceName = "";
|
||||
String thingType = "";
|
||||
Map<String, Object> properties = new TreeMap<>();
|
||||
|
||||
try {
|
||||
ShellyThingConfiguration config = fillConfig(bindingConfig, ipAddress);
|
||||
api = gen2 ? new Shelly2ApiRpc(name, config, httpClient) : new Shelly1HttpApi(name, config, httpClient);
|
||||
api.initialize();
|
||||
devInfo = api.getDeviceInfo();
|
||||
mac = getString(devInfo.mac);
|
||||
model = devInfo.type;
|
||||
auth = getBool(devInfo.auth);
|
||||
if (name.isEmpty() || name.startsWith("shellyplusrange")) {
|
||||
name = devInfo.hostname;
|
||||
}
|
||||
if (devInfo.name != null) {
|
||||
deviceName = devInfo.name;
|
||||
}
|
||||
|
||||
thingType = substringBeforeLast(name, "-");
|
||||
profile = api.getDeviceProfile(thingType, devInfo);
|
||||
api.close();
|
||||
deviceName = profile.name;
|
||||
mode = devInfo.mode;
|
||||
properties = ShellyBaseHandler.fillDeviceProperties(profile);
|
||||
|
||||
// get thing type from device name
|
||||
thingUID = ShellyThingCreator.getThingUID(name, model, mode, false);
|
||||
} catch (ShellyApiException e) {
|
||||
ShellyApiResult result = e.getApiResult();
|
||||
if (result.isHttpAccessUnauthorized()) {
|
||||
// create shellyunknown thing - will be changed during thing initialization with valid credentials
|
||||
thingUID = ShellyThingCreator.getThingUID(name, model, mode, true);
|
||||
}
|
||||
} catch (IllegalArgumentException | IOException e) { // maybe some format description was buggy
|
||||
logger.debug("Discovery: Unable to discover thing", e);
|
||||
} finally {
|
||||
if (api != null) {
|
||||
api.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (thingUID != null) {
|
||||
addProperty(properties, PROPERTY_MAC_ADDRESS, mac);
|
||||
addProperty(properties, CONFIG_DEVICEIP, ipAddress);
|
||||
addProperty(properties, PROPERTY_MODEL_ID, model);
|
||||
addProperty(properties, PROPERTY_SERVICE_NAME, name);
|
||||
addProperty(properties, PROPERTY_DEV_NAME, deviceName);
|
||||
addProperty(properties, PROPERTY_DEV_TYPE, thingType);
|
||||
addProperty(properties, PROPERTY_DEV_GEN, gen2 ? "2" : "1");
|
||||
addProperty(properties, PROPERTY_DEV_MODE, mode);
|
||||
addProperty(properties, PROPERTY_DEV_AUTH, auth ? "yes" : "no");
|
||||
|
||||
String thingLabel = deviceName.isEmpty() ? name + " - " + ipAddress
|
||||
: deviceName + " (" + name + "@" + ipAddress + ")";
|
||||
return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(thingLabel)
|
||||
.withRepresentationProperty(PROPERTY_SERVICE_NAME).build();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ShellyThingConfiguration fillConfig(ShellyBindingConfiguration bindingConfig, String address)
|
||||
throws IOException {
|
||||
ShellyThingConfiguration config = new ShellyThingConfiguration();
|
||||
config.deviceIp = address;
|
||||
config.userId = bindingConfig.defaultUserId;
|
||||
config.password = bindingConfig.defaultPassword;
|
||||
return config;
|
||||
}
|
||||
|
||||
private static void addProperty(Map<String, Object> properties, String key, @Nullable String value) {
|
||||
properties.put(key, value != null ? value : "");
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.shelly.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
|
||||
import static org.openhab.core.thing.Thing.PROPERTY_MAC_ADDRESS;
|
||||
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Device discovery creates a thing in the inbox for each vehicle
|
||||
* found in the data received from {@link ShellyBluDiscoveryService}.
|
||||
*
|
||||
* @author Markus Michels - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShellyBluDiscoveryService extends AbstractDiscoveryService {
|
||||
private final Logger logger = LoggerFactory.getLogger(ShellyBluDiscoveryService.class);
|
||||
|
||||
private final BundleContext bundleContext;
|
||||
private final ShellyThingTable thingTable;
|
||||
private static final int TIMEOUT = 10;
|
||||
private @Nullable ServiceRegistration<?> discoveryService;
|
||||
|
||||
public ShellyBluDiscoveryService(BundleContext bundleContext, ShellyThingTable thingTable) {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT);
|
||||
this.bundleContext = bundleContext;
|
||||
this.thingTable = thingTable;
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public void registerDeviceDiscoveryService() {
|
||||
if (discoveryService == null) {
|
||||
discoveryService = bundleContext.registerService(DiscoveryService.class.getName(), this, new Hashtable<>());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Starting BLU Discovery");
|
||||
thingTable.startScan();
|
||||
}
|
||||
|
||||
public void discoveredResult(ThingTypeUID tuid, String model, String serviceName, String address,
|
||||
Map<String, Object> properties) {
|
||||
ThingUID uid = ShellyThingCreator.getThingUID(serviceName, model, "", true);
|
||||
logger.debug("Adding discovered thing with id {}", uid.toString());
|
||||
properties.put(PROPERTY_MAC_ADDRESS, address);
|
||||
String thingLabel = "Shelly BLU " + model + " (" + serviceName + ")";
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
|
||||
.withRepresentationProperty(PROPERTY_DEV_NAME).withLabel(thingLabel).build();
|
||||
thingDiscovered(result);
|
||||
}
|
||||
|
||||
public void unregisterDeviceDiscoveryService() {
|
||||
if (discoveryService != null) {
|
||||
discoveryService.unregister();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
super.deactivate();
|
||||
unregisterDeviceDiscoveryService();
|
||||
}
|
||||
}
|
@ -14,32 +14,20 @@ package org.openhab.binding.shelly.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
|
||||
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
|
||||
import static org.openhab.core.thing.Thing.PROPERTY_MODEL_ID;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import javax.jmdns.ServiceInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.shelly.internal.api.ShellyApiException;
|
||||
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
|
||||
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
|
||||
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
|
||||
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
|
||||
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
|
||||
import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
|
||||
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
|
||||
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
|
||||
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
|
||||
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
@ -109,15 +97,8 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
||||
return null;
|
||||
}
|
||||
|
||||
String address = "";
|
||||
try {
|
||||
String mode = "";
|
||||
String model = "unknown";
|
||||
String deviceName = "";
|
||||
ThingUID thingUID = null;
|
||||
ShellyDeviceProfile profile;
|
||||
Map<String, Object> properties = new TreeMap<>();
|
||||
|
||||
String address = "";
|
||||
name = service.getName().toLowerCase();
|
||||
Inet4Address[] hostAddresses = service.getInet4Addresses();
|
||||
if ((hostAddresses != null) && (hostAddresses.length > 0)) {
|
||||
@ -145,65 +126,7 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
||||
|
||||
String gen = getString(service.getPropertyString("gen"));
|
||||
boolean gen2 = "2".equals(gen) || "3".equals(gen);
|
||||
ShellyApiInterface api = null;
|
||||
boolean auth = false;
|
||||
ShellySettingsDevice devInfo;
|
||||
try {
|
||||
api = gen2 ? new Shelly2ApiRpc(name, config, httpClient) : new Shelly1HttpApi(name, config, httpClient);
|
||||
api.initialize();
|
||||
devInfo = api.getDeviceInfo();
|
||||
model = devInfo.type;
|
||||
gen2 = !(devInfo.gen == 1); // gen 2+3
|
||||
auth = getBool(devInfo.auth);
|
||||
if (devInfo.name != null) {
|
||||
deviceName = devInfo.name;
|
||||
}
|
||||
|
||||
profile = api.getDeviceProfile(thingType, devInfo);
|
||||
api.close();
|
||||
logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
|
||||
deviceName = profile.name;
|
||||
mode = devInfo.mode;
|
||||
properties = ShellyBaseHandler.fillDeviceProperties(profile);
|
||||
logger.trace("{}: thingType={}, deviceType={}, mode={}, symbolic name={}", name, thingType,
|
||||
devInfo.type, mode.isEmpty() ? "<standard>" : mode, deviceName);
|
||||
|
||||
// get thing type from device name
|
||||
thingUID = ShellyThingCreator.getThingUID(name, model, mode, false);
|
||||
} catch (ShellyApiException e) {
|
||||
ShellyApiResult result = e.getApiResult();
|
||||
if (result.isHttpAccessUnauthorized()) {
|
||||
logger.info("{}: {}", name, messages.get("discovery.protected", address));
|
||||
|
||||
// create shellyunknown thing - will be changed during thing initialization with valid credentials
|
||||
thingUID = ShellyThingCreator.getThingUID(name, model, mode, true);
|
||||
} else {
|
||||
logger.debug("{}: {}", name, messages.get("discovery.failed", address, e.toString()));
|
||||
}
|
||||
} catch (IllegalArgumentException e) { // maybe some format description was buggy
|
||||
logger.debug("{}: Discovery failed!", name, e);
|
||||
} finally {
|
||||
if (api != null) {
|
||||
api.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (thingUID != null) {
|
||||
addProperty(properties, CONFIG_DEVICEIP, address);
|
||||
addProperty(properties, PROPERTY_MODEL_ID, model);
|
||||
addProperty(properties, PROPERTY_SERVICE_NAME, name);
|
||||
addProperty(properties, PROPERTY_DEV_NAME, deviceName);
|
||||
addProperty(properties, PROPERTY_DEV_TYPE, thingType);
|
||||
addProperty(properties, PROPERTY_DEV_GEN, gen2 ? "2" : "1");
|
||||
addProperty(properties, PROPERTY_DEV_MODE, mode);
|
||||
addProperty(properties, PROPERTY_DEV_AUTH, auth ? "yes" : "no");
|
||||
|
||||
logger.debug("{}: Adding Shelly {}, UID={}", name, deviceName, thingUID.getAsString());
|
||||
String thingLabel = deviceName.isEmpty() ? name + " - " + address
|
||||
: deviceName + " (" + name + "@" + address + ")";
|
||||
return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(thingLabel)
|
||||
.withRepresentationProperty(PROPERTY_SERVICE_NAME).build();
|
||||
}
|
||||
return ShellyBasicDiscoveryService.createResult(gen2, name, address, bindingConfig, httpClient, messages);
|
||||
} catch (IOException | NullPointerException e) {
|
||||
// maybe some format description was buggy
|
||||
logger.debug("{}: Exception on processing serviceInfo '{}'", name, service.getNiceTextString(), e);
|
||||
@ -211,10 +134,6 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addProperty(Map<String, Object> properties, String key, @Nullable String value) {
|
||||
properties.put(key, value != null ? value : "");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ThingUID getThingUID(@Nullable ServiceInfo service) throws IllegalArgumentException {
|
||||
|
@ -174,6 +174,7 @@ public class ShellyThingCreator {
|
||||
public static final String THING_TYPE_SHELLYPLUSI4DC_STR = "shellyplusi4dc";
|
||||
public static final String THING_TYPE_SHELLYPLUSHT_STR = "shellyplusht";
|
||||
public static final String THING_TYPE_SHELLYPLUSSMOKE_STR = "shellyplussmoke";
|
||||
public static final String THING_TYPE_SHELLYPLUSUNI_STR = "shellyplusuni";
|
||||
public static final String THING_TYPE_SHELLYPLUSPLUGS_STR = "shellyplusplug";
|
||||
public static final String THING_TYPE_SHELLYPLUSPLUGUS_STR = "shellyplusplugus";
|
||||
public static final String THING_TYPE_SHELLYPLUSDIMMERUS_STR = "shellypluswdus";
|
||||
@ -375,6 +376,7 @@ public class ShellyThingCreator {
|
||||
THING_TYPE_MAPPING.put(SHELLYDT_PLUSI4, THING_TYPE_SHELLYPLUSI4_STR);
|
||||
THING_TYPE_MAPPING.put(SHELLYDT_PLUSHT, THING_TYPE_SHELLYPLUSHT_STR);
|
||||
THING_TYPE_MAPPING.put(SHELLYDT_PLUSSMOKE, THING_TYPE_SHELLYPLUSSMOKE_STR);
|
||||
THING_TYPE_MAPPING.put(SHELLYDT_PLUSUNI, THING_TYPE_SHELLYUNI_STR);
|
||||
THING_TYPE_MAPPING.put(SHELLYDT_PLUSDIMMERUS, THING_TYPE_SHELLYPLUSDIMMERUS_STR);
|
||||
THING_TYPE_MAPPING.put(SHELLYDT_PLUSDIMMER10V, THING_TYPE_SHELLYPLUSDIMMER10V_STR);
|
||||
|
||||
@ -460,6 +462,7 @@ public class ShellyThingCreator {
|
||||
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSI4_STR, THING_TYPE_SHELLYPLUSI4_STR);
|
||||
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSHT_STR, THING_TYPE_SHELLYPLUSHT_STR);
|
||||
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSSMOKE_STR, THING_TYPE_SHELLYPLUSSMOKE_STR);
|
||||
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSUNI_STR, THING_TYPE_SHELLYUNI_STR);
|
||||
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSDIMMERUS_STR, THING_TYPE_SHELLYPLUSDIMMERUS_STR);
|
||||
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSDIMMER10V_STR, THING_TYPE_SHELLYPLUSDIMMER10V_STR);
|
||||
|
||||
|
@ -45,15 +45,18 @@ import org.openhab.binding.shelly.internal.api1.Shelly1CoapHandler;
|
||||
import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO;
|
||||
import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
|
||||
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
|
||||
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2APClientList.Shelly2APClient;
|
||||
import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
|
||||
import org.openhab.binding.shelly.internal.api2.ShellyBluApi;
|
||||
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
|
||||
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
|
||||
import org.openhab.binding.shelly.internal.discovery.ShellyBasicDiscoveryService;
|
||||
import org.openhab.binding.shelly.internal.discovery.ShellyThingCreator;
|
||||
import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
|
||||
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
|
||||
import org.openhab.binding.shelly.internal.util.ShellyChannelCache;
|
||||
import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
@ -93,6 +96,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
|
||||
|
||||
protected final ShellyApiInterface api;
|
||||
private final HttpClient httpClient;
|
||||
private final ShellyThingTable thingTable;
|
||||
|
||||
private ShellyBindingConfiguration bindingConfig;
|
||||
protected ShellyThingConfiguration config = new ShellyThingConfiguration();
|
||||
@ -139,6 +143,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
|
||||
final Shelly1CoapServer coapServer, final HttpClient httpClient) {
|
||||
super(thing);
|
||||
|
||||
this.thingTable = thingTable;
|
||||
this.thingName = getString(thing.getLabel());
|
||||
this.messages = translationProvider;
|
||||
this.cache = new ShellyChannelCache(this);
|
||||
@ -177,15 +182,10 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
|
||||
initJob = scheduler.schedule(() -> {
|
||||
boolean start = true;
|
||||
try {
|
||||
initializeThingConfig();
|
||||
logger.debug("{}: Device config: Device address={}, HTTP user/password={}/{}, update interval={}",
|
||||
thingName, config.deviceAddress, config.userId.isEmpty() ? "<non>" : config.userId,
|
||||
config.password.isEmpty() ? "<none>" : "***", config.updateInterval);
|
||||
logger.debug(
|
||||
"{}: Configured Events: Button: {}, Switch (on/off): {}, Push: {}, Roller: {}, Sensor: {}, CoIoT: {}, Enable AutoCoIoT: {}",
|
||||
thingName, config.eventsButton, config.eventsSwitch, config.eventsPush, config.eventsRoller,
|
||||
config.eventsSensorReport, config.eventsCoIoT, bindingConfig.autoCoIoT);
|
||||
start = initializeThing();
|
||||
if (initializeThingConfig()) {
|
||||
logger.debug("{}: Config: {}", thingName, config);
|
||||
start = initializeThing();
|
||||
}
|
||||
} catch (ShellyApiException e) {
|
||||
start = handleApiException(e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
@ -253,6 +253,8 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
|
||||
if (api.isInitialized()) {
|
||||
api.startScan();
|
||||
}
|
||||
|
||||
checkRangeExtender(profile);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -356,6 +358,9 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
|
||||
updateProperties(tmpPrf, tmpPrf.status);
|
||||
checkVersion(tmpPrf, tmpPrf.status);
|
||||
|
||||
// Check for Range Extender mode, add secondary device to Inbox
|
||||
checkRangeExtender(tmpPrf);
|
||||
|
||||
startCoap(config, tmpPrf);
|
||||
if (!gen2 && !blu) {
|
||||
api.setActionURLs(); // register event urls
|
||||
@ -582,6 +587,21 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRangeExtender(ShellyDeviceProfile prf) {
|
||||
if (getBool(prf.settings.rangeExtender) && config.enableRangeExtender && prf.status.rangeExtender != null
|
||||
&& prf.status.rangeExtender.apClients != null) {
|
||||
for (Shelly2APClient client : profile.status.rangeExtender.apClients) {
|
||||
String secondaryIp = config.deviceIp + ":" + client.mport.toString();
|
||||
String name = "shellyplusrange-" + client.mac.replaceAll(":", "");
|
||||
DiscoveryResult result = ShellyBasicDiscoveryService.createResult(true, name, secondaryIp,
|
||||
bindingConfig, httpClient, messages);
|
||||
if (result != null) {
|
||||
thingTable.discoveredResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showThingConfig(ShellyDeviceProfile profile) {
|
||||
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {}", thingName,
|
||||
profile.device.hostname, profile.device.type, profile.hwRev, profile.hwBatchId, profile.fwVersion,
|
||||
@ -955,7 +975,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
|
||||
/**
|
||||
* Initialize the binding's thing configuration, calc update counts
|
||||
*/
|
||||
protected void initializeThingConfig() {
|
||||
protected boolean initializeThingConfig() {
|
||||
thingType = getThing().getThingTypeUID().getId();
|
||||
final Map<String, String> properties = getThing().getProperties();
|
||||
thingName = getString(properties.get(PROPERTY_SERVICE_NAME));
|
||||
@ -970,18 +990,20 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
|
||||
if (config.deviceAddress.isEmpty()) {
|
||||
logger.debug("{}: IP/MAC address for the device must not be empty", thingName); // may not set in .things
|
||||
// file
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
config.deviceAddress = config.deviceAddress.toLowerCase().replace(":", ""); // remove : from MAC address and
|
||||
// convert to lower case
|
||||
if (!config.deviceIp.isEmpty()) {
|
||||
try {
|
||||
InetAddress addr = InetAddress.getByName(config.deviceIp);
|
||||
String ip = config.deviceIp.contains(":") ? substringBefore(config.deviceIp, ":") : config.deviceIp;
|
||||
String port = config.deviceIp.contains(":") ? substringAfter(config.deviceIp, ":") : "";
|
||||
InetAddress addr = InetAddress.getByName(ip);
|
||||
String saddr = addr.getHostAddress();
|
||||
if (!config.deviceIp.equals(saddr)) {
|
||||
if (!ip.equals(saddr)) {
|
||||
logger.debug("{}: hostname {} resolved to IP address {}", thingName, config.deviceIp, saddr);
|
||||
config.deviceIp = saddr;
|
||||
config.deviceIp = saddr + (port.isEmpty() ? ip : ip + ":" + port);
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
logger.debug("{}: Unable to resolve hostname {}", thingName, config.deviceIp);
|
||||
@ -994,7 +1016,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
|
||||
if (config.localIp.startsWith("169.254")) {
|
||||
setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "config-status.error.network-config",
|
||||
config.localIp);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!profile.isGen2 && config.userId.isEmpty() && !bindingConfig.defaultUserId.isEmpty()) {
|
||||
@ -1028,6 +1050,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
|
||||
|
||||
skipCount = config.updateInterval / UPDATE_STATUS_INTERVAL_SECONDS;
|
||||
logger.trace("{}: updateInterval = {}s -> skipCount = {}", thingName, config.updateInterval, skipCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void checkVersion(ShellyDeviceProfile prf, ShellySettingsStatus status) {
|
||||
@ -1123,6 +1146,9 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
|
||||
properties.replace(PROPERTY_DEV_MODE, mode);
|
||||
updateProperties(properties);
|
||||
changeThingType(thingTypeUID, getConfig());
|
||||
} else {
|
||||
logger.debug("{}: to {}", thingName, thingType);
|
||||
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Unable to change thing type to " + thingType);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.shelly.internal.discovery.ShellyBluDiscoveryService;
|
||||
import org.openhab.binding.shelly.internal.discovery.ShellyBasicDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
@ -34,7 +35,7 @@ import org.osgi.service.component.annotations.Deactivate;
|
||||
@Component(service = ShellyThingTable.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
|
||||
public class ShellyThingTable {
|
||||
private Map<String, ShellyThingInterface> thingTable = new ConcurrentHashMap<>();
|
||||
private @Nullable ShellyBluDiscoveryService bluDiscoveryService;
|
||||
private @Nullable ShellyBasicDiscoveryService discoveryService;
|
||||
|
||||
public void addThing(String key, ShellyThingInterface thing) {
|
||||
if (thingTable.containsKey(key)) {
|
||||
@ -80,9 +81,9 @@ public class ShellyThingTable {
|
||||
}
|
||||
|
||||
public void startDiscoveryService(BundleContext bundleContext) {
|
||||
if (bluDiscoveryService == null) {
|
||||
bluDiscoveryService = new ShellyBluDiscoveryService(bundleContext, this);
|
||||
bluDiscoveryService.registerDeviceDiscoveryService();
|
||||
if (discoveryService == null) {
|
||||
discoveryService = new ShellyBasicDiscoveryService(bundleContext, this);
|
||||
discoveryService.registerDeviceDiscoveryService();
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,16 +94,22 @@ public class ShellyThingTable {
|
||||
}
|
||||
|
||||
public void stopDiscoveryService() {
|
||||
if (bluDiscoveryService != null) {
|
||||
bluDiscoveryService.unregisterDeviceDiscoveryService();
|
||||
bluDiscoveryService = null;
|
||||
if (discoveryService != null) {
|
||||
discoveryService.unregisterDeviceDiscoveryService();
|
||||
discoveryService = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void discoveredResult(ThingTypeUID uid, String model, String serviceName, String address,
|
||||
Map<String, Object> properties) {
|
||||
if (bluDiscoveryService != null) {
|
||||
bluDiscoveryService.discoveredResult(uid, model, serviceName, address, properties);
|
||||
if (discoveryService != null) {
|
||||
discoveryService.discoveredResult(uid, model, serviceName, address, properties);
|
||||
}
|
||||
}
|
||||
|
||||
public void discoveredResult(DiscoveryResult result) {
|
||||
if (discoveryService != null) {
|
||||
discoveryService.discoveredResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,6 @@
|
||||
<parameter name="deviceIp" type="text" required="true">
|
||||
<label>@text/thing-type.config.shelly.deviceIp.label</label>
|
||||
<description>@text/thing-type.config.shelly.deviceIp.description</description>
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="false">
|
||||
<label>@text/thing-type.config.shelly.password.label</label>
|
||||
@ -27,6 +26,11 @@
|
||||
<description>@text/thing-type.config.shelly.enableBluGateway.description</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="enableRangeExtender" type="boolean" required="false">
|
||||
<label>@text/thing-type.config.shelly.enableRangeExtender.label</label>
|
||||
<description>@text/thing-type.config.shelly.enableRangeExtender.description</description>
|
||||
<default>true</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="thing-type:shelly:roller-gen2">
|
||||
|
@ -141,6 +141,8 @@ thing-type.config.shelly.updateInterval.label = Status Interval
|
||||
thing-type.config.shelly.updateInterval.description = Interval for the device status update
|
||||
thing-type.config.shelly.enableBluGateway.label = Enable BLU Gateway Support
|
||||
thing-type.config.shelly.enableBluGateway.description = Enables BLU Gateway support including auto-upload of the required script
|
||||
thing-type.config.shelly.enableRangeExtender.label = Enable Range Extender Support
|
||||
thing-type.config.shelly.enableRangeExtender.description = Auto discovers devices, which are connected using the Shelly Range Extender support
|
||||
thing-type.config.shelly.eventsButton.label = Button Events
|
||||
thing-type.config.shelly.eventsButton.description = Activates the Button Action URLS
|
||||
thing-type.config.shelly.eventsPush.label = Push Events
|
||||
|
Loading…
Reference in New Issue
Block a user