| | lowBattery | Switch | yes | Low battery alert (< 20%) |
| device | gatewayDevice | String | yes | Shelly forwarded last status update (BLU gateway), could vary from packet to packet |
-
-
### Shelly BLU Door/Window Sensor (thing-type: shellybludw)
See notes on discovery of Shelly BLU devices above.
| | lowBattery | Switch | yes | Low battery alert (< 20%) |
| device | gatewayDevice | String | yes | Shelly forwarded last status update (BLU gateway), could vary from packet to packet |
-## Shelly BLU Motion Sensor (thing-type: shellyblumotion)
+### Shelly BLU Motion Sensor (thing-type: shellyblumotion)
See notes on discovery of Shelly BLU devices above.
<artifactId>org.openhab.binding.shelly</artifactId>
<name>openHAB Add-ons :: Bundles :: Shelly Binding Gen1+2</name>
- <dependencies>
- <dependency>
- <groupId>org.eclipse.jetty.websocket</groupId>
- <artifactId>websocket-server</artifactId>
- <version>9.4.46.v20220331</version>
- <scope>compile</scope>
- </dependency>
- </dependencies>
-
</project>
public static final String PROPERTY_DEV_TYPE = "deviceType";
public static final String PROPERTY_DEV_MODE = "deviceMode";
public static final String PROPERTY_DEV_GEN = "deviceGeneration";
+ public static final String PROPERTY_DEV_AUTH = "deviceAuth";
public static final String PROPERTY_GW_DEVICE = "gatewayDevice";
public static final String PROPERTY_HWREV = "deviceHwRev";
public static final String PROPERTY_HWBATCH = "deviceHwBatch";
ShellyBaseHandler handler = null;
if (thingType.equals(THING_TYPE_SHELLYPROTECTED_STR)) {
- logger.debug("{}: Create new thing of type {} using ShellyProtectedHandler", thing.getLabel(),
+ logger.debug("{}: Create new thing of type {} using ShellyProtectedHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyProtectedHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
} else if (thingType.equals(THING_TYPE_SHELLYBULB_STR) || thingType.equals(THING_TYPE_SHELLYDUO_STR)
device.hostname = device.mac.length() >= 12 ? "shelly-" + device.mac.toUpperCase().substring(6, 11)
: "unknown";
}
+ device.mode = getString(settings.mode).toLowerCase();
name = getString(settings.name);
hwRev = settings.hwinfo != null ? getString(settings.hwinfo.hwRevision) : "";
hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
// If device is not yet intialized or the enabled property is missing we assume that CoIoT is enabled
return true;
}
+
+ public static String buildBluServiceName(String name, String mac) throws IllegalArgumentException {
+ String model = name.contains("-") ? substringBefore(name, "-") : name; // e.g. SBBT-02C or just SBDW
+ switch (model) {
+ case SHELLYDT_BLUBUTTON:
+ return (THING_TYPE_SHELLYBLUBUTTON_STR + "-" + mac).toLowerCase();
+ case SHELLYDT_BLUDW:
+ return (THING_TYPE_SHELLYBLUDW_STR + "-" + mac).toLowerCase();
+ case SHELLYDT_BLUMOTION:
+ return (THING_TYPE_SHELLYBLUMOTION_STR + "-" + mac).toLowerCase();
+ default:
+ throw new IllegalArgumentException("Unsupported BLU device model " + model);
+ }
+ }
}
}
return apiResult.response; // successful
} catch (ShellyApiException e) {
+ if (e.isHttpAccessUnauthorized() && !profile.isGen2 && !basicAuth && !config.password.isEmpty()) {
+ logger.debug("{}: Access is unauthorized, auto-activate basic auth", thingName);
+ basicAuth = true;
+ apiResult = innerRequest(HttpMethod.GET, uri, null, "");
+ }
+
if (e.isConnectionError()
|| (!e.isTimeout() && !apiResult.isHttpServerError()) && !apiResult.isNotFound()
|| profile.hasBattery || (retries == 0)) {
timeout = true;
timeoutErrors++; // count the retries
- logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
-
retries--;
+ if (profile.alwaysOn) {
+ logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
+ }
}
}
throw new ShellyApiException("API Timeout or inconsistent result"); // successful
import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
}
if (!coiotBound) {
thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
- logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
+ logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
if (iVersion == COIOT_VERSION_1) {
coiot = new Shelly1CoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
} else if (iVersion == COIOT_VERSION_2) {
}
}
+ // Don't change state to online when thing is in status config error
+ // (e.g. auth failed, but device sends COAP packets via multicast)
+ if (thingHandler.getThingStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR) {
+ logger.debug("{}: The device is not configuired correctly, skip Coap packet", thingName);
+ return;
+ }
+
// If we received a CoAP message successful the thing must be online
thingHandler.setThingOnline();
List<CoIotSensor> sensorUpdates = list.generic;
Map<String, State> updates = new TreeMap<String, State>();
- logger.debug("{}: {} CoAP sensor updates received", thingName, sensorUpdates.size());
+ logger.debug("{}: {} CoAP sensor updates received", thingName, sensorUpdates.size());
int failed = 0;
ShellyColorUtils col = new ShellyColorUtils();
for (int i = 0; i < sensorUpdates.size(); i++) {
rs.isValid = sm.isValid = emeter.isValid = true;
if (cs.state != null) {
if (!getString(rs.state).equals(cs.state)) {
- logger.debug("{}: Roller status changed from {} to {}, updateChannels={}", thingName, rs.state,
+ logger.debug("{}: Roller status changed from {} to {}, updateChannels={}", thingName, rs.state,
mapValue(MAP_ROLLER_STATE, cs.state), updateChannels);
}
rs.state = mapValue(MAP_ROLLER_STATE, cs.state);
@Override
public void initialize() throws ShellyApiException {
- if (!initialized) {
- rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp);
- rpcSocket.addMessageHandler(this);
- initialized = true;
- } else {
+ if (initialized) {
logger.debug("{}: Disconnect Rpc Socket on initialize", thingName);
disconnect();
}
+ rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp);
+ rpcSocket.addMessageHandler(this);
+ initialized = true;
}
@Override
}
private void disconnect() {
+ if (rpcSocket.isConnected()) {
+ logger.debug("{}: Disconnect Rpc Socket", thingName);
+ }
rpcSocket.disconnect();
}
@Override
public void close() {
- logger.debug("{}: Closing Rpc API (socket is {}, discovery={})", thingName,
- rpcSocket.isConnected() ? "connected" : "disconnected", discovery);
+ if (initialized || rpcSocket.isConnected()) {
+ logger.debug("{}: Closing Rpc API (socket is {}, discovery={})", thingName,
+ rpcSocket.isConnected() ? "connected" : "disconnected", discovery);
+ }
disconnect();
initialized = false;
}
if (s.isOpen()) {
logger.debug("{}: Disconnecting WebSocket ({} -> {})", thingName, s.getLocalAddress(),
s.getRemoteAddress());
- s.disconnect();
}
+ s.disconnect();
s.close(StatusCode.NORMAL, "Socket closed");
session = null;
}
- client.stop();
} catch (Exception e) {
if (e.getCause() instanceof InterruptedException) {
logger.debug("{}: Unable to close socket - interrupted", thingName); // e.g. device was rebooted
} else {
logger.debug("{}: Unable to close socket", thingName, e);
}
+ } finally {
+ // make sure client is stopped / thread terminates / socket resource is free up
+ try {
+ client.stop();
+ } catch (Exception e) {
+ logger.debug("{}: Unable to close Web Socket", thingName, e);
+ }
}
}
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
-import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.ArrayList;
public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
ShellySettingsDevice info = new ShellySettingsDevice();
info.hostname = !config.serviceName.isEmpty() ? config.serviceName : "";
- info.fw = "1234";
- info.type = "SBBT";
+ info.fw = "";
+ info.type = "BLU";
info.mac = config.deviceAddress;
info.auth = false;
- info.gen = 99;
+ info.gen = 2;
return info;
}
profile.gateway = getThing().getProperty(PROPERTY_GW_DEVICE);
}
- ShellySettingsDevice device = getDeviceInfo();
+ profile.device = getDeviceInfo();
if (config.serviceName.isEmpty()) {
config.serviceName = getString(profile.device.hostname);
}
- profile.fwDate = substringBefore(device.fw, "/");
- profile.fwVersion = substringBefore(ShellyDeviceProfile.extractFwVersion(device.fw.replace("/", "/v")), "-");
- profile.status.update.oldVersion = profile.fwVersion;
+
+ // for now we have no API to get this information
+ profile.fwDate = profile.fwVersion = profile.status.update.oldVersion = "";
profile.status.hasUpdate = profile.status.update.hasUpdate = false;
if (profile.hasBattery) {
}
logger.debug("{}: BLU Device discovered", thingName);
if (e.data.name != null) {
- profile.settings.name = buildBluServiceName(e.data.name, e.data.addr);
+ profile.settings.name = ShellyDeviceProfile.buildBluServiceName(e.data.name, e.data.addr);
}
break;
case SHELLY2_EVENT_BLUDATA:
if (updated) {
}
}
-
- public static String buildBluServiceName(String name, String mac) throws IllegalArgumentException {
- String model = name.contains("-") ? substringBefore(name, "-") : name; // e.g. SBBT-02C or just SBDW
- switch (model) {
- case SHELLYDT_BLUBUTTON:
- return (THING_TYPE_SHELLYBLUBUTTON_STR + "-" + mac).toLowerCase();
- case SHELLYDT_BLUDW:
- return (THING_TYPE_SHELLYBLUDW_STR + "-" + mac).toLowerCase();
- case SHELLYDT_BLUMOTION:
- return (THING_TYPE_SHELLYBLUMOTION_STR + "-" + mac).toLowerCase();
- default:
- throw new IllegalArgumentException("Unsupported BLU device model " + model);
- }
- }
}
boolean gen2 = "2".equals(service.getPropertyString("gen"));
ShellyApiInterface api = null;
+ boolean auth = false;
ShellySettingsDevice devInfo;
try {
api = gen2 ? new Shelly2ApiRpc(name, config, httpClient) : new Shelly1HttpApi(name, config, httpClient);
api.initialize();
devInfo = api.getDeviceInfo();
model = devInfo.type;
+ auth = devInfo.auth;
if (devInfo.name != null) {
deviceName = devInfo.name;
}
+
profile = api.getDeviceProfile(thingType, devInfo);
api.close();
logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
addProperty(properties, PROPERTY_DEV_TYPE, thingType);
addProperty(properties, PROPERTY_DEV_GEN, gen2 ? "2" : "1");
addProperty(properties, PROPERTY_DEV_MODE, mode);
+ addProperty(properties, PROPERTY_DEV_AUTH, auth ? "yes" : "no");
logger.debug("{}: Adding Shelly {}, UID={}", name, deviceName, thingUID.getAsString());
String thingLabel = deviceName.isEmpty() ? name + " - " + address
config.eventsSensorReport, config.eventsCoIoT, bindingConfig.autoCoIoT);
start = initializeThing();
} catch (ShellyApiException e) {
- ShellyApiResult res = e.getApiResult();
- String mid = "";
- if (e.isJsonError()) { // invalid JSON format
- mid = "offline.status-error-unexpected-error";
- start = false;
- } else if (isAuthorizationFailed(res)) {
- mid = "offline.conf-error-access-denied";
- start = false;
- } else if (profile.alwaysOn && e.isConnectionError()) {
- mid = "offline.status-error-connect";
- }
- if (!mid.isEmpty()) {
- setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, mid, e.toString());
- }
- logger.debug("{}: Unable to initialize: {}, retrying later", thingName, e.toString());
+ start = handleApiException(e);
} catch (IllegalArgumentException e) {
logger.debug("{}: Unable to initialize, retrying later", thingName, e);
} finally {
}, 2, TimeUnit.SECONDS);
}
+ private boolean handleApiException(ShellyApiException e) {
+ ShellyApiResult res = e.getApiResult();
+ ThingStatusDetail errorCode = ThingStatusDetail.COMMUNICATION_ERROR;
+ String status = "";
+ boolean retry = true;
+ if (e.isJsonError()) { // invalid JSON format
+ logger.debug("{}: Unable to parse API response: {}; json={}", thingName, res.getUrl(), res.response, e);
+ status = "offline.status-error-unexpected-error";
+ errorCode = ThingStatusDetail.CONFIGURATION_ERROR;
+ retry = false;
+ } else if (res.isHttpAccessUnauthorized()) {
+ status = "offline.conf-error-access-denied";
+ errorCode = ThingStatusDetail.CONFIGURATION_ERROR;
+ retry = false;
+ } else if (isWatchdogExpired()) {
+ status = "offline.status-error-watchdog";
+ } else if (res.httpCode >= 400) {
+ logger.debug("{}: Unexpected API result: {}/{}", thingName, res.httpCode, res.httpReason, e);
+ status = "offline.status-error-unexpected-api-result";
+ retry = false;
+ } else if (profile.alwaysOn && (e.isConnectionError() || res.isHttpTimeout())) {
+ status = "offline.status-error-connect";
+ }
+
+ if (!status.isEmpty()) {
+ setThingOffline(errorCode, status, e.toString());
+ } else {
+ logger.debug("{}: Unable to initialize: {}, retrying later", thingName, e.toString());
+ }
+
+ if (!retry) {
+ api.close();
+ }
+
+ return retry;
+ }
+
@Override
public ShellyThingConfiguration getThingConfig() {
return config;
requestUpdates(1, false);
}
} catch (ShellyApiException e) {
- ShellyApiResult res = e.getApiResult();
- if (isAuthorizationFailed(res)) {
+ if (!handleApiException(e)) {
return;
}
+
+ ShellyApiResult res = e.getApiResult();
if (res.isNotCalibrtated()) {
logger.warn("{}: {}", thingName, messages.get("roller.calibrating"));
} else {
} catch (ShellyApiException e) {
// http call failed: go offline except for battery devices, which might be in
// sleep mode. Once the next update is successful the device goes back online
- String status = "";
- ShellyApiResult res = e.getApiResult();
- if (profile.alwaysOn && e.isConnectionError()) {
- status = "offline.status-error-connect";
- } else if (res.isHttpAccessUnauthorized()) {
- status = "offline.conf-error-access-denied";
- } else if (isWatchdogStarted()) {
- if (!isWatchdogExpired()) {
- logger.debug("{}: Ignore API Timeout on {} {}, retry later", thingName, res.method, res.url);
- if (profile.alwaysOn) { // suppress for battery powered sensors
- logger.debug("{}: Ignore API Timeout on {} {}, retry later", thingName, res.method, res.url);
- }
- }
- } else if (e.isJSONException()) {
- status = "offline.status-error-unexpected-api-result";
- logger.debug("{}: Unable to parse API response: {}; json={}", thingName, res.getUrl(), res.response, e);
- } else if (res.isHttpTimeout()) {
- // Watchdog not started, e.g. device in sleep mode
- if (isThingOnline()) { // ignore when already offline
- status = "offline.status-error-watchdog";
- }
- } else {
- status = "offline.status-error-unexpected-api-result";
- logger.debug("{}: Unexpected API result: {}", thingName, res.response, e);
- }
-
- if (!status.isEmpty()) {
- setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, status);
- }
+ handleApiException(e);
} catch (NullPointerException | IllegalArgumentException e) {
logger.debug("{}: Unable to refresh status: {}", thingName, messages.get("statusupdate.failed"), e);
} finally {
}
if (prf.isRoller && prf.settings.favorites != null) {
String channelId = mkChannelId(CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_FAV);
- logger.debug("{}: Adding {} roler favorite(s) to channel description", thingName,
+ logger.debug("{}: Adding {} roler favorite(s) to channel description", thingName,
prf.settings.favorites.size());
channelDefinitions.clearStateOptions(channelId);
int fid = 1;
String minVersion = !gen2 ? SHELLY_API_MIN_FWVERSION : SHELLY2_API_MIN_FWVERSION;
if (version.compare(prf.fwVersion, minVersion) < 0) {
logger.warn("{}: {}", prf.device.hostname,
- messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate));
+ messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, minVersion));
}
}
if (!gen2 && bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0)
}
}
- /**
- * Checks the http response for authorization error.
- * If the authorization failed the binding can't access the device settings and determine the thing type. In this
- * case the thing type shelly-unknown is set.
- *
- * @param result exception details including the http respone
- * @return true if the authorization failed
- */
- protected boolean isAuthorizationFailed(ShellyApiResult result) {
- if (result.isHttpAccessUnauthorized()) {
- // If the device is password protected the API doesn't provide settings to the device settings
- setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-access-denied");
- return true;
- }
- return false;
- }
-
/**
* Change type of this thing.
*
properties.put(PROPERTY_SERVICE_NAME, config.serviceName);
String deviceName = getString(profile.settings.name);
properties.put(PROPERTY_SERVICE_NAME, config.serviceName);
- properties.put(PROPERTY_DEV_GEN, "1");
+ properties.put(PROPERTY_DEV_GEN, !profile.isGen2 ? "1" : "2");
+ properties.put(PROPERTY_DEV_AUTH, getBool(profile.device.auth) ? "yes" : "no");
if (!deviceName.isEmpty()) {
properties.put(PROPERTY_DEV_NAME, deviceName);
}
- properties.put(PROPERTY_DEV_GEN, !profile.isGen2 ? "1" : "2");
// add status properties
if (status.wifiSta != null) {
package org.openhab.binding.shelly.internal.handler;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api2.ShellyBluApi.buildBluServiceName;
import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import static org.openhab.core.thing.Thing.PROPERTY_MODEL_ID;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
public static void addBluThing(String gateway, Shelly2NotifyEvent e, ShellyThingTable thingTable) {
String model = substringBefore(getString(e.data.name), "-").toUpperCase();
- String mac = e.data.addr.replace(":", "");
+ String mac = e.data.addr.replaceAll(":", "");
String ttype = "";
logger.debug("{}: Create thing for new BLU device {}: {} / {}", gateway, e.data.name, model, mac);
ThingTypeUID tuid;
logger.debug("{}: Unsupported BLU device model {}, MAC={}", gateway, model, mac);
return;
}
- String serviceName = buildBluServiceName(model, mac);
+ String serviceName = ShellyDeviceProfile.buildBluServiceName(getString(e.data.name), mac);
Map<String, Object> properties = new TreeMap<>();
addProperty(properties, PROPERTY_MODEL_ID, model);
if (status.tmp != null && getBool(status.tmp.isValid) && !thingHandler.getProfile().isSensor
&& status.tmp.tC != SHELLY_API_INVTEMP) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
- toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
+ toQuantityType(getDouble(status.tmp.tC), DIGITS_TEMP, SIUnits.CELSIUS));
} else if (status.temperature != null && status.temperature != SHELLY_API_INVTEMP) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(getDouble(status.temperature), DIGITS_NONE, SIUnits.CELSIUS));
String action = getUrlParm(parameters, URLPARM_ACTION).toLowerCase();
String uidParm = getUrlParm(parameters, URLPARM_UID).toLowerCase();
- logger.debug("Generating overview for {} devices", getThingHandlers().size());
+ logger.debug("Generating overview for {} devices", getThingHandlers().size());
String html = "";
Map<String, String> properties = new HashMap<>();
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 = profile.numMeters > 1 && !profile.isRoller && !profile.isRGBW2;
+ boolean accuChannel = profile.hasRelays && profile.numMeters > 1 && !profile.isRoller && !profile.isRGBW2;
addChannel(thing, add, accuChannel, CHGR_DEVST, CHANNEL_DEVST_ACCUWATTS);
addChannel(thing, add, accuChannel, CHGR_DEVST, CHANNEL_DEVST_ACCUTOTAL);
addChannel(thing, add, accuChannel && (status.emeters != null), CHGR_DEVST, CHANNEL_DEVST_ACCURETURNED);
message.versioncheck.autocoiot = INFO: Firmware is full-filling the minimum version to auto-enable CoIoT
message.init.noipaddress = Unable to detect local IP address. Please make sure that IPv4 is enabled for this interface and check openHAB Network Configuration.
message.command.failed = ERROR: Unable to process command {0} for channel {1}
-message.command.init = Thing not yet initialized, command {0} triggered initialization
+message.command.init = Thing not yet initialized, command {0} triggered initialization
message.status.unknown.initializing = Initializing or device in sleep mode.
message.statusupdate.failed = Unable to update status
message.status.managerstarted = Shelly Manager started at http(s)://{0}:{1}/shelly/manager
thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter
# BLU devices
-thing-type.shelly.shellyblubutton.description = Shelly BLU Button
+thing-type.shelly.shellyblubutton.description = Shelly BLU Button 1
thing-type.shelly.shellybludw.description = Shelly BLU Door/Window Sensor
thing-type.shelly.shellyblumotion.description = Shelly BLU Motion Sensor
channel-type.shelly.temperature5.label = Temperature 5
channel-type.shelly.temperature6.description = Temperature of external Sensor #5
channel-type.shelly.targetTemp.label = Target Temperature
-channel-type.shelly.targetTemp.description = Target Temperature in °C to be reached in auto-temperature mode
+channel-type.shelly.targetTemp.description = Target Temperature in °C to be reached in auto-temperature mode
channel-type.shelly.humidity.label = Humidity
channel-type.shelly.humidity.description = Relative humidity (0..100%)
channel-type.shelly.rollerShutter.label = Roller Control (0=open, 100=closed)