[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 373 additions and 98 deletions

View File

@ -1,6 +1,7 @@
# Shelly Binding # Shelly Binding
This Binding integrates [Shelly devices](https://shelly.cloud) devloped by Allterco. This Binding integrates [Shelly devices](https://shelly.cloud) devloped by Allterco.
![](https://shop.shelly.cloud/image/cache/catalog/shelly_1/s1_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_dimmer2/shelly_dimmer2_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_vintage/shelly_vintage_A60-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_plug_s/s_plug_s_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_button1/shelly_button1_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_gas/shelly_gas_eu-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_ht/s_ht_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_1/s1_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_dimmer2/shelly_dimmer2_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_vintage/shelly_vintage_A60-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_plug_s/s_plug_s_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_button1/shelly_button1_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_gas/shelly_gas_eu-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_ht/s_ht_x1-80x80.jpg)
Allterco provides a rich set of smart home devices. All of them are WiFi enabled (2,4GHz, IPv4 only) and provide a documented API. Allterco provides a rich set of smart home devices. All of them are WiFi enabled (2,4GHz, IPv4 only) and provide a documented API.
@ -793,6 +794,10 @@ You can define 2 items (1 Switch, 1 Number) mapping to the same channel, see exa
### Shelly Motion (thing-type: shellymotion) ### 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 | |Group |Channel |Type |read-only|Description |
|----------|---------------|---------|---------|---------------------------------------------------------------------| |----------|---------------|---------|---------|---------------------------------------------------------------------|
|sensors |motion |Switch |yes |ON: Motion was detected | |sensors |motion |Switch |yes |ON: Motion was detected |
@ -801,10 +806,17 @@ You can define 2 items (1 Switch, 1 Number) mapping to the same channel, see exa
| |illumination |String |yes |Current illumination: dark/twilight/bright | | |illumination |String |yes |Current illumination: dark/twilight/bright |
| |vibration |Switch |yes |ON: Vibration detected | | |vibration |Switch |yes |ON: Vibration detected |
| |charger |Switch |yes |ON: USB charging cable is connected external power supply activated. | | |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) | | |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) |
|battery |batteryLevel |Number |yes |Battery Level in % | |battery |batteryLevel |Number |yes |Battery Level in % |
| |lowBattery |Switch |yes |Low battery alert (< 20%) | | |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) ### Shelly Button 1 (thing-type: shellybutton1)
|Group |Channel |Type |read-only|Description | |Group |Channel |Type |read-only|Description |

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -249,8 +249,10 @@ public class ShellyBindingConstants {
public static final String CHANNEL_SENSOR_VALVE = "valve"; public static final String CHANNEL_SENSOR_VALVE = "valve";
public static final String CHANNEL_SENSOR_SSTATE = "status"; // Shelly Gas 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_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 = "motion";
public static final String CHANNEL_SENSOR_MOTION_TS = "motionTimestamp"; 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"; public static final String CHANNEL_SENSOR_ERROR = "lastError";
// External sensors for Shelly1/1PM // 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 static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; 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.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler; import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyLightHandler; 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.ShellyProtectedHandler;
import org.openhab.binding.shelly.internal.handler.ShellyRelayHandler; import org.openhab.binding.shelly.internal.handler.ShellyRelayHandler;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider; import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
@ -141,8 +143,8 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
return null; return null;
} }
public Map<String, ShellyBaseHandler> getThingHandlers() { public Map<String, ShellyManagerInterface> getThingHandlers() {
return deviceListeners; return new HashMap<>(deviceListeners);
} }
/** /**

View File

@ -15,6 +15,7 @@ package org.openhab.binding.shelly.internal.api;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor.ShellyMotionSettings;
import org.openhab.core.thing.CommonTriggerEvents; import org.openhab.core.thing.CommonTriggerEvents;
import com.google.gson.annotations.SerializedName; 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_CELSIUS = "C";
public static final String SHELLY_TEMP_FAHRENHEIT = "F"; 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 static class ShellySettingsDevice {
public String type; public String type;
public String mac; public String mac;
@ -267,7 +274,7 @@ public class ShellyApiJsonDTO {
} }
public static class ShellySettingsMqtt { public static class ShellySettingsMqtt {
public Boolean enabled; public Boolean enable;
public String server; public String server;
public String user; public String user;
@SerializedName("reconnect_timeout_max") @SerializedName("reconnect_timeout_max")
@ -292,10 +299,17 @@ public class ShellyApiJsonDTO {
public static class ShellySettingsCoiot { // FW 1.6+ public static class ShellySettingsCoiot { // FW 1.6+
@SerializedName("update_period") @SerializedName("update_period")
public Integer updatePeriod; 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 static class ShellySettingsSntp {
public String server; public String server;
public Boolean enabled;
} }
public static class ShellySettingsLogin { public static class ShellySettingsLogin {
@ -319,10 +333,6 @@ public class ShellyApiJsonDTO {
public Boolean connected; public Boolean connected;
} }
public static class ShellyStatusMqtt {
public Boolean connected;
}
public static class ShellySettingsHwInfo { public static class ShellySettingsHwInfo {
@SerializedName("hw_revision") @SerializedName("hw_revision")
public String hwRevision; public String hwRevision;
@ -532,8 +542,11 @@ public class ShellyApiJsonDTO {
public ShellySettingsWiFiNetwork wifiSta; public ShellySettingsWiFiNetwork wifiSta;
@SerializedName("wifi_sta1") @SerializedName("wifi_sta1")
public ShellySettingsWiFiNetwork wifiSta1; public ShellySettingsWiFiNetwork wifiSta1;
// public ShellySettingsMqtt mqtt; // not used for now @SerializedName("wifirecovery_reboot_enabled")
// public ShellySettingsSntp sntp; // not used for now public Boolean wifiRecoveryReboot;
public ShellySettingsMqtt mqtt; // not used for now
public ShellySettingsSntp sntp; // not used for now
public ShellySettingsCoiot coiot; // Firmware 1.6+ public ShellySettingsCoiot coiot; // Firmware 1.6+
public ShellySettingsLogin login; public ShellySettingsLogin login;
@SerializedName("pin_code") @SerializedName("pin_code")
@ -544,8 +557,8 @@ public class ShellyApiJsonDTO {
public Boolean discoverable; // FW 1.6+ public Boolean discoverable; // FW 1.6+
public String fw; public String fw;
@SerializedName("build_info") @SerializedName("build_info")
ShellySettingsBuildInfo buildInfo; public ShellySettingsBuildInfo buildInfo;
ShellyStatusCloud cloud; public ShellyStatusCloud cloud;
@SerializedName("sleep_mode") @SerializedName("sleep_mode")
public ShellySensorSleepMode sleepMode; // FW 1.6 public ShellySensorSleepMode sleepMode; // FW 1.6
@SerializedName("external_power") @SerializedName("external_power")
@ -626,6 +639,18 @@ public class ShellyApiJsonDTO {
@SerializedName("favorites_enabled") @SerializedName("favorites_enabled")
public Boolean favoritesEnabled; public Boolean favoritesEnabled;
public ArrayList<ShellyFavPos> favorites; 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 static class ShellySettingsAttributes {
@ -644,11 +669,17 @@ public class ShellyApiJsonDTO {
public String fw; // current FW version public String fw; // current FW version
} }
public static class ShellyActionsStats {
public Integer skipped;
}
public static class ShellySettingsStatus { public static class ShellySettingsStatus {
public String name; // FW 1.8: Symbolic Device name is configurable public String name; // FW 1.8: Symbolic Device name is configurable
@SerializedName("wifi_sta") @SerializedName("wifi_sta")
public ShellySettingsWiFiNetwork wifiSta; // WiFi client configuration. See /settings/sta for details public ShellySettingsWiFiNetwork wifiSta; // WiFi client configuration. See /settings/sta for details
public ShellyStatusCloud cloud;
public ShellyStatusMqtt mqtt;
public String time; public String time;
public Integer serial; public Integer serial;
@ -658,6 +689,8 @@ public class ShellyApiJsonDTO {
public Boolean discoverable; // FW 1.6+ public Boolean discoverable; // FW 1.6+
@SerializedName("cfg_changed_cnt") @SerializedName("cfg_changed_cnt")
public Integer cfgChangedCount; // FW 1.8 public Integer cfgChangedCount; // FW 1.8
@SerializedName("actions_stats")
public ShellyActionsStats astats;
public ArrayList<ShellySettingsRelay> relays; public ArrayList<ShellySettingsRelay> relays;
public ArrayList<ShellySettingsRoller> rollers; public ArrayList<ShellySettingsRoller> rollers;
@ -690,6 +723,9 @@ public class ShellyApiJsonDTO {
public Long fsFree; public Long fsFree;
public Long uptime; public Long uptime;
@SerializedName("sleep_time") // Shelly Motion
public Integer sleepTime;
public String json; public String json;
} }
@ -908,6 +944,17 @@ public class ShellyApiJsonDTO {
public Integer vibration; // Whether vibration is detected 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 ShellyExtTemperature {
public static class ShellyShortTemp { public static class ShellyShortTemp {
public Double tC; // temperature in deg C public Double tC; // temperature in deg C
@ -953,8 +1000,6 @@ public class ShellyApiJsonDTO {
public Boolean motion; // Shelly Sense: true=motion detected public Boolean motion; // Shelly Sense: true=motion detected
public Boolean charger; // Shelly Sense: true=charger connected 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") @SerializedName("act_reasons")
public List<Object> actReasons; // HT/Smoke/Flood: list of reasons which woke up the device 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.HashMap;
import java.util.Map; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer; 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.ShellySettingsGlobal;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsInput; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsInput;
@ -41,6 +44,7 @@ import com.google.gson.Gson;
@NonNullByDefault @NonNullByDefault
public class ShellyDeviceProfile { public class ShellyDeviceProfile {
private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class); 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 boolean initialized = false; // true when initialized
@ -54,6 +58,8 @@ public class ShellyDeviceProfile {
public String hostname = ""; public String hostname = "";
public String mode = ""; public String mode = "";
public boolean discoverable = true; public boolean discoverable = true;
public boolean auth = false;
public boolean alwaysOn = true;
public String hwRev = ""; public String hwRev = "";
public String hwBatchId = ""; public String hwBatchId = "";
@ -115,11 +121,11 @@ public class ShellyDeviceProfile {
hostname = settings.device.hostname != null && !settings.device.hostname.isEmpty() hostname = settings.device.hostname != null && !settings.device.hostname.isEmpty()
? settings.device.hostname.toLowerCase() ? settings.device.hostname.toLowerCase()
: "shelly-" + mac.toUpperCase().substring(6, 11); : "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) : ""; hwRev = settings.hwinfo != null ? getString(settings.hwinfo.hwRevision) : "";
hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : ""; hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
fwDate = substringBefore(settings.fw, "/"); fwDate = substringBefore(settings.fw, "/");
fwVersion = substringBetween(settings.fw, "/", "@"); fwVersion = extractFwVersion(settings.fw);
fwId = substringAfter(settings.fw, "@"); fwId = substringAfter(settings.fw, "@");
discoverable = (settings.discoverable == null) || settings.discoverable; discoverable = (settings.discoverable == null) || settings.discoverable;
@ -129,8 +135,6 @@ public class ShellyDeviceProfile {
if ((numRelays > 0) && (settings.relays == null)) { if ((numRelays > 0) && (settings.relays == null)) {
numRelays = 0; numRelays = 0;
} }
isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2);
isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER);
hasRelays = (numRelays > 0) || isDimmer; hasRelays = (numRelays > 0) || isDimmer;
numRollers = getInteger(settings.device.numRollers); numRollers = getInteger(settings.device.numRollers);
numInputs = settings.inputs != null ? settings.inputs.size() : hasRelays ? isRoller ? 2 : 1 : 0; numInputs = settings.inputs != null ? settings.inputs.size() : hasRelays ? isRoller ? 2 : 1 : 0;
@ -177,6 +181,9 @@ public class ShellyDeviceProfile {
return; return;
} }
isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2);
isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER);
isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR); isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR);
isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR) isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR)
|| thingType.equals(THING_TYPE_SHELLYDUORGBW_STR); || thingType.equals(THING_TYPE_SHELLYDUORGBW_STR);
@ -198,14 +205,14 @@ public class ShellyDeviceProfile {
isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_STR); isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_STR);
isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR); isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR);
isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isMotion || 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 hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion;
// the charger
alwaysOn = !hasBattery || isMotion || isSense; // true means: device is reachable all the time (no sleep mode)
} }
public void updateFromStatus(ShellySettingsStatus status) { public void updateFromStatus(ShellySettingsStatus status) {
if (hasRelays) { if (hasRelays) {
// Dimmer-2 doesn't report inputs under /settings, only on /status, we need to update that info after // Dimmer-2 doesn't report inputs under /settings, only on /status, we need to update that info after init
// initialization
if (status.inputs != null) { if (status.inputs != null) {
numInputs = status.inputs.size(); numInputs = status.inputs.size();
} }
@ -317,4 +324,24 @@ public class ShellyDeviceProfile {
} }
return -1; 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)); .convert(getDouble(status.tmp.value));
status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value : f; 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 // SHelly H&T uses external_power, Sense uses charger
status.charger = status.externalPower != 0; status.charger = profile.settings.externalPower != 0;
} }
return status; 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; String type = SHELLY_CLASS_RELAY;
if (profile.isRoller) { if (profile.isRoller) {
type = SHELLY_CLASS_ROLLER; type = SHELLY_CLASS_ROLLER;
} else if (profile.isLight) { } else if (profile.isLight) {
type = SHELLY_CLASS_LIGHT; type = SHELLY_CLASS_LIGHT;
} }
String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value;
+ ((Integer) value.intValue()).toString();
request(uri); 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 { public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
request(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE)); request(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
} }
@ -239,6 +241,10 @@ public class ShellyHttpApi {
ShellySettingsLogin.class); 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 { public String deviceReboot() throws ShellyApiException {
return callApi(SHELLY_URL_RESTART, String.class); return callApi(SHELLY_URL_RESTART, String.class);
} }
@ -251,6 +257,10 @@ public class ShellyHttpApi {
return callApi("/ota?" + uri, ShellySettingsUpdate.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 * Change between White and Color Mode
* *

View File

@ -111,8 +111,12 @@ public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCo
break; break;
case "1103": // roller_0: S, rollerPos, 0-100, unknown -1 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)); 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, updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT)); 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; break;
case "1105": // S, valvle, closed/opened/not_connected/failure/closing/opening/checking or unbknown 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)); updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE, getStringType(s.valueStr));
@ -304,6 +308,8 @@ public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCo
break; break;
case "3120": // motionActive case "3120": // motionActive
// {"I":3120,"T":"S","D":"motionActive","R":["0/1","-1"],"L":1}, // {"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; break;
case "6108": // A, gas, none/mild/heavy/test or unknown 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.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api.ShellyHttpApi; import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
import org.openhab.binding.shelly.internal.coap.ShellyCoapHandler; 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.coap.ShellyCoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration; import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration; import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
@ -72,7 +73,7 @@ import org.slf4j.LoggerFactory;
* @author Markus Michels - Initial contribution * @author Markus Michels - Initial contribution
*/ */
@NonNullByDefault @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 Logger logger = LoggerFactory.getLogger(ShellyBaseHandler.class);
protected final ShellyChannelDefinitions channelDefinitions; 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 // 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 // fully initialized here. In this case the CoAP messages triggers auto-initialization (like the Action URL does
// when enabled) // when enabled)
if (config.eventsCoIoT && profile.hasBattery && !profile.isMotion && !profile.isSense) { if (config.eventsCoIoT && !profile.alwaysOn) {
coap.start(thingName, config); 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 // New Shelly devices might use a different endpoint for the CoAP listener
tmpPrf.coiotEndpoint = devInfo.coiot; tmpPrf.coiotEndpoint = devInfo.coiot;
} }
tmpPrf.auth = devInfo.auth; // missing in /settings
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {} ({})", logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {} ({})",
thingName, tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion, 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("{}: Shelly settings info for {}: {}", thingName, tmpPrf.hostname, tmpPrf.settingsJson);
logger.debug("{}: Device " logger.debug("{}: Device "
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})" + "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})"
+ ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}" + ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}"
+ ",updatePeriod:{}sec", thingName, tmpPrf.hasRelays, tmpPrf.numRelays, tmpPrf.isRoller, + ",alwaysOn:{}, ,updatePeriod:{}sec", thingName, tmpPrf.hasRelays, tmpPrf.numRelays, tmpPrf.isRoller,
tmpPrf.numRollers, tmpPrf.isDimmer, tmpPrf.numMeters, tmpPrf.isEMeter, tmpPrf.isSensor, tmpPrf.isDW, tmpPrf.numRollers, tmpPrf.isDimmer, tmpPrf.numMeters, tmpPrf.isEMeter, tmpPrf.isSensor, tmpPrf.isDW,
tmpPrf.hasBattery, tmpPrf.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "", tmpPrf.hasBattery, tmpPrf.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "",
tmpPrf.isSense, tmpPrf.isLight, profile.isBulb, tmpPrf.isDuo, tmpPrf.isRGBW2, tmpPrf.inColor, tmpPrf.isSense, tmpPrf.isMotion, tmpPrf.isLight, profile.isBulb, tmpPrf.isDuo, tmpPrf.isRGBW2,
tmpPrf.updatePeriod); tmpPrf.inColor, tmpPrf.alwaysOn, tmpPrf.updatePeriod);
// update thing properties // update thing properties
tmpPrf.status = api.getStatus(); tmpPrf.status = api.getStatus();
tmpPrf.updateFromStatus(tmpPrf.status); tmpPrf.updateFromStatus(tmpPrf.status);
updateProperties(tmpPrf, tmpPrf.status); updateProperties(tmpPrf, tmpPrf.status);
checkVersion(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) { if (autoCoIoT) {
logger.debug("{}: Auto-CoIoT is enabled, disabling action urls", thingName); logger.debug("{}: Auto-CoIoT is enabled, disabling action urls", thingName);
config.eventsCoIoT = true; config.eventsCoIoT = true;
@ -327,6 +345,14 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
api.setLedStatus(SHELLY_LED_POWER_DISABLE, command == OnOffType.ON); api.setLedStatus(SHELLY_LED_POWER_DISABLE, command == OnOffType.ON);
break; 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: default:
update = handleDeviceCommand(channelUID, command); update = handleDeviceCommand(channelUID, command);
break; break;
@ -368,10 +394,9 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
initializeThing(); // may fire an exception if initialization failed initializeThing(); // may fire an exception if initialization failed
} }
// Get profile, if refreshSettings == true reload settings from device // Get profile, if refreshSettings == true reload settings from device
profile = getProfile(refreshSettings); logger.trace("{}: Updating status (refreshSettings={})", thingName, refreshSettings);
logger.trace("{}: Updating status", thingName);
ShellySettingsStatus status = api.getStatus(); ShellySettingsStatus status = api.getStatus();
profile = getProfile(refreshSettings || checkRestarted(status));
profile.status = status; profile.status = status;
profile.updateFromStatus(status); profile.updateFromStatus(status);
@ -446,16 +471,18 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
return getThing().getStatus() == ThingStatus.OFFLINE; return getThing().getStatus() == ThingStatus.OFFLINE;
} }
@Override
public void setThingOnline() { public void setThingOnline() {
if (!isThingOnline()) { if (!isThingOnline()) {
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
// request 3 updates in a row (during the first 2+3*3 sec) // 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(); restartWatchdog();
} }
@Override
public void setThingOffline(ThingStatusDetail detail, String messageKey) { public void setThingOffline(ThingStatusDetail detail, String messageKey) {
if (!isThingOffline()) { if (!isThingOffline()) {
logger.info("{}: Thing goes OFFLINE: {}", thingName, messages.get(messageKey)); logger.info("{}: Thing goes OFFLINE: {}", thingName, messages.get(messageKey));
@ -485,8 +512,9 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
} }
public void reinitializeThing() { public void reinitializeThing() {
logger.debug("{}: Re-Initialize Thing", thingName);
updateStatus(ThingStatus.UNKNOWN); updateStatus(ThingStatus.UNKNOWN);
requestUpdates(1, true); requestUpdates(0, true);
} }
private void fillDeviceStatus(ShellySettingsStatus status, boolean updated) { private void fillDeviceStatus(ShellySettingsStatus status, boolean updated) {
@ -495,6 +523,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
// Update uptime and WiFi, internal temp // Update uptime and WiFi, internal temp
ShellyComponents.updateDeviceStatus(this, status); ShellyComponents.updateDeviceStatus(this, status);
stats.wifiRssi = status.wifiSta.rssi;
if (api.isInitialized()) { if (api.isInitialized()) {
stats.timeoutErrors = api.getTimeoutErrors(); stats.timeoutErrors = api.getTimeoutErrors();
@ -503,15 +532,9 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
stats.remainingWatchdog = watchdog > 0 ? now() - watchdog : 0; stats.remainingWatchdog = watchdog > 0 ? now() - watchdog : 0;
// Check various device indicators like overheating // Check various device indicators like overheating
logger.debug("{}: status.update={}, lastUpdate={}", thingName, status.uptime, stats.lastUptime); if (checkRestarted(status)) {
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 // Force re-initialization on next status update
if (!profile.hasBattery || profile.isMotion) { if (profile.alwaysOn) {
reinitializeThing(); reinitializeThing();
} }
} else if (getBool(status.overtemperature)) { } else if (getBool(status.overtemperature)) {
@ -521,6 +544,15 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
} else if (getBool(status.loaderror)) { } else if (getBool(status.loaderror)) {
alarm = ALARM_TYPE_LOADERR; 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.lastUptime = getLong(status.uptime);
stats.coiotMessages = coap.getMessageCount(); stats.coiotMessages = coap.getMessageCount();
stats.coiotErrors = coap.getErrorCount(); 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 * 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, logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate,
prf.fwId, SHELLY_API_MIN_FWVERSION)); prf.fwId, SHELLY_API_MIN_FWVERSION));
} else { } 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, logger.warn("{}: {}", prf.hostname, messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate,
prf.fwId, SHELLY_API_MIN_FWVERSION)); 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 * @return true=Update schedule, false=skipped (too many updates already
* scheduled) * scheduled)
*/ */
@Override
public boolean requestUpdates(int requestCount, boolean refreshSettings) { public boolean requestUpdates(int requestCount, boolean refreshSettings) {
this.refreshSettings |= refreshSettings; this.refreshSettings |= refreshSettings;
if (refreshSettings) { if (refreshSettings) {
if (requestCount == 0) { if (requestCount == 0) {
logger.debug("{}: Request settings refresh", thingName); logger.debug("{}: Request settings refresh", thingName);
} }
scheduledUpdates = requestCount; scheduledUpdates = 1;
return true; return true;
} }
if (scheduledUpdates < 10) { // < 30s 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), profile.isRoller ? CHANNEL_EVENT_TRIGGER : CHANNEL_BUTTON_TRIGGER + profile.getInputSuffix(idx),
trigger); trigger);
updateChannel(group, CHANNEL_LAST_UPDATE, getTimestamp()); updateChannel(group, CHANNEL_LAST_UPDATE, getTimestamp());
if (!profile.hasBattery) { if (profile.alwaysOn) {
// refresh status of the input channel // refresh status of the input channel
requestUpdates(1, false); requestUpdates(1, false);
} }
@ -941,6 +992,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
return !stopping && cache.updateChannel(channelId, value, force); return !stopping && cache.updateChannel(channelId, value, force);
} }
@Override
public State getChannelValue(String group, String channel) { public State getChannelValue(String group, String channel) {
return cache.getValue(group, channel); return cache.getValue(group, channel);
} }
@ -1005,6 +1057,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
* @param status the /status result * @param status the /status result
*/ */
protected void updateProperties(ShellyDeviceProfile profile, ShellySettingsStatus status) { protected void updateProperties(ShellyDeviceProfile profile, ShellySettingsStatus status) {
logger.debug("{}: Update properties", thingName);
Map<String, Object> properties = fillDeviceProperties(profile); Map<String, Object> properties = fillDeviceProperties(profile);
String serviceName = getString(getThing().getProperties().get(PROPERTY_SERVICE_NAME)); String serviceName = getString(getThing().getProperties().get(PROPERTY_SERVICE_NAME));
String hostname = getString(profile.settings.device.hostname).toLowerCase(); String hostname = getString(profile.settings.device.hostname).toLowerCase();
@ -1112,6 +1165,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
* @return ShellyDeviceProfile instance * @return ShellyDeviceProfile instance
* @throws ShellyApiException * @throws ShellyApiException
*/ */
@Override
public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException { public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException {
try { try {
refreshSettings |= forceRefresh; refreshSettings |= forceRefresh;
@ -1127,6 +1181,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
return profile; return profile;
} }
@Override
public ShellyDeviceProfile getProfile() { public ShellyDeviceProfile getProfile() {
return profile; return profile;
} }
@ -1185,16 +1240,28 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
return false; return false;
} }
public ShellyDeviceStats getStats() { @Override
return stats; public String getThingName() {
} return thingName;
public Map<String, String> getStatsProp() {
return stats.asProperties(getString(profile.settings.timezone));
} }
@Override
public void resetStats() { public void resetStats() {
// reset statistics // reset statistics
stats = new ShellyDeviceStats(); 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, thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(getDouble(status.temperature), DIGITS_NONE, SIUnits.CELSIUS)); 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)); thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate));
return false; // device status never triggers update return false; // device status never triggers update
@ -212,7 +215,7 @@ public class ShellyComponents {
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH, updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
toQuantityType(getDouble(totalWatts), DIGITS_KWH, Units.KILOWATT_HOUR)); toQuantityType(getDouble(totalWatts), DIGITS_KWH, Units.KILOWATT_HOUR));
if (updated) { if (updated && timestamp > 0) {
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
getTimestamp(getString(profile.settings.timezone), timestamp)); getTimestamp(getString(profile.settings.timezone), timestamp));
} }
@ -247,7 +250,6 @@ public class ShellyComponents {
boolean updated = false; boolean updated = false;
if (profile.isSensor || profile.hasBattery) { if (profile.isSensor || profile.hasBattery) {
ShellyStatusSensor sdata = thingHandler.api.getSensorStatus(); ShellyStatusSensor sdata = thingHandler.api.getSensorStatus();
if (!thingHandler.areChannelsCreated()) { if (!thingHandler.areChannelsCreated()) {
thingHandler.logger.trace("{}: Create missing sensor channel(s)", thingHandler.thingName); thingHandler.logger.trace("{}: Create missing sensor channel(s)", thingHandler.thingName);
thingHandler.updateChannelDefinitions( thingHandler.updateChannelDefinitions(
@ -355,6 +357,8 @@ public class ShellyComponents {
getOnOff(sdata.motion)); getOnOff(sdata.motion));
} }
if (sdata.sensor != null) { // Shelly 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, updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
getOnOff(sdata.sensor.motion)); getOnOff(sdata.sensor.motion));
long timestamp = getLong(sdata.sensor.motionTimestamp); long timestamp = getLong(sdata.sensor.motionTimestamp);

View File

@ -26,7 +26,7 @@ import org.openhab.binding.shelly.internal.util.ShellyUtils;
@NonNullByDefault @NonNullByDefault
public class ShellyDeviceStats { public class ShellyDeviceStats {
public long lastUptime = 0; public long lastUptime = 0;
public long unexpectedRestarts = 0; public long restarts = 0;
public long timeoutErrors = 0; public long timeoutErrors = 0;
public long timeoutsRecorvered = 0; public long timeoutsRecorvered = 0;
public long remainingWatchdog = 0; public long remainingWatchdog = 0;
@ -35,20 +35,22 @@ public class ShellyDeviceStats {
public long lastAlarmTs = 0; public long lastAlarmTs = 0;
public long coiotMessages = 0; public long coiotMessages = 0;
public long coiotErrors = 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<>(); Map<String, String> prop = new HashMap<>();
prop.put("lastUptime", String.valueOf(lastUptime)); 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("timeoutErrors", String.valueOf(timeoutErrors));
prop.put("timeoutsRecovered", String.valueOf(timeoutsRecorvered)); prop.put("timeoutsRecovered", String.valueOf(timeoutsRecorvered));
prop.put("remainingWatchdog", String.valueOf(remainingWatchdog)); prop.put("remainingWatchdog", String.valueOf(remainingWatchdog));
prop.put("alarmCount", String.valueOf(alarms)); prop.put("alarmCount", String.valueOf(alarms));
prop.put("lastAlarm", lastAlarm); prop.put("lastAlarm", lastAlarm);
prop.put("lastAlarmTs", prop.put("lastAlarmTs", ShellyUtils.convertTimestamp(lastAlarmTs));
lastAlarmTs != 0 ? ShellyUtils.getTimestamp(timeZone, lastAlarmTs).format(null).replace('T', ' ') : "");
prop.put("coiotMessages", String.valueOf(coiotMessages)); prop.put("coiotMessages", String.valueOf(coiotMessages));
prop.put("coiotErrors", String.valueOf(coiotErrors)); prop.put("coiotErrors", String.valueOf(coiotErrors));
prop.put("wifiRssi", String.valueOf(wifiRssi));
return prop; 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: case CHANNEL_TIMER_AUTOON:
logger.debug("{}: Set Auto-ON timer to {}", thingName, command); 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; break;
case CHANNEL_TIMER_AUTOOFF: case CHANNEL_TIMER_AUTOOFF:
logger.debug("{}: Set Auto-OFF timer to {}", thingName, command); 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; break;
} }
return true; return true;
@ -219,7 +219,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
*/ */
private void handleRoller(Command command, String groupName, Integer index, boolean isControl) private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
throws ShellyApiException { throws ShellyApiException {
Integer position = -1; int position = -1;
if ((command instanceof UpDownType) || (command instanceof OnOffType)) { if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
ShellyControlRoller rstatus = api.getRollerStatus(index); 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); logger.debug("{}: Open roller", thingName);
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN); int shpos = profile.getRollerFav(config.favoriteUP - 1);
int pos = profile.getRollerFav(config.favoriteUP - 1); if (shpos > 0) {
position = pos > 0 ? pos : SHELLY_MAX_ROLLER_POS;
if (pos > 0) {
logger.debug("{}: Use favoriteUP id {} for positioning roller({}%)", thingName, config.favoriteUP, 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); logger.debug("{}: Closing roller", thingName);
int pos = profile.getRollerFav(config.favoriteDOWN - 1); int shpos = profile.getRollerFav(config.favoriteDOWN - 1);
if (pos > 0) { if (shpos > 0) {
// use favorite position // use favorite position
if (pos > 0) { logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName,
logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName, config.favoriteDOWN, shpos);
config.favoriteDOWN, pos); api.setRollerPos(index, shpos);
} position = shpos;
api.setRollerPos(index, pos);
} else { } else {
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE); api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
position = SHELLY_MAX_ROLLER_POS;
} }
position = SHELLY_MAX_ROLLER_POS - pos;
} }
} else if (command == StopMoveType.STOP) { } else if (command == StopMoveType.STOP) {
logger.debug("{}: Stop roller", thingName); logger.debug("{}: Stop roller", thingName);
@ -275,8 +279,8 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
"Invalid value type for roller control/position" + command.getClass().toString()); "Invalid value type for roller control/position" + command.getClass().toString());
} }
// take position from RollerShutter control and map to Shelly positon (OH: // take position from RollerShutter control and map to Shelly positon
// 0=closed, 100=open; Shelly 0=open, 100=closed) // OH: 0=closed, 100=open; Shelly 0=open, 100=closed)
// take position 1:1 from position channel // take position 1:1 from position channel
position = isControl ? SHELLY_MAX_ROLLER_POS - position : position; position = isControl ? SHELLY_MAX_ROLLER_POS - position : position;
validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS); 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); logger.debug("{}: Changing roller position to {}", thingName, position);
api.setRollerPos(index, position); api.setRollerPos(index, position);
} }
if (position != -1) { if (position != -1) {
// make sure both are in sync // make sure both are in sync
if (isControl) { if (isControl) {
int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS)); 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)); updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
} else { } else {
logger.debug("{}: Set roller position channel to {}", thingName, position);
updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position)); updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
} }
} }
@ -399,6 +406,8 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
String state = getString(control.state); String state = getString(control.state);
if (state.equals(SHELLY_ALWD_ROLLER_TURN_STOP)) { // only valid in stop 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)); 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, updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT)); toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,

View File

@ -100,7 +100,7 @@ public class ShellyChannelDefinitions {
public ShellyChannelDefinitions(@Reference ShellyTranslationProvider translationProvider) { public ShellyChannelDefinitions(@Reference ShellyTranslationProvider translationProvider) {
ShellyTranslationProvider m = translationProvider; ShellyTranslationProvider m = translationProvider;
// Device: Internal Temp // Device
CHANNEL_DEFINITIONS CHANNEL_DEFINITIONS
// Device // Device
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_NAME, "deviceName", ITEMT_STRING)) .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_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, "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_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_FLOOD, "sensorFlood", ITEMT_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_SMOKE, "sensorSmoke", 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_PPM, "sensorPPM", ITEMT_NUMBER))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_VALVE, "sensorValve", ITEMT_STRING)) .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_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_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)) .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_LAST_UPDATE, "lastUpdate", ITEMT_DATETIME))
// Button/ix3 // Button/ix3
@ -249,6 +251,7 @@ public class ShellyChannelDefinitions {
addChannel(thing, add, (status.tmp != null) || (status.temperature != null), CHGR_DEVST, addChannel(thing, add, (status.tmp != null) || (status.temperature != null), CHGR_DEVST,
CHANNEL_DEVST_ITEMP); 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 // 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 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_UPDATE);
addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_UPTIME); addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_UPTIME);
addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_HEARTBEAT); addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_HEARTBEAT);
addChannel(thing, add, profile.settings.ledPowerDisable != null, CHGR_DEVST, CHANNEL_LED_POWER_DISABLE);
if (profile.settings.ledPowerDisable != null) { addChannel(thing, add, profile.settings.ledPowerDisable != null, CHGR_DEVST, CHANNEL_LED_STATUS_DISABLE); // WiFi
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
}
return add; return add;
} }
@ -417,6 +416,8 @@ public class ShellyChannelDefinitions {
CHANNEL_SENSOR_MOTION); CHANNEL_SENSOR_MOTION);
if (sdata.sensor != null) { // DW, Sense or 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.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 addChannel(thing, newChannels, sdata.sensor.motionTimestamp != null, CHANNEL_GROUP_SENSOR, // Motion
CHANNEL_SENSOR_MOTION_TS); CHANNEL_SENSOR_MOTION_TS);
addChannel(thing, newChannels, sdata.sensor.vibration != null, CHANNEL_GROUP_SENSOR, 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.i18n.TranslationProvider;
import org.osgi.framework.Bundle; import org.osgi.framework.Bundle;
@ -44,14 +45,15 @@ public class ShellyTranslationProvider {
this.localeProvider = localeProvider; 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); 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 { try {
Locale locale = localeProvider.getLocale(); 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) { } catch (IllegalArgumentException e) {
return "Unable to load message for key " + key; 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.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import javax.measure.Unit; import javax.measure.Unit;
@ -53,6 +54,7 @@ import com.google.gson.internal.Primitives;
@NonNullByDefault @NonNullByDefault
public class ShellyUtils { public class ShellyUtils {
private final static String PRE = "Unable to create object of type "; 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 { public static <T> T fromJson(Gson gson, @Nullable String json, Class<T> classOfT) throws ShellyApiException {
@Nullable @Nullable
@ -256,7 +258,7 @@ public class ShellyUtils {
public static DateTimeType getTimestamp(String zone, long timestamp) { public static DateTimeType getTimestamp(String zone, long timestamp) {
try { try {
if (timestamp == 0) { if (timestamp == 0) {
return getTimestamp(); throw new IllegalArgumentException("Timestamp value 0 is invalid");
} }
ZoneId zoneId = !zone.isEmpty() ? ZoneId.of(zone) : ZoneId.systemDefault(); ZoneId zoneId = !zone.isEmpty() ? ZoneId.of(zone) : ZoneId.systemDefault();
ZonedDateTime zdt = LocalDateTime.now().atZone(zoneId); 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) { public static Integer getLightIdFromGroup(String groupName) {
if (groupName.startsWith(CHANNEL_GROUP_LIGHT_CHANNEL)) { if (groupName.startsWith(CHANNEL_GROUP_LIGHT_CHANNEL)) {
return Integer.parseInt(substringAfter(groupName, CHANNEL_GROUP_LIGHT_CHANNEL)) - 1; 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.statusupdate.failed = Unable to update status
message.event.triggered = Event triggered: {0} message.event.triggered = Event triggered: {0}
message.coap.init.failed = Unable to start CoIoT: {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.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.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 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 # Device
channel-type.shelly.deviceName.label = Device Name channel-type.shelly.deviceName.label = Device Name
channel-type.shelly.deviceName.description = Symbolic Device Name as configured in the Shelly App. 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 # Relay, external sensors
channel-type.shelly.outputName.label = Output Name 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.temperature3.description = Temperature of external Sensor #3
channel-type.shelly.humidity.label = Humidity channel-type.shelly.humidity.label = Humidity
channel-type.shelly.humidity.description = Relative humidity (0..100%) 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.label = Last Motion
channel-type.shelly.motionTimestamp.description = Timestamp when last motion was detected. 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 # LED disable
channel-type.shelly.ledPowerDisable.label = Disable Power LED 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.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.sensorVibration.description = ON: Sensor hat eine Vibration erkannt
channel-type.shelly.sensorMotion.label = Bewegung channel-type.shelly.sensorMotion.label = Bewegung
channel-type.shelly.sensorMotion.description = ON: Es wurde eine Bewegung erkannt 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.label = Letzte Bewegung
channel-type.shelly.motionTimestamp.description = Datum/Uhrzeit, wann die letzte Bewegung erkannt wurde. channel-type.shelly.motionTimestamp.description = Datum/Uhrzeit, wann die letzte Bewegung erkannt wurde.
channel-type.shelly.sensorValve.label = Ventil 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.running = Test läuft
channel-type.shelly.selfTest.state.option.completed = abgeschlossen channel-type.shelly.selfTest.state.option.completed = abgeschlossen
channel-type.shelly.selfTest.state.option.unknown = unbekannt 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 readOnly="true">
</state> </state>
</channel-type> </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"> <channel-type id="sensorMotion">
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>Motion</label> <label>Motion</label>
@ -352,6 +359,12 @@
<state readOnly="true"> <state readOnly="true">
</state> </state>
</channel-type> </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"> <channel-type id="senseKey">
<item-type>String</item-type> <item-type>String</item-type>