[shelly] Improved Motion Support, Support CoIoT Unicast, fixes (#10220)

* New feature: Shelly Manager

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

* Removed Shelly Manager to reduce PR size (will be another PR)

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

* CoIoT initialization handles new COIOT options for the device,
sensorSleepTime is now adadvanced; Roller set position 0/100 is mapped
to UP/DOWN; Reference to Shelly Manager removed from README

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

* Nullpointer check added on settings.coiot (4Pro has this null)

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

* README updated

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

* Use regex to extract fw version from string, check fw version to detect
restarted, README updated, moved channel sensorSleepTime from group
device to sensors

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

* Review changes

Signed-off-by: Markus Michels <markus7017@gmail.com>
This commit is contained in:
Markus Michels
2021-03-03 17:43:02 +01:00
committed by GitHub
parent 51ddbdb84d
commit 3af0392724
24 changed files with 373 additions and 98 deletions

View File

@@ -249,8 +249,10 @@ public class ShellyBindingConstants {
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

View File

@@ -14,6 +14,7 @@ package org.openhab.binding.shelly.internal;
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;
@@ -25,6 +26,7 @@ import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
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;
@@ -141,8 +143,8 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
return null;
}
public Map<String, ShellyBaseHandler> getThingHandlers() {
return deviceListeners;
public Map<String, ShellyManagerInterface> getThingHandlers() {
return new HashMap<>(deviceListeners);
}
/**

View File

@@ -15,6 +15,7 @@ package org.openhab.binding.shelly.internal.api;
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;
@@ -226,6 +227,12 @@ public class ShellyApiJsonDTO {
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;
@@ -267,7 +274,7 @@ public class ShellyApiJsonDTO {
}
public static class ShellySettingsMqtt {
public Boolean enabled;
public Boolean enable;
public String server;
public String user;
@SerializedName("reconnect_timeout_max")
@@ -292,10 +299,17 @@ public class ShellyApiJsonDTO {
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 {
@@ -319,10 +333,6 @@ public class ShellyApiJsonDTO {
public Boolean connected;
}
public static class ShellyStatusMqtt {
public Boolean connected;
}
public static class ShellySettingsHwInfo {
@SerializedName("hw_revision")
public String hwRevision;
@@ -532,8 +542,11 @@ public class ShellyApiJsonDTO {
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")
@@ -544,8 +557,8 @@ public class ShellyApiJsonDTO {
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")
@@ -626,6 +639,18 @@ public class ShellyApiJsonDTO {
@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 {
@@ -644,11 +669,17 @@ public class ShellyApiJsonDTO {
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;
@@ -658,6 +689,8 @@ public class ShellyApiJsonDTO {
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;
@@ -690,6 +723,9 @@ public class ShellyApiJsonDTO {
public Long fsFree;
public Long uptime;
@SerializedName("sleep_time") // Shelly Motion
public Integer sleepTime;
public String json;
}
@@ -908,6 +944,17 @@ public class ShellyApiJsonDTO {
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
@@ -953,8 +1000,6 @@ public class ShellyApiJsonDTO {
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

View File

@@ -18,8 +18,11 @@ import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
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;
@@ -41,6 +44,7 @@ import com.google.gson.Gson;
@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
@@ -54,6 +58,8 @@ public class ShellyDeviceProfile {
public String hostname = "";
public String mode = "";
public boolean discoverable = true;
public boolean auth = false;
public boolean alwaysOn = true;
public String hwRev = "";
public String hwBatchId = "";
@@ -115,11 +121,11 @@ public class ShellyDeviceProfile {
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;
@@ -129,8 +135,6 @@ public class ShellyDeviceProfile {
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;
@@ -177,6 +181,9 @@ public class ShellyDeviceProfile {
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);
@@ -198,14 +205,14 @@ public class ShellyDeviceProfile {
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();
}
@@ -317,4 +324,24 @@ public class ShellyDeviceProfile {
}
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;
}
}

View File

@@ -194,26 +194,28 @@ public class ShellyHttpApi {
.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));
}
@@ -239,6 +241,10 @@ public class ShellyHttpApi {
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);
}
@@ -251,6 +257,10 @@ public class ShellyHttpApi {
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
*

View File

@@ -111,8 +111,12 @@ public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCo
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));
@@ -304,6 +308,8 @@ public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCo
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

View File

@@ -38,6 +38,7 @@ import org.openhab.binding.shelly.internal.api.ShellyApiResult;
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;
@@ -72,7 +73,7 @@ import org.slf4j.LoggerFactory;
* @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;
@@ -216,7 +217,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.isMotion && !profile.isSense) {
if (config.eventsCoIoT && !profile.alwaysOn) {
coap.start(thingName, config);
}
@@ -242,6 +243,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
// 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,
@@ -249,18 +251,34 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
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;
@@ -327,6 +345,14 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
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;
@@ -368,10 +394,9 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
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);
@@ -446,16 +471,18 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
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));
@@ -485,8 +512,9 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
}
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) {
@@ -495,6 +523,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
// Update uptime and WiFi, internal temp
ShellyComponents.updateDeviceStatus(this, status);
stats.wifiRssi = status.wifiSta.rssi;
if (api.isInitialized()) {
stats.timeoutErrors = api.getTimeoutErrors();
@@ -503,15 +532,9 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
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)) {
@@ -521,6 +544,15 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
} 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();
@@ -530,6 +562,24 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
}
}
/**
* 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
*
@@ -752,7 +802,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
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));
}
@@ -838,13 +888,14 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
* @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
@@ -920,7 +971,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
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);
}
@@ -941,6 +992,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
return !stopping && cache.updateChannel(channelId, value, force);
}
@Override
public State getChannelValue(String group, String channel) {
return cache.getValue(group, channel);
}
@@ -1005,6 +1057,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
* @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();
@@ -1112,6 +1165,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
* @return ShellyDeviceProfile instance
* @throws ShellyApiException
*/
@Override
public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException {
try {
refreshSettings |= forceRefresh;
@@ -1127,6 +1181,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
return profile;
}
@Override
public ShellyDeviceProfile getProfile() {
return profile;
}
@@ -1185,16 +1240,28 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
return false;
}
public ShellyDeviceStats getStats() {
return stats;
}
public Map<String, String> getStatsProp() {
return stats.asProperties(getString(profile.settings.timezone));
@Override
public String getThingName() {
return thingName;
}
@Override
public void resetStats() {
// reset statistics
stats = new ShellyDeviceStats();
}
@Override
public ShellyDeviceStats getStats() {
return stats;
}
@Override
public ShellyHttpApi getApi() {
return api;
}
public Map<String, String> getStatsProp() {
return stats.asProperties();
}
}

View File

@@ -64,6 +64,9 @@ public class ShellyComponents {
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
@@ -212,7 +215,7 @@ public class ShellyComponents {
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));
}
@@ -247,7 +250,6 @@ public class ShellyComponents {
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(
@@ -355,6 +357,8 @@ public class ShellyComponents {
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);

View File

@@ -26,7 +26,7 @@ import org.openhab.binding.shelly.internal.util.ShellyUtils;
@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;
@@ -35,20 +35,22 @@ public class ShellyDeviceStats {
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;
}
}

View File

@@ -0,0 +1,52 @@
/**
* 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);
}

View File

@@ -134,11 +134,11 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
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;
@@ -219,7 +219,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
*/
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);
@@ -235,29 +235,33 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
}
}
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);
@@ -275,8 +279,8 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
"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);
@@ -284,12 +288,15 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
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));
}
}
@@ -399,6 +406,8 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
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,

View File

@@ -100,7 +100,7 @@ public class ShellyChannelDefinitions {
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))
@@ -179,12 +179,14 @@ public class ShellyChannelDefinitions {
.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
@@ -249,6 +251,7 @@ public class ShellyChannelDefinitions {
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
@@ -259,13 +262,9 @@ public class ShellyChannelDefinitions {
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;
}
@@ -417,6 +416,8 @@ public class ShellyChannelDefinitions {
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,

View File

@@ -16,6 +16,7 @@ import java.util.Locale;
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;
@@ -44,14 +45,15 @@ public class ShellyTranslationProvider {
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;
}

View File

@@ -24,6 +24,7 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import javax.measure.Unit;
@@ -53,6 +54,7 @@ import com.google.gson.internal.Primitives;
@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
@@ -256,7 +258,7 @@ public class ShellyUtils {
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);
@@ -268,6 +270,18 @@ public class ShellyUtils {
}
}
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;

View File

@@ -34,7 +34,7 @@ message.status.unknown.initializing = Initializing or device in sleep mode.
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
@@ -42,6 +42,8 @@ message.roller.favmissing = Roller position favorites are not supported by insta
# 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
@@ -54,6 +56,8 @@ channel-type.shelly.temperature3.label = Temperature 3
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.
@@ -64,7 +68,6 @@ channel-type.shelly.rollerState.description = State of the roller (open/closed/s
# 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

View File

@@ -600,6 +600,8 @@ channel-type.shelly.sensorVibration.label = Vibration
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
@@ -689,5 +691,7 @@ channel-type.shelly.selfTest.state.option.not_completed = Nicht abgeschlossen
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.

View File

@@ -293,6 +293,13 @@
<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>
@@ -352,6 +359,12 @@
<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>