# Shelly Binding
This Binding integrates [Shelly devices](https://shelly.cloud) devloped by Allterco.
+
      
Allterco provides a rich set of smart home devices. All of them are WiFi enabled (2,4GHz, IPv4 only) and provide a documented API.
### Shelly Motion (thing-type: shellymotion)
+Important: The Shelly Motion does only support CoIoT Unicast, which means you need to set the CoIoT peer address.
+
+Use device WebUI, open COIOT settings, make sure CoIoT is enabled and enter the openHAB IP address or
+
|Group |Channel |Type |read-only|Description |
|----------|---------------|---------|---------|---------------------------------------------------------------------|
|sensors |motion |Switch |yes |ON: Motion was detected |
| |illumination |String |yes |Current illumination: dark/twilight/bright |
| |vibration |Switch |yes |ON: Vibration detected |
| |charger |Switch |yes |ON: USB charging cable is connected external power supply activated. |
+| |motionActive |Switch |yes |ON: Motion detection is currently active |
+| |sensorSleepTime|Number |no |Specifies the number of sec the sensor should not report events ]
| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) |
|battery |batteryLevel |Number |yes |Battery Level in % |
| |lowBattery |Switch |yes |Low battery alert (< 20%) |
+Use case for the 'sensorSleepTime':
+You have a Motion controlling your light.
+You switch off the light and want to leave the room, but the motion sensor immediately switches light back on.
+Using 'sensorSleepTime' you could suppress motion events while leaving the room, e.g. for 5sec and the light doesn's switch on.
+
### Shelly Button 1 (thing-type: shellybutton1)
|Group |Channel |Type |read-only|Description |
public static final String CHANNEL_SENSOR_VALVE = "valve";
public static final String CHANNEL_SENSOR_SSTATE = "status"; // Shelly Gas
public static final String CHANNEL_SENSOR_ALARM_STATE = "alarmState";
+ public static final String CHANNEL_SENSOR_MOTION_ACT = "motionActive";
public static final String CHANNEL_SENSOR_MOTION = "motion";
public static final String CHANNEL_SENSOR_MOTION_TS = "motionTimestamp";
+ public static final String CHANNEL_SENSOR_SLEEPTIME = "sensorSleepTime";
public static final String CHANNEL_SENSOR_ERROR = "lastError";
// External sensors for Shelly1/1PM
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyLightHandler;
+import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.handler.ShellyProtectedHandler;
import org.openhab.binding.shelly.internal.handler.ShellyRelayHandler;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
return null;
}
- public Map<String, ShellyBaseHandler> getThingHandlers() {
- return deviceListeners;
+ public Map<String, ShellyManagerInterface> getThingHandlers() {
+ return new HashMap<>(deviceListeners);
}
/**
import java.util.ArrayList;
import java.util.List;
+import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor.ShellyMotionSettings;
import org.openhab.core.thing.CommonTriggerEvents;
import com.google.gson.annotations.SerializedName;
public static final String SHELLY_TEMP_CELSIUS = "C";
public static final String SHELLY_TEMP_FAHRENHEIT = "F";
+ // Motion
+ public static final int SHELLY_MOTION_SLEEPTIME_OFFSET = 3; // we need to substract and offset
+
+ // CoIoT Multicast setting
+ public static final String SHELLY_COIOT_MCAST = "mcast";
+
public static class ShellySettingsDevice {
public String type;
public String mac;
}
public static class ShellySettingsMqtt {
- public Boolean enabled;
+ public Boolean enable;
public String server;
public String user;
@SerializedName("reconnect_timeout_max")
public static class ShellySettingsCoiot { // FW 1.6+
@SerializedName("update_period")
public Integer updatePeriod;
+ public Boolean enabled; // Motion 1.0.7: Coap can be disabled
+ public String peer; // if set the device uses singlecast CoAP, mcast=set back to Multicast
+ }
+
+ public static class ShellyStatusMqtt {
+ public Boolean connected;
}
public static class ShellySettingsSntp {
public String server;
+ public Boolean enabled;
}
public static class ShellySettingsLogin {
public Boolean connected;
}
- public static class ShellyStatusMqtt {
- public Boolean connected;
- }
-
public static class ShellySettingsHwInfo {
@SerializedName("hw_revision")
public String hwRevision;
public ShellySettingsWiFiNetwork wifiSta;
@SerializedName("wifi_sta1")
public ShellySettingsWiFiNetwork wifiSta1;
- // public ShellySettingsMqtt mqtt; // not used for now
- // public ShellySettingsSntp sntp; // not used for now
+ @SerializedName("wifirecovery_reboot_enabled")
+ public Boolean wifiRecoveryReboot;
+
+ public ShellySettingsMqtt mqtt; // not used for now
+ public ShellySettingsSntp sntp; // not used for now
public ShellySettingsCoiot coiot; // Firmware 1.6+
public ShellySettingsLogin login;
@SerializedName("pin_code")
public Boolean discoverable; // FW 1.6+
public String fw;
@SerializedName("build_info")
- ShellySettingsBuildInfo buildInfo;
- ShellyStatusCloud cloud;
+ public ShellySettingsBuildInfo buildInfo;
+ public ShellyStatusCloud cloud;
@SerializedName("sleep_mode")
public ShellySensorSleepMode sleepMode; // FW 1.6
@SerializedName("external_power")
@SerializedName("favorites_enabled")
public Boolean favoritesEnabled;
public ArrayList<ShellyFavPos> favorites;
+
+ // Motion
+ public ShellyMotionSettings motion;
+ @SerializedName("tamper_sensitivity")
+ public Integer tamperSensitivity;
+ @SerializedName("dark_threshold")
+ public Integer darkThreshold;
+ @SerializedName("twilight_threshold")
+ public Integer twilightThreshold;
+
+ @SerializedName("sleep_time") // Shelly Motion
+ public Integer sleepTime;
}
public static class ShellySettingsAttributes {
public String fw; // current FW version
}
+ public static class ShellyActionsStats {
+ public Integer skipped;
+ }
+
public static class ShellySettingsStatus {
public String name; // FW 1.8: Symbolic Device name is configurable
@SerializedName("wifi_sta")
public ShellySettingsWiFiNetwork wifiSta; // WiFi client configuration. See /settings/sta for details
+ public ShellyStatusCloud cloud;
+ public ShellyStatusMqtt mqtt;
public String time;
public Integer serial;
public Boolean discoverable; // FW 1.6+
@SerializedName("cfg_changed_cnt")
public Integer cfgChangedCount; // FW 1.8
+ @SerializedName("actions_stats")
+ public ShellyActionsStats astats;
public ArrayList<ShellySettingsRelay> relays;
public ArrayList<ShellySettingsRoller> rollers;
public Long fsFree;
public Long uptime;
+ @SerializedName("sleep_time") // Shelly Motion
+ public Integer sleepTime;
+
public String json;
}
public Integer vibration; // Whether vibration is detected
}
+ public static class ShellyMotionSettings {
+ public Integer sensitivity;
+ @SerializedName("blind_time_minutes")
+ public Integer blindTimeMinutes;
+ @SerializedName("pulse_count")
+ public Integer pulseCount;
+ @SerializedName("operating_mode")
+ public Integer operatingMode;
+ public Boolean enabled;
+ }
+
public static class ShellyExtTemperature {
public static class ShellyShortTemp {
public Double tC; // temperature in deg C
public Boolean motion; // Shelly Sense: true=motion detected
public Boolean charger; // Shelly Sense: true=charger connected
- @SerializedName("external_power")
- public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense
@SerializedName("act_reasons")
public List<Object> actReasons; // HT/Smoke/Flood: list of reasons which woke up the device
import java.util.HashMap;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsGlobal;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsInput;
@NonNullByDefault
public class ShellyDeviceProfile {
private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class);
+ private final static Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+");
public boolean initialized = false; // true when initialized
public String hostname = "";
public String mode = "";
public boolean discoverable = true;
+ public boolean auth = false;
+ public boolean alwaysOn = true;
public String hwRev = "";
public String hwBatchId = "";
hostname = settings.device.hostname != null && !settings.device.hostname.isEmpty()
? settings.device.hostname.toLowerCase()
: "shelly-" + mac.toUpperCase().substring(6, 11);
- mode = !getString(settings.mode).isEmpty() ? getString(settings.mode).toLowerCase() : "";
+ mode = getString(settings.mode).toLowerCase();
hwRev = settings.hwinfo != null ? getString(settings.hwinfo.hwRevision) : "";
hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
fwDate = substringBefore(settings.fw, "/");
- fwVersion = substringBetween(settings.fw, "/", "@");
+ fwVersion = extractFwVersion(settings.fw);
fwId = substringAfter(settings.fw, "@");
discoverable = (settings.discoverable == null) || settings.discoverable;
if ((numRelays > 0) && (settings.relays == null)) {
numRelays = 0;
}
- isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2);
- isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER);
hasRelays = (numRelays > 0) || isDimmer;
numRollers = getInteger(settings.device.numRollers);
numInputs = settings.inputs != null ? settings.inputs.size() : hasRelays ? isRoller ? 2 : 1 : 0;
return;
}
+ isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2);
+ isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER);
+
isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR);
isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR)
|| thingType.equals(THING_TYPE_SHELLYDUORGBW_STR);
isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_STR);
isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR);
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
+ hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion;
+
+ alwaysOn = !hasBattery || isMotion || isSense; // true means: device is reachable all the time (no sleep mode)
}
public void updateFromStatus(ShellySettingsStatus status) {
if (hasRelays) {
- // Dimmer-2 doesn't report inputs under /settings, only on /status, we need to update that info after
- // initialization
+ // Dimmer-2 doesn't report inputs under /settings, only on /status, we need to update that info after init
if (status.inputs != null) {
numInputs = status.inputs.size();
}
}
return -1;
}
+
+ public static String extractFwVersion(@Nullable String version) {
+ if (version != null) {
+ Matcher matcher = VERSION_PATTERN.matcher(version);
+ if (matcher.find()) {
+ // e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master
+ return matcher.group(0);
+ }
+ }
+ return "";
+ }
+
+ public boolean coiotEnabled() {
+ if ((settings.coiot != null) && (settings.coiot.enabled != null)) {
+ return settings.coiot.enabled;
+ }
+
+ // If device is not yet intialized or the enabled property is missing we assume that CoIoT is enabled
+ return true;
+ }
}
.convert(getDouble(status.tmp.value));
status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value : f;
}
- if ((status.charger == null) && (status.externalPower != null)) {
+ if ((status.charger == null) && (profile.settings.externalPower != null)) {
// SHelly H&T uses external_power, Sense uses charger
- status.charger = status.externalPower != 0;
+ status.charger = profile.settings.externalPower != 0;
}
-
return status;
}
- public void setTimer(Integer index, String timerName, Double value) throws ShellyApiException {
+ public void setTimer(int index, String timerName, int value) throws ShellyApiException {
String type = SHELLY_CLASS_RELAY;
if (profile.isRoller) {
type = SHELLY_CLASS_ROLLER;
} else if (profile.isLight) {
type = SHELLY_CLASS_LIGHT;
}
- String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "="
- + ((Integer) value.intValue()).toString();
+ String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value;
request(uri);
}
+ public void setSleepTime(int value) throws ShellyApiException {
+ request(SHELLY_URL_SETTINGS + "?sleep_time=" + value);
+ }
+
public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
request(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
}
ShellySettingsLogin.class);
}
+ public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
+ return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class);
+ }
+
public String deviceReboot() throws ShellyApiException {
return callApi(SHELLY_URL_RESTART, String.class);
}
return callApi("/ota?" + uri, ShellySettingsUpdate.class);
}
+ public String setCloud(boolean enabled) throws ShellyApiException {
+ return callApi("/settings/cloud/?enabled=" + (enabled ? "1" : "0"), String.class);
+ }
+
/**
* Change between White and Color Mode
*
break;
case "1103": // roller_0: S, rollerPos, 0-100, unknown -1
int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min((int) value, SHELLY_MAX_ROLLER_POS));
+ logger.debug("{}: CoAP update roller position: control={}, position={}", thingName,
+ SHELLY_MAX_ROLLER_POS - pos, pos);
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
+ updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
+ toQuantityType((double) pos, Units.PERCENT));
break;
case "1105": // S, valvle, closed/opened/not_connected/failure/closing/opening/checking or unbknown
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE, getStringType(s.valueStr));
break;
case "3120": // motionActive
// {"I":3120,"T":"S","D":"motionActive","R":["0/1","-1"],"L":1},
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT,
+ getTimestamp(getString(profile.settings.timezone), (long) s.value));
break;
case "6108": // A, gas, none/mild/heavy/test or unknown
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
import org.openhab.binding.shelly.internal.coap.ShellyCoapHandler;
+import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO;
import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
-public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceListener {
+public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceListener, ShellyManagerInterface {
protected final Logger logger = LoggerFactory.getLogger(ShellyBaseHandler.class);
protected final ShellyChannelDefinitions channelDefinitions;
// 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.isMotion && !profile.isSense) {
+ if (config.eventsCoIoT && !profile.alwaysOn) {
coap.start(thingName, config);
}
// New Shelly devices might use a different endpoint for the CoAP listener
tmpPrf.coiotEndpoint = devInfo.coiot;
}
+ tmpPrf.auth = devInfo.auth; // missing in /settings
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {} ({})",
thingName, tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion,
logger.debug("{}: Shelly settings info for {}: {}", thingName, tmpPrf.hostname, tmpPrf.settingsJson);
logger.debug("{}: Device "
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})"
- + ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}"
- + ",updatePeriod:{}sec", thingName, tmpPrf.hasRelays, tmpPrf.numRelays, tmpPrf.isRoller,
+ + ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}"
+ + ",alwaysOn:{}, ,updatePeriod:{}sec", thingName, tmpPrf.hasRelays, tmpPrf.numRelays, tmpPrf.isRoller,
tmpPrf.numRollers, tmpPrf.isDimmer, tmpPrf.numMeters, tmpPrf.isEMeter, tmpPrf.isSensor, tmpPrf.isDW,
tmpPrf.hasBattery, tmpPrf.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "",
- tmpPrf.isSense, tmpPrf.isLight, profile.isBulb, tmpPrf.isDuo, tmpPrf.isRGBW2, tmpPrf.inColor,
- tmpPrf.updatePeriod);
+ tmpPrf.isSense, tmpPrf.isMotion, tmpPrf.isLight, profile.isBulb, tmpPrf.isDuo, tmpPrf.isRGBW2,
+ tmpPrf.inColor, tmpPrf.alwaysOn, tmpPrf.updatePeriod);
// update thing properties
tmpPrf.status = api.getStatus();
tmpPrf.updateFromStatus(tmpPrf.status);
updateProperties(tmpPrf, tmpPrf.status);
checkVersion(tmpPrf, tmpPrf.status);
+ if (config.eventsCoIoT && (tmpPrf.settings.coiot != null) && (tmpPrf.settings.coiot.enabled != null)) {
+ String devpeer = getString(tmpPrf.settings.coiot.peer);
+ String ourpeer = config.localIp + ":" + ShellyCoapJSonDTO.COIOT_PORT;
+ if (!tmpPrf.settings.coiot.enabled || (profile.isMotion && devpeer.isEmpty())) {
+ try {
+ api.setCoIoTPeer(ourpeer);
+ logger.info("{}: CoIoT peer updated to {}", thingName, ourpeer);
+ } catch (ShellyApiException e) {
+ logger.debug("{}: Unable to set CoIoT peer: {}", thingName, e.toString());
+ }
+ } else if (!devpeer.equals(ourpeer)) {
+ logger.warn("{}: CoIoT peer in device settings does not point this to this host, disabling CoIoT",
+ thingName);
+ config.eventsCoIoT = autoCoIoT = false;
+ }
+ }
if (autoCoIoT) {
logger.debug("{}: Auto-CoIoT is enabled, disabling action urls", thingName);
config.eventsCoIoT = true;
api.setLedStatus(SHELLY_LED_POWER_DISABLE, command == OnOffType.ON);
break;
+ case CHANNEL_SENSOR_SLEEPTIME:
+ logger.debug("{}: Set sensor sleep time to {}", thingName, command);
+ int value = ((DecimalType) command).intValue();
+ value = value > 0 ? Math.max(SHELLY_MOTION_SLEEPTIME_OFFSET, value - SHELLY_MOTION_SLEEPTIME_OFFSET)
+ : 0;
+ api.setSleepTime(value);
+ break;
+
default:
update = handleDeviceCommand(channelUID, command);
break;
initializeThing(); // may fire an exception if initialization failed
}
// Get profile, if refreshSettings == true reload settings from device
- profile = getProfile(refreshSettings);
-
- logger.trace("{}: Updating status", thingName);
+ logger.trace("{}: Updating status (refreshSettings={})", thingName, refreshSettings);
ShellySettingsStatus status = api.getStatus();
+ profile = getProfile(refreshSettings || checkRestarted(status));
profile.status = status;
profile.updateFromStatus(status);
return getThing().getStatus() == ThingStatus.OFFLINE;
}
+ @Override
public void setThingOnline() {
if (!isThingOnline()) {
updateStatus(ThingStatus.ONLINE);
// request 3 updates in a row (during the first 2+3*3 sec)
- requestUpdates(!profile.hasBattery ? 3 : 1, channelsCreated == false);
+ requestUpdates(profile.alwaysOn ? 3 : 1, channelsCreated == false);
}
restartWatchdog();
}
+ @Override
public void setThingOffline(ThingStatusDetail detail, String messageKey) {
if (!isThingOffline()) {
logger.info("{}: Thing goes OFFLINE: {}", thingName, messages.get(messageKey));
}
public void reinitializeThing() {
+ logger.debug("{}: Re-Initialize Thing", thingName);
updateStatus(ThingStatus.UNKNOWN);
- requestUpdates(1, true);
+ requestUpdates(0, true);
}
private void fillDeviceStatus(ShellySettingsStatus status, boolean updated) {
// Update uptime and WiFi, internal temp
ShellyComponents.updateDeviceStatus(this, status);
+ stats.wifiRssi = status.wifiSta.rssi;
if (api.isInitialized()) {
stats.timeoutErrors = api.getTimeoutErrors();
stats.remainingWatchdog = watchdog > 0 ? now() - watchdog : 0;
// Check various device indicators like overheating
- 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);
-
+ if (checkRestarted(status)) {
// Force re-initialization on next status update
- if (!profile.hasBattery || profile.isMotion) {
+ if (profile.alwaysOn) {
reinitializeThing();
}
} else if (getBool(status.overtemperature)) {
} else if (getBool(status.loaderror)) {
alarm = ALARM_TYPE_LOADERR;
}
+ State internalTemp = getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP);
+ if (internalTemp != UnDefType.NULL) {
+ int temp = ((Number) internalTemp).intValue();
+ if (temp > stats.maxInternalTemp) {
+ logger.debug("{}: Max Internal Temp for device changed to {}", thingName, temp);
+ stats.maxInternalTemp = temp;
+ }
+ }
+
stats.lastUptime = getLong(status.uptime);
stats.coiotMessages = coap.getMessageCount();
stats.coiotErrors = coap.getErrorCount();
}
}
+ /**
+ * Check if device has restarted and needs a new Thing initialization
+ *
+ * @return true: restart detected
+ */
+
+ private boolean checkRestarted(ShellySettingsStatus status) {
+ if (profile.isInitialized() && (status.uptime < stats.lastUptime || !profile.status.update.oldVersion.isEmpty()
+ && !status.update.oldVersion.equals(profile.status.update.oldVersion))) {
+ logger.debug("{}: Device restart #{} detected", thingName, stats.restarts);
+ stats.restarts++;
+ postEvent(ALARM_TYPE_RESTARTED, true);
+ updateProperties(profile, status);
+ return true;
+ }
+ return false;
+ }
+
/**
* Save alarm to the lastAlarm channel
*
logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate,
prf.fwId, SHELLY_API_MIN_FWVERSION));
} else {
- if (version.compare(prf.fwVersion, SHELLY_API_MIN_FWVERSION) < 0) {
+ if ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWVERSION) < 0) && !profile.isMotion) {
logger.warn("{}: {}", prf.hostname, messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate,
prf.fwId, SHELLY_API_MIN_FWVERSION));
}
* @return true=Update schedule, false=skipped (too many updates already
* scheduled)
*/
+ @Override
public boolean requestUpdates(int requestCount, boolean refreshSettings) {
this.refreshSettings |= refreshSettings;
if (refreshSettings) {
if (requestCount == 0) {
logger.debug("{}: Request settings refresh", thingName);
}
- scheduledUpdates = requestCount;
+ scheduledUpdates = 1;
return true;
}
if (scheduledUpdates < 10) { // < 30s
profile.isRoller ? CHANNEL_EVENT_TRIGGER : CHANNEL_BUTTON_TRIGGER + profile.getInputSuffix(idx),
trigger);
updateChannel(group, CHANNEL_LAST_UPDATE, getTimestamp());
- if (!profile.hasBattery) {
+ if (profile.alwaysOn) {
// refresh status of the input channel
requestUpdates(1, false);
}
return !stopping && cache.updateChannel(channelId, value, force);
}
+ @Override
public State getChannelValue(String group, String channel) {
return cache.getValue(group, channel);
}
* @param status the /status result
*/
protected void updateProperties(ShellyDeviceProfile profile, ShellySettingsStatus status) {
+ logger.debug("{}: Update properties", thingName);
Map<String, Object> properties = fillDeviceProperties(profile);
String serviceName = getString(getThing().getProperties().get(PROPERTY_SERVICE_NAME));
String hostname = getString(profile.settings.device.hostname).toLowerCase();
* @return ShellyDeviceProfile instance
* @throws ShellyApiException
*/
+ @Override
public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException {
try {
refreshSettings |= forceRefresh;
return profile;
}
+ @Override
public ShellyDeviceProfile getProfile() {
return profile;
}
return false;
}
+ @Override
+ public String getThingName() {
+ return thingName;
+ }
+
+ @Override
+ public void resetStats() {
+ // reset statistics
+ stats = new ShellyDeviceStats();
+ }
+
+ @Override
public ShellyDeviceStats getStats() {
return stats;
}
- public Map<String, String> getStatsProp() {
- return stats.asProperties(getString(profile.settings.timezone));
+ @Override
+ public ShellyHttpApi getApi() {
+ return api;
}
- public void resetStats() {
- // reset statistics
- stats = new ShellyDeviceStats();
+ public Map<String, String> getStatsProp() {
+ return stats.asProperties();
}
}
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(getDouble(status.temperature), DIGITS_NONE, SIUnits.CELSIUS));
}
+ thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SLEEPTIME,
+ toQuantityType(getInteger(status.sleepTime), Units.SECOND));
+
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate));
return false; // device status never triggers update
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
toQuantityType(getDouble(totalWatts), DIGITS_KWH, Units.KILOWATT_HOUR));
- if (updated) {
+ if (updated && timestamp > 0) {
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
getTimestamp(getString(profile.settings.timezone), timestamp));
}
boolean updated = false;
if (profile.isSensor || profile.hasBattery) {
ShellyStatusSensor sdata = thingHandler.api.getSensorStatus();
-
if (!thingHandler.areChannelsCreated()) {
thingHandler.logger.trace("{}: Create missing sensor channel(s)", thingHandler.thingName);
thingHandler.updateChannelDefinitions(
getOnOff(sdata.motion));
}
if (sdata.sensor != null) { // Shelly Motion
+ updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT,
+ getOnOff(sdata.sensor.motionActive));
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
getOnOff(sdata.sensor.motion));
long timestamp = getLong(sdata.sensor.motionTimestamp);
@NonNullByDefault
public class ShellyDeviceStats {
public long lastUptime = 0;
- public long unexpectedRestarts = 0;
+ public long restarts = 0;
public long timeoutErrors = 0;
public long timeoutsRecorvered = 0;
public long remainingWatchdog = 0;
public long lastAlarmTs = 0;
public long coiotMessages = 0;
public long coiotErrors = 0;
+ public int wifiRssi = 0;
+ public int maxInternalTemp = 0;
- public Map<String, String> asProperties(String timeZone) {
+ public Map<String, String> asProperties() {
Map<String, String> prop = new HashMap<>();
prop.put("lastUptime", String.valueOf(lastUptime));
- prop.put("unexpectedRestarts", String.valueOf(unexpectedRestarts));
+ prop.put("deviceRestarts", String.valueOf(restarts));
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("lastAlarmTs", ShellyUtils.convertTimestamp(lastAlarmTs));
prop.put("coiotMessages", String.valueOf(coiotMessages));
prop.put("coiotErrors", String.valueOf(coiotErrors));
+ prop.put("wifiRssi", String.valueOf(wifiRssi));
return prop;
}
}
--- /dev/null
+/**
+ * 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 org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.shelly.internal.api.ShellyApiException;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link ShellyManagerInterface} implements the interface for Shelly Manager to access the thing handler
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public interface ShellyManagerInterface {
+
+ public Thing getThing();
+
+ public String getThingName();
+
+ public ShellyDeviceProfile getProfile();
+
+ public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
+
+ public ShellyHttpApi getApi();
+
+ public ShellyDeviceStats getStats();
+
+ public void resetStats();
+
+ public State getChannelValue(String group, String channel);
+
+ public void setThingOnline();
+
+ public void setThingOffline(ThingStatusDetail detail, String messageKey);
+
+ public boolean requestUpdates(int requestCount, boolean refreshSettings);
+}
case CHANNEL_TIMER_AUTOON:
logger.debug("{}: Set Auto-ON timer to {}", thingName, command);
- api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command));
+ api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command).intValue());
break;
case CHANNEL_TIMER_AUTOOFF:
logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
- api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command));
+ api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command).intValue());
break;
}
return true;
*/
private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
throws ShellyApiException {
- Integer position = -1;
+ int position = -1;
if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
ShellyControlRoller rstatus = api.getRollerStatus(index);
}
}
- if (command == UpDownType.UP || command == OnOffType.ON) {
+ if (command == UpDownType.UP || command == OnOffType.ON
+ || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 100))) {
logger.debug("{}: Open roller", thingName);
- api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
- int pos = profile.getRollerFav(config.favoriteUP - 1);
- position = pos > 0 ? pos : SHELLY_MAX_ROLLER_POS;
- if (pos > 0) {
+ int shpos = profile.getRollerFav(config.favoriteUP - 1);
+ if (shpos > 0) {
logger.debug("{}: Use favoriteUP id {} for positioning roller({}%)", thingName, config.favoriteUP,
- pos);
+ shpos);
+ api.setRollerPos(index, shpos);
+ position = shpos;
+ } else {
+ api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
+ position = SHELLY_MIN_ROLLER_POS;
}
- } else if (command == UpDownType.DOWN || command == OnOffType.OFF) {
+ } else if (command == UpDownType.DOWN || command == OnOffType.OFF
+ || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 0))) {
logger.debug("{}: Closing roller", thingName);
- int pos = profile.getRollerFav(config.favoriteDOWN - 1);
- if (pos > 0) {
+ int shpos = profile.getRollerFav(config.favoriteDOWN - 1);
+ if (shpos > 0) {
// use favorite position
- if (pos > 0) {
- logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName,
- config.favoriteDOWN, pos);
- }
- api.setRollerPos(index, pos);
+ logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName,
+ config.favoriteDOWN, shpos);
+ api.setRollerPos(index, shpos);
+ position = shpos;
} else {
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
+ position = SHELLY_MAX_ROLLER_POS;
}
- position = SHELLY_MAX_ROLLER_POS - pos;
}
} else if (command == StopMoveType.STOP) {
logger.debug("{}: Stop roller", thingName);
"Invalid value type for roller control/position" + command.getClass().toString());
}
- // take position from RollerShutter control and map to Shelly positon (OH:
- // 0=closed, 100=open; Shelly 0=open, 100=closed)
+ // take position from RollerShutter control and map to Shelly positon
+ // OH: 0=closed, 100=open; Shelly 0=open, 100=closed)
// take position 1:1 from position channel
position = isControl ? SHELLY_MAX_ROLLER_POS - position : position;
validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS);
logger.debug("{}: Changing roller position to {}", thingName, position);
api.setRollerPos(index, position);
}
+
if (position != -1) {
// make sure both are in sync
if (isControl) {
int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS));
+ logger.debug("{}: Set roller position for control channel to {}", thingName, pos);
updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
} else {
+ logger.debug("{}: Set roller position channel to {}", thingName, position);
updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
}
}
String state = getString(control.state);
if (state.equals(SHELLY_ALWD_ROLLER_TURN_STOP)) { // only valid in stop state
int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(control.currentPos, SHELLY_MAX_ROLLER_POS));
+ logger.debug("{}: REST Update roller position: control={}, position={}", thingName,
+ SHELLY_MAX_ROLLER_POS - pos, pos);
updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
public ShellyChannelDefinitions(@Reference ShellyTranslationProvider translationProvider) {
ShellyTranslationProvider m = translationProvider;
- // Device: Internal Temp
+ // Device
CHANNEL_DEFINITIONS
// Device
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_NAME, "deviceName", ITEMT_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_TILT, "sensorTilt", ITEMT_ANGLE))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_MOTION, "sensorMotion", ITEMT_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_MOTION_TS, "motionTimestamp", ITEMT_DATETIME))
+ .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_MOTION_ACT, "motionActive", ITEMT_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_FLOOD, "sensorFlood", ITEMT_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_SMOKE, "sensorSmoke", ITEMT_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_PPM, "sensorPPM", ITEMT_NUMBER))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_VALVE, "sensorValve", ITEMT_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_ALARM_STATE, "alarmState", ITEMT_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_ERROR, "sensorError", ITEMT_STRING))
+ .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_SLEEPTIME, "sensorSleepTime", ITEMT_NUMBER))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_LAST_UPDATE, "lastUpdate", ITEMT_DATETIME))
// Button/ix3
addChannel(thing, add, (status.tmp != null) || (status.temperature != null), CHGR_DEVST,
CHANNEL_DEVST_ITEMP);
}
+ addChannel(thing, add, profile.settings.sleepTime != null, CHGR_SENSOR, CHANNEL_SENSOR_SLEEPTIME);
// If device has more than 1 meter the channel accumulatedWatts receives the accumulated value
boolean accuChannel = (((status.meters != null) && (status.meters.size() > 1) && !profile.isRoller
addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_UPDATE);
addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_UPTIME);
addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_HEARTBEAT);
-
- if (profile.settings.ledPowerDisable != null) {
- addChannel(thing, add, true, CHGR_DEVST, CHANNEL_LED_POWER_DISABLE);
- }
- if (profile.settings.ledStatusDisable != null) {
- addChannel(thing, add, true, CHGR_DEVST, CHANNEL_LED_STATUS_DISABLE); // WiFi status LED
- }
+ addChannel(thing, add, profile.settings.ledPowerDisable != null, CHGR_DEVST, CHANNEL_LED_POWER_DISABLE);
+ addChannel(thing, add, profile.settings.ledPowerDisable != null, CHGR_DEVST, CHANNEL_LED_STATUS_DISABLE); // WiFi
+ //
return add;
}
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.motionActive != null, CHANNEL_GROUP_SENSOR, // Motion
+ CHANNEL_SENSOR_MOTION_ACT);
addChannel(thing, newChannels, sdata.sensor.motionTimestamp != null, CHANNEL_GROUP_SENSOR, // Motion
CHANNEL_SENSOR_MOTION_TS);
addChannel(thing, newChannels, sdata.sensor.vibration != null, CHANNEL_GROUP_SENSOR,
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.util.ShellyUtils;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.osgi.framework.Bundle;
this.localeProvider = localeProvider;
}
- public @Nullable String get(String key, @Nullable Object... arguments) {
+ public String get(String key, @Nullable Object... arguments) {
return getText(key.contains("@text/") || key.contains(".shelly.") ? key : "message." + key, arguments);
}
- public @Nullable String getText(String key, @Nullable Object... arguments) {
+ public String getText(String key, @Nullable Object... arguments) {
try {
Locale locale = localeProvider.getLocale();
- return i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments);
+ String message = i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments);
+ return ShellyUtils.getString(message);
} catch (IllegalArgumentException e) {
return "Unable to load message for key " + key;
}
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
import javax.measure.Unit;
@NonNullByDefault
public class ShellyUtils {
private final static String PRE = "Unable to create object of type ";
+ public final static DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern(DateTimeType.DATE_PATTERN);
public static <T> T fromJson(Gson gson, @Nullable String json, Class<T> classOfT) throws ShellyApiException {
@Nullable
public static DateTimeType getTimestamp(String zone, long timestamp) {
try {
if (timestamp == 0) {
- return getTimestamp();
+ throw new IllegalArgumentException("Timestamp value 0 is invalid");
}
ZoneId zoneId = !zone.isEmpty() ? ZoneId.of(zone) : ZoneId.systemDefault();
ZonedDateTime zdt = LocalDateTime.now().atZone(zoneId);
}
}
+ public static String getTimestamp(DateTimeType dt) {
+ return dt.getZonedDateTime().toString().replace('T', ' ').replace('-', '/');
+ }
+
+ public static String convertTimestamp(long ts) {
+ if (ts == 0) {
+ return "";
+ }
+ String time = DATE_TIME.format(ZonedDateTime.ofInstant(Instant.ofEpochSecond(ts), ZoneId.systemDefault()));
+ return time.replace('T', ' ').replace('-', '/');
+ }
+
public static Integer getLightIdFromGroup(String groupName) {
if (groupName.startsWith(CHANNEL_GROUP_LIGHT_CHANNEL)) {
return Integer.parseInt(substringAfter(groupName, CHANNEL_GROUP_LIGHT_CHANNEL)) - 1;
message.statusupdate.failed = Unable to update status
message.event.triggered = Event triggered: {0}
message.coap.init.failed = Unable to start CoIoT: {0}
-message.discovery.disabled = Device is marked as non-discoverable -> skip
+message.discovery.disabled = Device is marked as non-discoverable, will be skipped
message.discovery.protected = Device {0} reported 'Access defined' (missing userid/password or incorrect).
message.discovery.failed = Device discovery of device with IP address {0} failed: {1}
message.roller.favmissing = Roller position favorites are not supported by installed firmware or not configured in the Shelly App
# Device
channel-type.shelly.deviceName.label = Device Name
channel-type.shelly.deviceName.description = Symbolic Device Name as configured in the Shelly App.
+channel-type.shelly.sensorSleepTime.label = Sensor Sleep Time
+channel-type.shelly.sensorSleepTime.description = The sensor will not send notifications and will not perform actions until the specified time expires. (0=disable)
# Relay, external sensors
channel-type.shelly.outputName.label = Output Name
channel-type.shelly.temperature3.description = Temperature of external Sensor #3
channel-type.shelly.humidity.label = Humidity
channel-type.shelly.humidity.description = Relative humidity (0..100%)
+channel-type.shelly.motionActive.label = Motion Active
+channel-type.shelly.motionActive.description = Indicates if motion sensor is active or within sleep time
channel-type.shelly.motionTimestamp.label = Last Motion
channel-type.shelly.motionTimestamp.description = Timestamp when last motion was detected.
# LED disable
channel-type.shelly.ledPowerDisable.label = Disable Power LED
-channel-type.shelly.ledPowerDisable.description = ON: The power status LED will be decativated
+channel-type.shelly.ledPowerDisable.description = ON: The power status LED will be deactivated
channel-type.shelly.ledStatusDisable.label = Disable Status LED
-channel-type.shelly.ledStatusDisable.description = ON: The WiFi status LED will be decativated
-
+channel-type.shelly.ledStatusDisable.description = ON: The WiFi status LED will be deactivated
channel-type.shelly.sensorVibration.description = ON: Sensor hat eine Vibration erkannt
channel-type.shelly.sensorMotion.label = Bewegung
channel-type.shelly.sensorMotion.description = ON: Es wurde eine Bewegung erkannt
+channel-type.shelly.motionActive.label = Bewegungssensor aktiv
+channel-type.shelly.motionActive.description = Zeigt an, ob die Bewegungserkennung aktiv oder pausiert ist
channel-type.shelly.motionTimestamp.label = Letzte Bewegung
channel-type.shelly.motionTimestamp.description = Datum/Uhrzeit, wann die letzte Bewegung erkannt wurde.
channel-type.shelly.sensorValve.label = Ventil
channel-type.shelly.selfTest.state.option.running = Test läuft
channel-type.shelly.selfTest.state.option.completed = abgeschlossen
channel-type.shelly.selfTest.state.option.unknown = unbekannt
+channel-type.shelly.sensorSleepTime.label = Sensor Standby Timer
+channel-type.shelly.sensorSleepTime.description = Das Gerät sendet kein Ereignis solange die Zeitspanne nicht abgelaufen ist.
<state readOnly="true">
</state>
</channel-type>
+ <channel-type id="motionActive" advanced="true">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.shelly.motionActive.label</label>
+ <description>channel-type.shelly.motionActive.description</description>
+ <state readOnly="true">
+ </state>
+ </channel-type>
<channel-type id="sensorMotion">
<item-type>Switch</item-type>
<label>Motion</label>
<state readOnly="true">
</state>
</channel-type>
+ <channel-type id="sensorSleepTime" advanced="true">
+ <item-type>Number:Time</item-type>
+ <label>@text/channel-type.shelly.sensorSleepTime.label</label>
+ <description>@text/channel-type.shelly.sensorSleepTime.description</description>
+ <state readOnly="false" min="0" max="86400" pattern="%.0f %unit%"/>
+ </channel-type>
<channel-type id="senseKey">
<item-type>String</item-type>