[shelly] Add Shelly Motion, minor improvements ()

* Support for Shelly Motion, some minotr improvements, README updated

Signed-off-by: Markus Michels <markus7017@gmail.com>

* minor changes

Signed-off-by: Markus Michels <markus7017@gmail.com>

* Bug fixes from hardening

Signed-off-by: Markus Michels <markus7017@gmail.com>

* review changes applied

Signed-off-by: Markus Michels <markus7017@gmail.com>

* review change

Signed-off-by: Markus Michels <markus7017@gmail.com>

* review changes, fix creations of sensors#motion and device#externalPower
for H%T; moved images/uiroller*.png to doc/images

Signed-off-by: Markus Michels <markus7017@gmail.com>

* missing in last fix

Signed-off-by: Markus Michels <markus7017@gmail.com>
This commit is contained in:
Markus Michels 2021-02-23 09:48:13 +01:00 committed by GitHub
parent fe7b91f4b7
commit fd1f7ebe75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 391 additions and 181 deletions

@ -30,6 +30,7 @@ Refer to [Advanced Users](doc/AdvancedUsers.md) for more information on openHAB
| shellydimmer | Shelly Dimmer | SHDM-1 |
| shellydimmer2 | Shelly Dimmer2 | SHDM-2 |
| shellyix3 | Shelly ix3 | SHIX3-1 |
| shellyuni | Shelly UNI | SHUNI-1 |
| shellyplug | Shelly Plug | SHPLG2-1 |
| shellyplugs | Shelly Plug-S | SHPLG-S |
| shellyem | Shelly EM with integrated Power Meters | SHEM |
@ -578,6 +579,23 @@ Using the Thing configuration option `brightnessAutoOn` you could decide if the
| |lastEvent |String |yes |S/SS/SSS for 1/2/3x Shortpush or L for Longpush |
| |eventCount |Number |yes |Counter gets incremented every time the device issues a button event. |
### Shelly UNI - Low voltage sensor/actor: shellyuni)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|----------------------------------------------------------------------------|
|relay1 | | | |See group relay1 for Shelly 2, no autoOn/autoOff/timerActive channels |
|relay2 | | | |See group relay1 for Shelly 2, no autoOn/autoOff/timerActive channels |
|sensors |temperature1 |Number |yes |Temperature value of external sensor #1 (if connected to temp/hum addon) |
| |temperature2 |Number |yes |Temperature value of external sensor #2 (if connected to temp/hum addon) |
| |temperature3 |Number |yes |Temperature value of external sensor #3 (if connected to temp/hum addon) |
| |humidity |Number |yes |Humidity in percent (if connected to temp/hum addon) |
| |voltage |Number |yes |ADCS voltage |
|status |input1 |Switch |yes |State of Input 1 |
| |input2 |Switch |yes |State of Input 2 |
| |button |Trigger |yes |Event trigger, see section Button Events |
| |lastEvent |String |yes |S/SS/SSS for 1/2/3x Shortpush or L for Longpush |
| |eventCount |Number |yes |Counter gets incremented every time the device issues a button event. |
### Shelly Bulb (thing-type: shellybulb)
|Group |Channel |Type |read-only|Description |

Binary file not shown.

Before

(image error) Size: 144 KiB

After

(image error) Size: 120 KiB

Binary file not shown.

Before

(image error) Size: 143 KiB

After

(image error) Size: 102 KiB

Binary file not shown.

Before

(image error) Size: 144 KiB

After

(image error) Size: 120 KiB

Binary file not shown.

Before

(image error) Size: 218 KiB

After

(image error) Size: 145 KiB

Binary file not shown.

Before

(image error) Size: 224 KiB

After

(image error) Size: 148 KiB

Binary file not shown.

Before

(image error) Size: 222 KiB

After

(image error) Size: 150 KiB

Binary file not shown.

Before

(image error) Size: 228 KiB

After

(image error) Size: 151 KiB

@ -164,7 +164,7 @@ public class ShellyBindingConstants {
THING_TYPE_SHELLYVINTAGE, THING_TYPE_SHELLYDUORGBW, THING_TYPE_SHELLYRGBW2_COLOR,
THING_TYPE_SHELLYRGBW2_WHITE, THING_TYPE_SHELLYHT, THING_TYPE_SHELLYSENSE, THING_TYPE_SHELLYEYE,
THING_TYPE_SHELLYSMOKE, THING_TYPE_SHELLYGAS, THING_TYPE_SHELLYFLOOD, THING_TYPE_SHELLYDOORWIN,
THING_TYPE_SHELLYDOORWIN2, THING_TYPE_SHELLYBUTTON1, /* THING_TYPE_SHELLMOTION, */
THING_TYPE_SHELLYDOORWIN2, THING_TYPE_SHELLYBUTTON1, THING_TYPE_SHELLMOTION,
THING_TYPE_SHELLYPROTECTED, THING_TYPE_SHELLYUNKNOWN).collect(Collectors.toSet()));
// Thing Configuration Properties
@ -333,6 +333,7 @@ public class ShellyBindingConstants {
public static final String ALARM_TYPE_OVERPOWER = "OVERPOWER";
public static final String ALARM_TYPE_OVERLOAD = "OVERLOAD";
public static final String ALARM_TYPE_LOADERR = "LOAD_ERROR";
public static final String ALARM_TYPE_SENSOR_ERROR = "SENSOR_ERROR";
public static final String ALARM_TYPE_LOW_BATTERY = "LOW_BATTERY";
// Event types

@ -54,12 +54,14 @@ import io.reactivex.annotations.NonNull;
@NonNullByDefault
@Component(service = { ThingHandlerFactory.class, ShellyHandlerFactory.class }, configurationPid = "binding.shelly")
public class ShellyHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = ShellyBindingConstants.SUPPORTED_THING_TYPES_UIDS;
private final Logger logger = LoggerFactory.getLogger(ShellyHandlerFactory.class);
private final HttpClient httpClient;
private final ShellyTranslationProvider messages;
private final ShellyCoapServer coapServer;
private final Set<ShellyBaseHandler> deviceListeners = ConcurrentHashMap.newKeySet();
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = ShellyBindingConstants.SUPPORTED_THING_TYPES_UIDS;
private final Map<String, ShellyBaseHandler> deviceListeners = new ConcurrentHashMap<>();
private ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
private String localIP = "";
private int httpPort = -1;
@ -129,7 +131,9 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
}
if (handler != null) {
deviceListeners.add(handler);
String uid = thing.getUID().getAsString();
deviceListeners.put(uid, handler);
logger.debug("Thing handler for uid {} added, total things = {}", uid, deviceListeners.size());
return handler;
}
@ -137,13 +141,18 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
return null;
}
public Map<String, ShellyBaseHandler> getThingHandlers() {
return deviceListeners;
}
/**
* Remove handler of things.
*/
@Override
protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) {
if (thingHandler instanceof ShellyBaseHandler) {
deviceListeners.remove(thingHandler);
String uid = thingHandler.getThing().getUID().getAsString();
deviceListeners.remove(uid);
}
}
@ -158,8 +167,9 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
public void onEvent(String ipAddress, String deviceName, String componentIndex, String eventType,
Map<String, String> parameters) {
logger.trace("{}: Dispatch event to thing handler", deviceName);
for (ShellyBaseHandler listener : deviceListeners) {
if (listener.onEvent(ipAddress, deviceName, componentIndex, eventType, parameters)) {
for (Map.Entry<String, ShellyBaseHandler> listener : deviceListeners.entrySet()) {
ShellyBaseHandler thingHandler = listener.getValue();
if (thingHandler.onEvent(ipAddress, deviceName, componentIndex, eventType, parameters)) {
// event processed
return;
}

@ -35,6 +35,7 @@ public class ShellyApiJsonDTO {
public static final String SHELLY_URL_SETTINGS_CLOUD = "/settings/cloud";
public static final String SHELLY_URL_LIST_IR = "/ir/list";
public static final String SHELLY_URL_SEND_IR = "/ir/emit";
public static final String SHELLY_URL_RESTART = "/reboot";
public static final String SHELLY_URL_SETTINGS_RELAY = "/settings/relay";
public static final String SHELLY_URL_STATUS_RELEAY = "/status/relay";
@ -231,6 +232,11 @@ public class ShellyApiJsonDTO {
public String hostname;
public String fw;
public Boolean auth;
@SerializedName("coiot") // Shelly Motion Multicast Endpoint
public String coiot;
public Integer longid;
@SerializedName("num_outputs")
public Integer numOutputs;
@SerializedName("num_meters")
@ -513,6 +519,8 @@ public class ShellyApiJsonDTO {
public String newVersion;
@SerializedName("old_version")
public String oldVersion;
@SerializedName("beta_version")
public String betaVersion;
}
public static class ShellySettingsGlobal {
@ -540,6 +548,8 @@ public class ShellyApiJsonDTO {
ShellyStatusCloud cloud;
@SerializedName("sleep_mode")
public ShellySensorSleepMode sleepMode; // FW 1.6
@SerializedName("external_power")
public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense
public String timezone;
public Double lat;

@ -81,6 +81,7 @@ public class ShellyDeviceProfile {
public boolean isSensor = false; // true for HT & Smoke
public boolean hasBattery = false; // true if battery device
public boolean isSense = false; // true if thing is a Shelly Sense
public boolean isMotion = false; // true if thing is a Shelly Sense
public boolean isHT = false; // true for H&T
public boolean isDW = false; // true for Door Window sensor
public boolean isButton = false; // true for a Shelly Button 1
@ -91,6 +92,8 @@ public class ShellyDeviceProfile {
public int updatePeriod = 2 * UPDATE_SETTINGS_INTERVAL_SECONDS + 10;
public String coiotEndpoint = "";
public Map<String, String> irCodes = new HashMap<>(); // Sense: list of stored IR codes
public ShellyDeviceProfile() {
@ -188,13 +191,13 @@ public class ShellyDeviceProfile {
boolean isSmoke = thingType.equals(THING_TYPE_SHELLYSMOKE_STR);
boolean isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR);
boolean isUNI = thingType.equals(THING_TYPE_SHELLYUNI_STR);
boolean isMotion = thingType.equals(THING_TYPE_SHELLYMOTION_STR);
isHT = thingType.equals(THING_TYPE_SHELLYHT_STR);
isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR);
isMotion = thingType.startsWith(THING_TYPE_SHELLYMOTION_STR);
isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR);
isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_STR);
isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR);
isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isSense;
isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isMotion || isSense;
hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion; // we assume that Sense is connected to
// the charger
}

@ -36,7 +36,9 @@ import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyLis
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLight;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsUpdate;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
@ -228,6 +230,27 @@ public class ShellyHttpApi {
request(SHELLY_URL_SETTINGS + "?" + parm + "=" + value);
}
public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
return callApi(SHELLY_URL_SETTINGS + "/login", ShellySettingsLogin.class);
}
public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
return callApi(SHELLY_URL_SETTINGS + "/login?enabled=yes&username=" + user + "&password=" + password,
ShellySettingsLogin.class);
}
public String deviceReboot() throws ShellyApiException {
return callApi(SHELLY_URL_RESTART, String.class);
}
public String factoryReset() throws ShellyApiException {
return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class);
}
public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
return callApi("/ota?" + uri, ShellySettingsUpdate.class);
}
/**
* Change between White and Color Mode
*

@ -83,7 +83,7 @@ public class ShellyCoIoTProtocol {
switch (sen.type.toLowerCase()) {
case "b": // BatteryLevel +
updateChannel(updates, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
toQuantityType(s.value, DIGITS_PERCENT, Units.PERCENT));
toQuantityType(s.value, 0, Units.PERCENT));
break;
case "h" /* Humidity */:
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,

@ -297,8 +297,10 @@ public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCo
break;
case "3119": // Motion timestamp
// {"I":3119,"T":"S","D":"timestamp","U":"s","R":["U32","-1"],"L":1},
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
getTimestamp(getString(profile.settings.timezone), (long) s.value));
if (s.value != 0) {
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
getTimestamp(getString(profile.settings.timezone), (long) s.value));
}
break;
case "3120": // motionActive
// {"I":3120,"T":"S","D":"motionActive","R":["0/1","-1"],"L":1},

@ -81,7 +81,10 @@ public class ShellyCoapHandler implements ShellyCoapListener {
private Request reqDescription = new Request(Code.GET, Type.CON);
private Request reqStatus = new Request(Code.GET, Type.CON);
private boolean discovering = false;
private int coiotPort = COIOT_PORT;
private long coiotMessages = 0;
private long coiotErrors = 0;
private int lastSerial = -1;
private String lastPayload = "";
private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
@ -118,8 +121,12 @@ public class ShellyCoapHandler implements ShellyCoapListener {
}
logger.debug("{}: Starting CoAP Listener", thingName);
coapServer.start(config.localIp, this);
statusClient = new CoapClient(completeUrl(config.deviceIp, COLOIT_URI_DEVSTATUS))
if (!profile.coiotEndpoint.isEmpty() && profile.coiotEndpoint.contains(":")) {
String ps = substringAfter(profile.coiotEndpoint, ":");
coiotPort = Integer.parseInt(ps);
}
coapServer.start(config.localIp, coiotPort, this);
statusClient = new CoapClient(completeUrl(config.deviceIp, coiotPort, COLOIT_URI_DEVSTATUS))
.setTimeout((long) SHELLY_API_TIMEOUT_MS).useNONs().setEndpoint(coapServer.getEndpoint());
@Nullable
Endpoint endpoint = null;
@ -152,10 +159,39 @@ public class ShellyCoapHandler implements ShellyCoapListener {
@Override
public void processResponse(@Nullable Response response) {
if (response == null) {
coiotErrors++;
return; // other device instance
}
ResponseCode code = response.getCode();
if (code != ResponseCode.CONTENT) {
// error handling
logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, code,
response.getPayloadString());
coiotErrors++;
return;
}
List<Option> options = response.getOptions().asSortedList();
String ip = response.getSourceContext().getPeerAddress().toString();
if (!ip.contains(config.deviceIp)) {
boolean match = ip.contains(config.deviceIp);
if (!match) {
// We can't identify device by IP, so we need to check the CoAP header's Global Device ID
for (Option opt : options) {
if (opt.getNumber() == COIOT_OPTION_GLOBAL_DEVID) {
String devid = opt.getStringValue();
if (devid.contains("#")) {
// Format: <device type>#<mac address>#<coap version>
String macid = substringBetween(devid, "#", "#");
if (profile.mac.toUpperCase().contains(macid.toUpperCase())) {
match = true;
break;
}
}
}
}
}
if (!match) {
// other instance
return;
}
@ -164,93 +200,87 @@ public class ShellyCoapHandler implements ShellyCoapListener {
String uri = "";
int serial = -1;
try {
coiotMessages++;
if (logger.isDebugEnabled()) {
logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName,
response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString());
}
if (response.isCanceled() || response.isDuplicate() || response.isRejected()) {
logger.debug("{} ({}): Packet was canceled, rejected or is a duplicate -> discard", thingName, devId);
coiotErrors++;
return;
}
if (response.getCode() == ResponseCode.CONTENT) {
payload = response.getPayloadString();
List<Option> options = response.getOptions().asSortedList();
int i = 0;
while (i < options.size()) {
Option opt = options.get(i);
switch (opt.getNumber()) {
case OptionNumberRegistry.URI_PATH:
uri = COLOIT_URI_BASE + opt.getStringValue();
break;
case COIOT_OPTION_GLOBAL_DEVID:
devId = opt.getStringValue();
String sVersion = substringAfterLast(devId, "#");
int iVersion = Integer.parseInt(sVersion);
if (coiotBound && (coiotVers != iVersion)) {
logger.debug(
"{}: CoIoT versopm has changed from {} to {}, maybe the firmware was upgraded",
thingName, coiotVers, iVersion);
thingHandler.reinitializeThing();
coiotBound = false;
payload = response.getPayloadString();
for (Option opt : options) {
switch (opt.getNumber()) {
case OptionNumberRegistry.URI_PATH:
uri = COLOIT_URI_BASE + opt.getStringValue();
break;
case OptionNumberRegistry.URI_HOST: // ignore
break;
case OptionNumberRegistry.CONTENT_FORMAT: // ignore
break;
case COIOT_OPTION_GLOBAL_DEVID:
devId = opt.getStringValue();
String sVersion = substringAfterLast(devId, "#");
int iVersion = Integer.parseInt(sVersion);
if (coiotBound && (coiotVers != iVersion)) {
logger.debug("{}: CoIoT versopm has changed from {} to {}, maybe the firmware was upgraded",
thingName, coiotVers, iVersion);
thingHandler.reinitializeThing();
coiotBound = false;
}
if (!coiotBound) {
thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
if (iVersion == COIOT_VERSION_1) {
coiot = new ShellyCoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
} else if (iVersion == COIOT_VERSION_2) {
coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap);
} else {
logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
return;
}
if (!coiotBound) {
thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
if (iVersion == COIOT_VERSION_1) {
coiot = new ShellyCoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
} else if (iVersion == COIOT_VERSION_2) {
coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap);
} else {
logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
return;
}
coiotVers = iVersion;
coiotBound = true;
}
break;
case COIOT_OPTION_STATUS_VALIDITY:
// validity = o.getIntegerValue();
break;
case COIOT_OPTION_STATUS_SERIAL:
serial = opt.getIntegerValue();
break;
default:
logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId,
opt.getNumber(), opt.getValue());
}
i++;
coiotVers = iVersion;
coiotBound = true;
}
break;
case COIOT_OPTION_STATUS_VALIDITY:
break;
case COIOT_OPTION_STATUS_SERIAL:
serial = opt.getIntegerValue();
break;
default:
logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId, opt.getNumber(),
opt.getValue());
}
}
// If we received a CoAP message successful the thing must be online
thingHandler.setThingOnline();
// If we received a CoAP message successful the thing must be online
thingHandler.setThingOnline();
// The device changes the serial on every update, receiving a message with the same serial is a
// duplicate, excep for battery devices! Those reset the serial every time when they wake-up
if ((serial == lastSerial) && payload.equals(lastPayload) && (!profile.hasBattery
|| coiot.getLastWakeup().equalsIgnoreCase("ext_power") || ((serial & 0xFF) != 0))) {
logger.debug("{}: Serial {} was already processed, ignore update", thingName, serial);
return;
// The device changes the serial on every update, receiving a message with the same serial is a
// duplicate, excep for battery devices! Those reset the serial every time when they wake-up
if ((serial == lastSerial) && payload.equals(lastPayload) && (!profile.hasBattery
|| coiot.getLastWakeup().equalsIgnoreCase("ext_power") || ((serial & 0xFF) != 0))) {
logger.debug("{}: Serial {} was already processed, ignore update", thingName, serial);
return;
}
// fixed malformed JSON :-(
payload = fixJSON(payload);
try {
if (uri.equalsIgnoreCase(COLOIT_URI_DEVDESC) || (uri.isEmpty() && payload.contains(COIOT_TAG_BLK))) {
handleDeviceDescription(devId, payload);
} else if (uri.equalsIgnoreCase(COLOIT_URI_DEVSTATUS)
|| (uri.isEmpty() && payload.contains(COIOT_TAG_GENERIC))) {
handleStatusUpdate(devId, payload, serial);
}
// fixed malformed JSON :-(
payload = fixJSON(payload);
try {
if (uri.equalsIgnoreCase(COLOIT_URI_DEVDESC)
|| (uri.isEmpty() && payload.contains(COIOT_TAG_BLK))) {
handleDeviceDescription(devId, payload);
} else if (uri.equalsIgnoreCase(COLOIT_URI_DEVSTATUS)
|| (uri.isEmpty() && payload.contains(COIOT_TAG_GENERIC))) {
handleStatusUpdate(devId, payload, serial);
}
} catch (ShellyApiException e) {
logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
}
} else {
// error handling
logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, response.getCode(),
response.getPayloadString());
} catch (ShellyApiException e) {
logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
coiotErrors++;
}
if (!discovering) {
@ -261,6 +291,7 @@ public class ShellyCoapHandler implements ShellyCoapListener {
} catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
resetSerial();
coiotErrors++;
}
}
@ -541,7 +572,7 @@ public class ShellyCoapHandler implements ShellyCoapListener {
}
resetSerial();
return newRequest(ipAddress, uri, con).send();
return newRequest(ipAddress, coiotPort, uri, con).send();
}
/**
@ -555,10 +586,10 @@ public class ShellyCoapHandler implements ShellyCoapListener {
* @return new packet
*/
private Request newRequest(String ipAddress, String uri, Type con) {
private Request newRequest(String ipAddress, int port, String uri, Type con) {
// We need to build our own Request to set an empty Token
Request request = new Request(Code.GET, con);
request.setURI(completeUrl(ipAddress, uri));
request.setURI(completeUrl(ipAddress, port, uri));
request.setToken(EMPTY_BYTE);
request.addMessageObserver(new MessageObserverAdapter() {
@Override
@ -601,26 +632,37 @@ public class ShellyCoapHandler implements ShellyCoapListener {
if (isStarted()) {
logger.debug("{}: Stopping CoAP Listener", thingName);
coapServer.stop(this);
if (statusClient != null) {
statusClient.shutdown();
CoapClient cclient = statusClient;
if (cclient != null) {
cclient.shutdown();
statusClient = null;
}
if (!reqDescription.isCanceled()) {
reqDescription.cancel();
Request request = reqDescription;
if (!request.isCanceled()) {
request.cancel();
}
if (!reqStatus.isCanceled()) {
reqStatus.cancel();
request = reqStatus;
if (!request.isCanceled()) {
request.cancel();
}
}
resetSerial();
coiotBound = false;
}
public long getMessageCount() {
return coiotMessages;
}
public long getErrorCount() {
return coiotErrors;
}
public void dispose() {
stop();
}
private static String completeUrl(String ipAddress, String uri) {
return "coap://" + ipAddress + ":" + COIOT_PORT + uri;
private static String completeUrl(String ipAddress, int port, String uri) {
return "coap://" + ipAddress + ":" + port + uri;
}
}

@ -49,7 +49,7 @@ public class ShellyCoapServer {
boolean started = false;
private CoapEndpoint statusEndpoint = new CoapEndpoint.Builder().build();
private @Nullable UdpMulticastConnector statusConnector;
private final CoapServer server = new CoapServer(NetworkConfig.getStandard(), COIOT_PORT);;
private CoapServer server = new CoapServer(NetworkConfig.getStandard(), COIOT_PORT);
private final Set<ShellyCoapListener> coapListeners = ConcurrentHashMap.newKeySet();
protected class ShellyStatusListener extends CoapResource {
@ -68,6 +68,7 @@ public class ShellyCoapServer {
Code code = exchange.getRequest().getCode();
switch (code) {
case CUSTOM_30:
case PUT: // Shelly Motion beta: incorrect, but handle the format
listener.processResponse(createResponse(request));
break;
default:
@ -77,17 +78,18 @@ public class ShellyCoapServer {
}
}
public synchronized void start(String localIp, ShellyCoapListener listener)
public synchronized void start(String localIp, int port, ShellyCoapListener listener)
throws UnknownHostException, SocketException {
if (!started) {
logger.debug("Initializing CoIoT listener (local IP={}:{})", localIp, COIOT_PORT);
logger.debug("Initializing CoIoT listener (local IP={}:{})", localIp, port);
NetworkConfig nc = NetworkConfig.getStandard();
InetAddress localAddr = InetAddress.getByName(localIp);
InetSocketAddress localPort = new InetSocketAddress(COIOT_PORT);
InetSocketAddress localPort = new InetSocketAddress(port);
// Join the multicast group on the selected network interface
statusConnector = new UdpMulticastConnector(localAddr, localPort, CoAP.MULTICAST_IPV4); // bind UDP listener
statusEndpoint = new CoapEndpoint.Builder().setNetworkConfig(nc).setConnector(statusConnector).build();
server = new CoapServer(NetworkConfig.getStandard(), port);
server.addEndpoint(statusEndpoint);
CoapResource cit = new ShellyStatusListener("cit", this);
CoapResource s = new ShellyStatusListener("s", this);

@ -20,6 +20,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link ShellyBindingConfiguration} class contains fields mapping binding configuration parameters.
@ -34,8 +35,8 @@ public class ShellyBindingConfiguration {
public static final String CONFIG_LOCAL_IP = "localIP";
public static final String CONFIG_AUTOCOIOT = "autoCoIoT";
public String defaultUserId = ""; // default for http basic user id
public String defaultPassword = ""; // default for http basic auth password
public String defaultUserId = "admin"; // default for http basic user id
public String defaultPassword = "admin"; // default for http basic auth password
public String localIP = ""; // default:use OH network config
public boolean autoCoIoT = true;
@ -59,7 +60,10 @@ public class ShellyBindingConfiguration {
}
}
public void updateFromProperties(Dictionary<String, Object> properties) {
public void updateFromProperties(@Nullable Dictionary<String, Object> properties) {
if (properties == null) { // saw this once
return;
}
List<String> keys = Collections.list(properties.keys());
Map<String, Object> dictCopy = keys.stream().collect(Collectors.toMap(Function.identity(), properties::get));
updateFromProperties(dictCopy);

@ -159,7 +159,7 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
// create shellyunknown thing - will be changed during thing initialization with valid credentials
thingUID = ShellyThingCreator.getThingUID(name, model, mode, true);
} else {
logger.info("{}: {}", name, messages.get("discovery.failed", address, e.toString()));
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);

@ -129,6 +129,10 @@ public class ShellyThingCreator {
if (name.startsWith(THING_TYPE_SHELLYRGBW2_PREFIX)) {
return mode.equals(SHELLY_MODE_COLOR) ? THING_TYPE_SHELLYRGBW2_COLOR_STR : THING_TYPE_SHELLYRGBW2_WHITE_STR;
}
if (name.startsWith(THING_TYPE_SHELLYMOTION_STR)) {
// depending on firmware release the Motion advertises under shellymotion-xxx or shellymotionsensor-xxxx
return THING_TYPE_SHELLYMOTION_STR;
}
// Check general mapping
if (!deviceType.isEmpty()) {

@ -83,6 +83,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
protected ShellyBindingConfiguration bindingConfig;
protected ShellyThingConfiguration config = new ShellyThingConfiguration();
protected ShellyDeviceProfile profile = new ShellyDeviceProfile(); // init empty profile to avoid NPE
protected ShellyDeviceStats stats = new ShellyDeviceStats();
private final ShellyCoapHandler coap;
public boolean autoCoIoT = false;
@ -90,9 +91,6 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
protected boolean stopping = false;
private boolean channelsCreated = false;
private long lastUptime = 0;
private long lastAlarmTs = 0;
private long lastTimeoutErros = -1;
private long watchdog = now();
private @Nullable ScheduledFuture<?> statusJob;
@ -179,7 +177,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
}
/**
* This routine is called every time the Thing configuration has been changed.
* This routine is called every time the Thing configuration has been changed
*/
@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
@ -218,7 +216,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
// Setup CoAP listener to we get the CoAP message, which triggers initialization even the thing could not be
// fully initialized here. In this case the CoAP messages triggers auto-initialization (like the Action URL does
// when enabled)
if (config.eventsCoIoT && profile.hasBattery && !profile.isSense) {
if (config.eventsCoIoT && profile.hasBattery && !profile.isMotion && !profile.isSense) {
coap.start(thingName, config);
}
@ -240,6 +238,10 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-wrong-mode");
return false;
}
if (!getString(devInfo.coiot).isEmpty()) {
// New Shelly devices might use a different endpoint for the CoAP listener
tmpPrf.coiotEndpoint = devInfo.coiot;
}
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {} ({})",
thingName, tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion,
@ -255,10 +257,10 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
tmpPrf.updatePeriod);
// update thing properties
ShellySettingsStatus status = api.getStatus();
tmpPrf.updateFromStatus(status);
updateProperties(tmpPrf, status);
checkVersion(tmpPrf, status);
tmpPrf.status = api.getStatus();
tmpPrf.updateFromStatus(tmpPrf.status);
updateProperties(tmpPrf, tmpPrf.status);
checkVersion(tmpPrf, tmpPrf.status);
if (autoCoIoT) {
logger.debug("{}: Auto-CoIoT is enabled, disabling action urls", thingName);
config.eventsCoIoT = true;
@ -271,8 +273,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
}
// All initialization done, so keep the profile and set Thing to ONLINE
profile = tmpPrf;
fillDeviceStatus(status, false);
fillDeviceStatus(tmpPrf.status, false);
postEvent(ALARM_TYPE_NONE, false);
api.setActionURLs(); // register event urls
if (config.eventsCoIoT) {
@ -281,6 +282,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
}
logger.debug("{}: Thing successfully initialized.", thingName);
profile = tmpPrf;
setThingOnline(); // if API call was successful the thing must be online
return true; // success
@ -370,6 +372,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
logger.trace("{}: Updating status", thingName);
ShellySettingsStatus status = api.getStatus();
profile.status = status;
profile.updateFromStatus(status);
// If status update was successful the thing must be online
@ -379,6 +382,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_NAME, getStringType(profile.settings.name));
updated |= this.updateDeviceStatus(status);
updated |= ShellyComponents.updateDeviceStatus(this, status);
fillDeviceStatus(status, updated);
updated |= updateInputs(status);
updated |= updateMeters(this, status);
updated |= updateSensors(this, status);
@ -388,10 +392,6 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
// Restart watchdog when status update was successful (no exception)
restartWatchdog();
if (scheduledUpdates <= 1) {
fillDeviceStatus(status, updated);
}
}
} catch (ShellyApiException e) {
// http call failed: go offline except for battery devices, which might be in
@ -402,7 +402,6 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
if (!isWatchdogExpired()) {
logger.debug("{}: Ignore API Timeout, retry later", thingName);
} else {
logger.debug("{}: Watchdog expired after {}sec,", thingName, profile.updatePeriod);
if (isThingOnline()) {
status = "offline.status-error-watchdog";
}
@ -473,17 +472,15 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
}
private boolean isWatchdogExpired() {
long timeout = profile.hasBattery ? profile.updatePeriod : profile.updatePeriod;
long delta = now() - watchdog;
if ((watchdog > 0) && (delta > timeout)) {
logger.trace("{}: Watchdog expired after {}sec (started={}, now={}", thingName, delta, watchdog, now());
if ((watchdog > 0) && (delta > profile.updatePeriod)) {
stats.remainingWatchdog = delta;
return true;
}
return false;
}
private boolean isWatchdogStarted() {
logger.trace("{}: Watchdog is {}", thingName, watchdog > 0 ? "started" : "inactive");
return watchdog > 0;
}
@ -495,23 +492,26 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
private void fillDeviceStatus(ShellySettingsStatus status, boolean updated) {
String alarm = "";
boolean force = false;
Map<String, String> propertyUpdates = new TreeMap<>();
// Update uptime and WiFi, internal temp
ShellyComponents.updateDeviceStatus(this, status);
if (api.isInitialized() && (lastTimeoutErros != api.getTimeoutErrors())) {
propertyUpdates.put(PROPERTY_STATS_TIMEOUTS, String.valueOf(api.getTimeoutErrors()));
propertyUpdates.put(PROPERTY_STATS_TRECOVERED, String.valueOf(api.getTimeoutsRecovered()));
lastTimeoutErros = api.getTimeoutErrors();
if (api.isInitialized()) {
stats.timeoutErrors = api.getTimeoutErrors();
stats.timeoutsRecorvered = api.getTimeoutsRecovered();
}
stats.remainingWatchdog = watchdog > 0 ? now() - watchdog : 0;
// Check various device indicators like overheating
if ((status.uptime < lastUptime) && (profile.isInitialized()) && !profile.hasBattery) {
logger.debug("{}: status.update={}, lastUpdate={}", thingName, status.uptime, stats.lastUptime);
if ((status.uptime < stats.lastUptime) && profile.isInitialized()) {
alarm = ALARM_TYPE_RESTARTED;
force = true;
stats.unexpectedRestarts++;
logger.debug("{}: Device restart #{} detected", thingName, stats.unexpectedRestarts);
// Force re-initialization on next status update
if (!profile.hasBattery) {
if (!profile.hasBattery || profile.isMotion) {
reinitializeThing();
}
} else if (getBool(status.overtemperature)) {
@ -521,15 +521,13 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
} else if (getBool(status.loaderror)) {
alarm = ALARM_TYPE_LOADERR;
}
lastUptime = getLong(status.uptime);
stats.lastUptime = getLong(status.uptime);
stats.coiotMessages = coap.getMessageCount();
stats.coiotErrors = coap.getErrorCount();
if (!alarm.isEmpty()) {
postEvent(alarm, force);
}
if (!propertyUpdates.isEmpty()) {
flushProperties(propertyUpdates);
}
}
/**
@ -542,14 +540,16 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
State value = cache.getValue(channelId);
String lastAlarm = value != UnDefType.NULL ? value.toString() : "";
if (force || !lastAlarm.equals(alarm) || (now() > (lastAlarmTs + HEALTH_CHECK_INTERVAL_SEC))) {
if (alarm.equals(ALARM_TYPE_NONE)) {
if (force || !lastAlarm.equals(alarm) || (now() > (stats.lastAlarmTs + HEALTH_CHECK_INTERVAL_SEC))) {
if (alarm.isEmpty() || alarm.equals(ALARM_TYPE_NONE)) {
cache.updateChannel(channelId, getStringType(alarm));
} else {
logger.info("{}: {}", thingName, messages.get("event.triggered", alarm));
triggerChannel(channelId, alarm);
cache.updateChannel(channelId, getStringType(alarm));
lastAlarmTs = now();
stats.lastAlarm = alarm;
stats.lastAlarmTs = now();
stats.alarms++;
}
}
}
@ -757,7 +757,8 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
prf.fwId, SHELLY_API_MIN_FWVERSION));
}
}
if (bindingConfig.autoCoIoT && (version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT) >= 0)) {
if (bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0)
|| (prf.fwVersion.equalsIgnoreCase("production_test"))) {
if (!config.eventsCoIoT) {
logger.info("{}: {}", thingName, messages.get("versioncheck.autocoiot"));
}
@ -1011,6 +1012,10 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
properties.put(PROPERTY_SERVICE_NAME, hostname);
logger.trace("{}: Updated serrviceName to {}", thingName, hostname);
}
String deviceName = getString(profile.settings.name);
if (!deviceName.isEmpty()) {
properties.put(PROPERTY_DEV_NAME, deviceName);
}
// add status properties
if (status.wifiSta != null) {
@ -1179,4 +1184,17 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiException {
return false;
}
public ShellyDeviceStats getStats() {
return stats;
}
public Map<String, String> getStatsProp() {
return stats.asProperties(getString(profile.settings.timezone));
}
public void resetStats() {
// reset statistics
stats = new ShellyDeviceStats();
}
}

@ -30,6 +30,7 @@ import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.UnDefType;
/***
* The{@link ShellyComponents} implements updates for supplemental components
@ -56,7 +57,7 @@ public class ShellyComponents {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPTIME,
toQuantityType((double) getLong(status.uptime), DIGITS_NONE, Units.SECOND));
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI, mapSignalStrength(rssi));
if (status.tmp != null) {
if ((status.tmp != null) && !thingHandler.getProfile().isSensor) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
} else if (status.temperature != null) {
@ -217,7 +218,7 @@ public class ShellyComponents {
}
}
if (updated && !profile.isRoller && !profile.isRGBW2) {
if (!profile.isRoller && !profile.isRGBW2) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS,
toQuantityType(accumulatedWatts, DIGITS_WATT, Units.WATT));
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL,
@ -244,7 +245,7 @@ public class ShellyComponents {
ShellyDeviceProfile profile = thingHandler.getProfile();
boolean updated = false;
if (profile.isSensor || profile.hasBattery || profile.isSense) {
if (profile.isSensor || profile.hasBattery) {
ShellyStatusSensor sdata = thingHandler.api.getSensorStatus();
if (!thingHandler.areChannelsCreated()) {
@ -260,6 +261,7 @@ public class ShellyComponents {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
getString(sdata.sensor.state).equalsIgnoreCase(SHELLY_API_DWSTATE_OPEN) ? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
String sensorError = getString(sdata.sensorError);
boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR,
getStringType(sdata.sensorError));
if (changed) {
@ -325,10 +327,21 @@ public class ShellyComponents {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VOLTAGE,
getDecimal(adc.voltage));
}
boolean charger = (getInteger(profile.settings.externalPower) == 1) || getBool(sdata.charger);
if ((profile.settings.externalPower != null) || (sdata.charger != null)) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
charger ? OnOffType.ON : OnOffType.OFF);
}
if (sdata.bat != null) { // no update for Sense
thingHandler.logger.trace("{}: Updating battery", thingHandler.thingName);
updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
toQuantityType(getDouble(sdata.bat.value), DIGITS_PERCENT, Units.PERCENT));
// Shelly HT has external_power under settings, Sense and Motion charger under status
if (!charger || !profile.isHT) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
toQuantityType(getDouble(sdata.bat.value), 0, Units.PERCENT));
} else {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
UnDefType.UNDEF);
}
boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW,
getDouble(sdata.bat.value) < thingHandler.config.lowBattery ? OnOffType.ON : OnOffType.OFF);
updated |= changed;
@ -336,6 +349,7 @@ public class ShellyComponents {
thingHandler.postEvent(ALARM_TYPE_LOW_BATTERY, false);
}
}
if (sdata.motion != null) { // Shelly Sense
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
getOnOff(sdata.motion));
@ -343,15 +357,14 @@ public class ShellyComponents {
if (sdata.sensor != null) { // Shelly Motion
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
getOnOff(sdata.sensor.motion));
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
getTimestamp(getString(profile.settings.timezone), sdata.sensor.motionTimestamp));
long timestamp = getLong(sdata.sensor.motionTimestamp);
if (timestamp != 0) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
getTimestamp(getString(profile.settings.timezone), timestamp));
}
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
getOnOff(sdata.sensor.vibration));
}
if (sdata.charger != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
getOnOff(sdata.charger));
}
updated |= thingHandler.updateInputs(status);

@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2021 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.handler;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.util.ShellyUtils;
/***
* {@link ShellyDeviceStats} some statistical values for the thing
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyDeviceStats {
public long lastUptime = 0;
public long unexpectedRestarts = 0;
public long timeoutErrors = 0;
public long timeoutsRecorvered = 0;
public long remainingWatchdog = 0;
public long alarms = 0;
public String lastAlarm = "";
public long lastAlarmTs = 0;
public long coiotMessages = 0;
public long coiotErrors = 0;
public Map<String, String> asProperties(String timeZone) {
Map<String, String> prop = new HashMap<>();
prop.put("lastUptime", String.valueOf(lastUptime));
prop.put("unexpectedRestarts", String.valueOf(unexpectedRestarts));
prop.put("timeoutErrors", String.valueOf(timeoutErrors));
prop.put("timeoutsRecovered", String.valueOf(timeoutsRecorvered));
prop.put("remainingWatchdog", String.valueOf(remainingWatchdog));
prop.put("alarmCount", String.valueOf(alarms));
prop.put("lastAlarm", lastAlarm);
prop.put("lastAlarmTs",
lastAlarmTs != 0 ? ShellyUtils.getTimestamp(timeZone, lastAlarmTs).format(null).replace('T', ' ') : "");
prop.put("coiotMessages", String.valueOf(coiotMessages));
prop.put("coiotErrors", String.valueOf(coiotErrors));
return prop;
}
}

@ -235,7 +235,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
}
}
if ((command == UpDownType.UP) || (command == OnOffType.ON)) {
if (command == UpDownType.UP || command == OnOffType.ON) {
logger.debug("{}: Open roller", thingName);
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
int pos = profile.getRollerFav(config.favoriteUP - 1);
@ -244,7 +244,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
logger.debug("{}: Use favoriteUP id {} for positioning roller({}%)", thingName, config.favoriteUP,
pos);
}
} else if ((command == UpDownType.DOWN) || (command == OnOffType.OFF)) {
} else if (command == UpDownType.DOWN || command == OnOffType.OFF) {
logger.debug("{}: Closing roller", thingName);
int pos = profile.getRollerFav(config.favoriteDOWN - 1);
if (pos > 0) {

@ -410,9 +410,12 @@ public class ShellyChannelDefinitions {
CHANNEL_SENSOR_ILLUM);
addChannel(thing, newChannels, sdata.flood != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD);
addChannel(thing, newChannels, sdata.smoke != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD);
addChannel(thing, newChannels, sdata.charger != null, CHGR_DEVST, CHANNEL_DEVST_CHARGER);
addChannel(thing, newChannels, sdata.motion != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION);
if (sdata.sensor != null) { // DW2 or Motion
addChannel(thing, newChannels, (profile.settings.externalPower != null) || (sdata.charger != null), CHGR_DEVST,
CHANNEL_DEVST_CHARGER);
addChannel(thing, newChannels,
sdata.motion != null || ((sdata.sensor != null) && (sdata.sensor.motion != null)), CHANNEL_GROUP_SENSOR,
CHANNEL_SENSOR_MOTION);
if (sdata.sensor != null) { // DW, Sense or Motion
addChannel(thing, newChannels, sdata.sensor.state != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT); // DW/DW2
addChannel(thing, newChannels, sdata.sensor.motionTimestamp != null, CHANNEL_GROUP_SENSOR, // Motion
CHANNEL_SENSOR_MOTION_TS);

@ -10,6 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.util;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.mkChannelId;

@ -134,13 +134,14 @@ public class ShellyUtils {
}
public static String substringAfterLast(@Nullable String string, String pattern) {
if (string != null) {
int pos = string.lastIndexOf(pattern);
if (pos != -1) {
return string.substring(pos + pattern.length());
}
if (string == null) {
return "";
}
return "";
int pos = string.lastIndexOf(pattern);
if (pos != -1) {
return string.substring(pos + pattern.length());
}
return string;
}
public static String substringBetween(@Nullable String string, String begin, String end) {
@ -236,12 +237,11 @@ public class ShellyUtils {
}
}
public static String urlEncode(String input) throws ShellyApiException {
public static String urlEncode(String input) {
try {
return URLEncoder.encode(input, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
throw new ShellyApiException(
"Unsupported encoding format: " + StandardCharsets.UTF_8.toString() + ", input=" + input, e);
return input;
}
}

@ -8,7 +8,7 @@ binding.shelly.config.autoCoIoT.label = Auto-CoIoT
binding.shelly.config.autoCoIoT.description = If enabled CoIoT will be automatically used when the devices runs a firmware version 1.6 or newer; false: Use thing configuration to enabled/disable CoIoT events.
discovery.failed# Config status messages
config-status.error.missing-device-ip=IP address of the Shelly device is missing.
config-status.error.missing-device-ip = IP address of the Shelly device is missing.
# Thing status descriptions
offline.conf-error-no-credentials = Device is password protected, but no credentials have been configured.
@ -18,6 +18,7 @@ offline.status-error-timeout = Device is not reachable (API timeout).
offline.status-error-unexpected-api-result = An unexpected API response. Please verify the logfile to get more detailed information.
offline.status-error-watchdog = Device is not responding, seems to be unavailable.
offline.status-error-restarted = The device has restarted and will be re-initialized.
offline.status-error-fwupgrade = Firmware upgrade in progress
message.versioncheck.failed = Unable to check firmware version: {0}
message.versioncheck.beta = Device is running a Beta version: {0}/{1} ({2}),make sure this is newer than {3} release build.

@ -11,7 +11,7 @@ binding.shelly.config.autoCoIoT.label = Auto-CoIoT
binding.shelly.config.autoCoIoT.description = Bei aktiviertem Auto-CoIoT wird das Protokoll aktiviert, sobald das Gerät eine Firmwareversion 1.6 oder neuer verwendet. Andernfalls wird dies über die Thing-Konfiguration gesteuert.
# Config status messages
config-status.error.missing-deviceip=Die IP-Adresse des Shelly Gerätes ist nicht konfiguriert.
config-status.error.missing-device-ip = Die IP-Adresse des Shelly Gerätes ist nicht konfiguriert.
# Thing status descriptions
offline.conf-error-no-credentials = Gerät ist passwortgeschützt, aber es sind keine Anmeldedaten konfiguriert.
@ -21,6 +21,7 @@ offline.status-error-timeout = Das Ger
offline.status-error-unexpected-api-result = Es trat ein unerwartetes Problem beim API-Zugriff auf. Überprüfen Sie die Logdatei für genauere Informationen.
offline.status-error-watchdog = Das Gerät antwortet nicht und ist vermutlich nicht mehr verfügbar.
offline.status-error-restarted = Das Gerät wurde neu gestartet und wird erneut initialisiert.
offline.status-error-fwupgrade = Gerätesoftware wird aktualisiert
# Status error messages
config-status.error.missing-userid = Keine Benutzerkennung in der Thing Konfiguration
@ -28,7 +29,7 @@ config-status.error.missing-userid = Keine Benutzerkennung in der Thing Konfigur
# General messages
message.versioncheck.failed = Firmware-Version konnte nicht geprüft werden: {0}
message.versioncheck.beta = Es wurde eine Betaversion erkannt: {0}/{1} ({2}), bitte sicherstellen, dass diese neuer ist als Version {3} (Release Build).
message.versioncheck.tooold = ACHTUNG: Eine alter Firmware wurde erkannt: {0}/{1} ({2}), minimal erforderlich {3}.
message.versioncheck.tooold = ACHTUNG: Eine alte Firmware wurde erkannt: {0}/{1} ({2}), minimal erforderlich {3}.
message.versioncheck.update = INFO: Eine neue Firmwareversion ist verfügbar, aktuell: {0}, neu: {1}
message.versioncheck.autocoiot = INFO: Die Firmware unterstützt die Anforderung, Auto-CoIoT wurde aktiviert.
message.init.noipaddress = Es konnte keine lokale IP-Adresse ermittelt werden. Bitte sicherstellen, dass IPv4 aktiviert ist und das richtige Interface in der openHAB Netzwerk-Konfiguration ausgewählt ist.
@ -506,8 +507,8 @@ channel-type.shelly.whiteBrightness.label = Helligkeit
channel-type.shelly.whiteBrightness.description = Helligkeit (0-100%, 0=aus)
channel-type.shelly.meterWatts.label = Leistung
channel-type.shelly.meterWatts.description = Aktueller Stromverbrauch in Watt
channel-type.shelly.meterAccuWatts.label = Kumulierte Verbrauch
channel-type.shelly.meterAccuWatts.description = Kumulierterr Verbrauch in Watt
channel-type.shelly.meterAccuWatts.label = Kumulierter Verbrauch
channel-type.shelly.meterAccuWatts.description = Kumulierter Verbrauch in Watt
channel-type.shelly.meterAccuTotal.label = Kumulierter Gesamtverbrauch
channel-type.shelly.meterAccuTotal.description = Kumulierter Gesamtverbrauch in kW/h
channel-type.shelly.meterAccuReturned.label = Kumulierte Einspeisung
@ -591,7 +592,7 @@ channel-type.shelly.sensorIllumination.state.option.unknown = Unbekannt
channel-type.shelly.sensorIllumination.description = Angabe zum erkannten Tageslichtwert
channel-type.shelly.sensorPPM.label = Gas-Konzentration
channel-type.shelly.sensorPPM.description = Gemessene Konzentration in PPM
channel-type.shelly.sensorADC.label = Spannung (ADC)
channel-type.shelly.sensorADC.label = Voltage (ADC)
channel-type.shelly.sensorADC.description = Gemessene Spannung
channel-type.shelly.sensorTilt.label = Öffnungswinkel
channel-type.shelly.sensorTilt.description = Öffnungswinkel in Grad (erfordert Kalibrierung in der App)

@ -65,11 +65,11 @@
<state readOnly="true">
</state>
</channel-type>
<channel-type id="batVoltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>Battery Voltage</label>
<description>Battery voltage in V</description>
<state readOnly="true" pattern="%.1f %unit%">
<channel-type id="externalPower" advanced="true">
<item-type>Switch</item-type>
<label>External Power</label>
<description>ON: External power is connected</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="uptime" advanced="true">

@ -115,7 +115,7 @@
</thing-type>
<thing-type id="shellymotion">
<label>Shelly Motion</label>
<label>Shelly Motion (SHMOS-1)</label>
<description>Shelly Motion Sensor (battery powered)</description>
<channel-groups>