[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
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)
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)
Important: The Shelly Motion does only support CoIoT Unicast, which means you need to set the CoIoT peer address.
Use device WebUI, open COIOT settings, make sure CoIoT is enabled and enter the openHAB IP address or
|Group |Channel |Type |read-only|Description |
|----------|---------------|---------|---------|---------------------------------------------------------------------|
|sensors |motion |Switch |yes |ON: Motion was detected |
@ -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 |
| |vibration |Switch |yes |ON: Vibration detected |
| |charger |Switch |yes |ON: USB charging cable is connected external power supply activated. |
| |motionActive |Switch |yes |ON: Motion detection is currently active |
| |sensorSleepTime|Number |no |Specifies the number of sec the sensor should not report events ]
| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) |
|battery |batteryLevel |Number |yes |Battery Level in % |
| |lowBattery |Switch |yes |Low battery alert (< 20%) |
Use case for the 'sensorSleepTime':
You have a Motion controlling your light.
You switch off the light and want to leave the room, but the motion sensor immediately switches light back on.
Using 'sensorSleepTime' you could suppress motion events while leaving the room, e.g. for 5sec and the light doesn's switch on.
### Shelly Button 1 (thing-type: shellybutton1)
|Group |Channel |Type |read-only|Description |

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_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>