[shelly] Add Shelly Motion, minor improvements (#10054)
* 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>
@ -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 |
|
||||
|
Before ![]() (image error) Size: 144 KiB After ![]() (image error) Size: 120 KiB ![]() ![]() |
Before ![]() (image error) Size: 143 KiB After ![]() (image error) Size: 102 KiB ![]() ![]() |
Before ![]() (image error) Size: 144 KiB After ![]() (image error) Size: 120 KiB ![]() ![]() |
Before ![]() (image error) Size: 218 KiB After ![]() (image error) Size: 145 KiB ![]() ![]() |
Before ![]() (image error) Size: 224 KiB After ![]() (image error) Size: 148 KiB ![]() ![]() |
Before ![]() (image error) Size: 222 KiB After ![]() (image error) Size: 150 KiB ![]() ![]() |
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);
|
||||
|
||||
|
54
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyDeviceStats.java
Normal file
@ -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>
|
||||
|