added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.shelly-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-shelly" description="Shelly Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-coap</feature>
<feature>openhab-transport-mdns</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.shelly/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,338 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link ShellyBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyBindingConstants {
public static final String VENDOR = "Shelly";
public static final String BINDING_ID = "shelly";
public static final String SYSTEM_ID = "system";
// Type names
public static final String THING_TYPE_SHELLY1_STR = "shelly1";
public static final String THING_TYPE_SHELLY1PM_STR = "shelly1pm";
public static final String THING_TYPE_SHELLYEM_STR = "shellyem";
public static final String THING_TYPE_SHELLY3EM_STR = "shellyem3"; // bad: misspelled product name, it's 3EM
public static final String THING_TYPE_SHELLY2_PREFIX = "shellyswitch";
public static final String THING_TYPE_SHELLY2_RELAY_STR = "shelly2-relay";
public static final String THING_TYPE_SHELLY2_ROLLER_STR = "shelly2-roller";
public static final String THING_TYPE_SHELLY25_PREFIX = "shellyswitch25";
public static final String THING_TYPE_SHELLY25_RELAY_STR = "shelly25-relay";
public static final String THING_TYPE_SHELLY25_ROLLER_STR = "shelly25-roller";
public static final String THING_TYPE_SHELLY4PRO_STR = "shelly4pro";
public static final String THING_TYPE_SHELLYPLUG_STR = "shellyplug";
public static final String THING_TYPE_SHELLYPLUGS_STR = "shellyplugs";
public static final String THING_TYPE_SHELLYDIMMER_STR = "shellydimmer";
public static final String THING_TYPE_SHELLYDIMMER2_STR = "shellydimmer2";
public static final String THING_TYPE_SHELLYIX3_STR = "shellyix3";
public static final String THING_TYPE_SHELLYBULB_STR = "shellybulb";
public static final String THING_TYPE_SHELLYDUO_STR = "shellybulbduo";
public static final String THING_TYPE_SHELLYVINTAGE_STR = "shellyvintage";
public static final String THING_TYPE_SHELLYRGBW2_PREFIX = "shellyrgbw2";
public static final String THING_TYPE_SHELLYRGBW2_COLOR_STR = "shellyrgbw2-color";
public static final String THING_TYPE_SHELLYRGBW2_WHITE_STR = "shellyrgbw2-white";
public static final String THING_TYPE_SHELLYHT_STR = "shellyht";
public static final String THING_TYPE_SHELLYSMOKE_STR = "shellysmoke";
public static final String THING_TYPE_SHELLYGAS_STR = "shellygas";
public static final String THING_TYPE_SHELLYFLOOD_STR = "shellyflood";
public static final String THING_TYPE_SHELLYDOORWIN_STR = "shellydw";
public static final String THING_TYPE_SHELLYDOORWIN2_STR = "shellydw2";
public static final String THING_TYPE_SHELLYEYE_STR = "shellyseye";
public static final String THING_TYPE_SHELLYSENSE_STR = "shellysense";
public static final String THING_TYPE_SHELLYBUTTON1_STR = "shellybutton1";
public static final String THING_TYPE_SHELLYPROTECTED_STR = "shellydevice";
public static final String THING_TYPE_SHELLYUNKNOWN_STR = "shellyunknown";
// Device Types
public static final String SHELLYDT_1 = "SHSW-1";
public static final String SHELLYDT_1PM = "SHSW-PM";
public static final String SHELLYDT_SHPLG = "SHPLG-1";
public static final String SHELLYDT_SHPLG_S = "SHPLG-S";
public static final String SHELLYDT_SHELLY2 = "SHSW-21";
public static final String SHELLYDT_SHELLY25 = "SHSW-25";
public static final String SHELLYDT_SHPRO = "SHSW-44";
public static final String SHELLYDT_EM = "SHEM";
public static final String SHELLYDT_3EM = "SHEM-3";
public static final String SHELLYDT_HT = "SHHT-1";
public static final String SHELLYDT_DW = "SHDW-1";
public static final String SHELLYDT_DW2 = "SHDW-2";
public static final String SHELLYDT_SENSE = "SHSEN-1";
public static final String SHELLYDT_GAS = "SHGS-1";
public static final String SHELLYDT_DIMMER = "SHDM-1";
public static final String SHELLYDT_DIMMER2 = "SHDM-2";
public static final String SHELLYDT_IX3 = "SHIX3-1";
public static final String SHELLYDT_BULB = "SHBLB-1";
public static final String SHELLYDT_DUO = "SHBDUO-1";
public static final String SHELLYDT_VINTAGE = "SHVIN-1";
public static final String SHELLYDT_RGBW2 = "SHRGBW2";
public static final String SHELLYDT_BUTTON1 = "SHBTN-1";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_SHELLY1 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1_STR);
public static final ThingTypeUID THING_TYPE_SHELLY1PM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1PM_STR);
public static final ThingTypeUID THING_TYPE_SHELLYEM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEM_STR);
public static final ThingTypeUID THING_TYPE_SHELLY3EM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY3EM_STR);
public static final ThingTypeUID THING_TYPE_SHELLY2_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY2_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLY2_ROLLER = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY2_ROLLER_STR);
public static final ThingTypeUID THING_TYPE_SHELLY25_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY25_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLY25_ROLLER = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY25_ROLLER_STR);
public static final ThingTypeUID THING_TYPE_SHELLY4PRO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY4PRO_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPLUG = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUG_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPLUGS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUGS_STR);
public static final ThingTypeUID THING_TYPE_SHELLYDIMMER = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYDIMMER_STR);
public static final ThingTypeUID THING_TYPE_SHELLYDIMMER2 = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYDIMMER2_STR);
public static final ThingTypeUID THING_TYPE_SHELLYIX3 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYIX3_STR);
public static final ThingTypeUID THING_TYPE_SHELLYBULB = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYBULB_STR);
public static final ThingTypeUID THING_TYPE_SHELLYDUO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYDUO_STR);
public static final ThingTypeUID THING_TYPE_SHELLYVINTAGE = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYVINTAGE_STR);
public static final ThingTypeUID THING_TYPE_SHELLYHT = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYHT_STR);
public static final ThingTypeUID THING_TYPE_SHELLYSENSE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSENSE_STR);
public static final ThingTypeUID THING_TYPE_SHELLYSMOKE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSMOKE_STR);
public static final ThingTypeUID THING_TYPE_SHELLYGAS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYGAS_STR);
public static final ThingTypeUID THING_TYPE_SHELLYFLOOD = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYFLOOD_STR);
public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYDOORWIN_STR);
public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN2 = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYDOORWIN2_STR);
public static final ThingTypeUID THING_TYPE_SHELLYBUTTON1 = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYBUTTON1_STR);
public static final ThingTypeUID THING_TYPE_SHELLYEYE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEYE_STR);
public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_COLOR = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYRGBW2_COLOR_STR);
public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_WHITE = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYRGBW2_WHITE_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPROTECTED = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYPROTECTED_STR);
public static final ThingTypeUID THING_TYPE_SHELLYUNKNOWN = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYUNKNOWN_STR);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream
.of(THING_TYPE_SHELLY1, THING_TYPE_SHELLY1PM, THING_TYPE_SHELLYEM, THING_TYPE_SHELLY3EM,
THING_TYPE_SHELLY2_RELAY, THING_TYPE_SHELLY2_ROLLER, THING_TYPE_SHELLY25_RELAY,
THING_TYPE_SHELLY25_ROLLER, THING_TYPE_SHELLY4PRO, THING_TYPE_SHELLYPLUG,
THING_TYPE_SHELLYPLUGS, THING_TYPE_SHELLYDIMMER, THING_TYPE_SHELLYDIMMER2,
THING_TYPE_SHELLYIX3, THING_TYPE_SHELLYBULB, THING_TYPE_SHELLYDUO, THING_TYPE_SHELLYVINTAGE,
THING_TYPE_SHELLYRGBW2_COLOR, THING_TYPE_SHELLYRGBW2_WHITE, THING_TYPE_SHELLYHT,
THING_TYPE_SHELLYSENSE, THING_TYPE_SHELLYEYE, THING_TYPE_SHELLYSMOKE, THING_TYPE_SHELLYGAS,
THING_TYPE_SHELLYFLOOD, THING_TYPE_SHELLYDOORWIN, THING_TYPE_SHELLYDOORWIN2,
THING_TYPE_SHELLYBUTTON1, THING_TYPE_SHELLYPROTECTED, THING_TYPE_SHELLYUNKNOWN)
.collect(Collectors.toSet()));
// Thing Configuration Properties
public static final String CONFIG_DEVICEIP = "deviceIp";
public static final String CONFIG_HTTP_USERID = "userId";
public static final String CONFIG_HTTP_PASSWORD = "password";
public static final String CONFIG_UPDATE_INTERVAL = "updateInterval";
public static final String PROPERTY_SERVICE_NAME = "serviceName";
public static final String PROPERTY_DEV_NAME = "deviceName";
public static final String PROPERTY_DEV_TYPE = "deviceType";
public static final String PROPERTY_DEV_MODE = "deviceMode";
public static final String PROPERTY_HWREV = "deviceHwRev";
public static final String PROPERTY_HWBATCH = "deviceHwBatch";
public static final String PROPERTY_UPDATE_PERIOD = "devUpdatePeriod";
public static final String PROPERTY_NUM_RELAYS = "numberRelays";
public static final String PROPERTY_NUM_ROLLERS = "numberRollers";
public static final String PROPERTY_NUM_METER = "numberMeters";
public static final String PROPERTY_LAST_ACTIVE = "lastActive";
public static final String PROPERTY_WIFI_NETW = "wifiNetwork";
public static final String PROPERTY_UPDATE_STATUS = "updateStatus";
public static final String PROPERTY_UPDATE_AVAILABLE = "updateAvailable";
public static final String PROPERTY_UPDATE_CURR_VERS = "updateCurrentVersion";
public static final String PROPERTY_UPDATE_NEW_VERS = "updateNewVersion";
public static final String PROPERTY_COAP_DESCR = "coapDeviceDescr";
public static final String PROPERTY_COAP_VERSION = "coapVersion";
public static final String PROPERTY_STATS_TIMEOUTS = "statsTimeoutErrors";
public static final String PROPERTY_STATS_TRECOVERED = "statsTimeoutsRecovered";
public static final String PROPERTY_COIOTAUTO = "coiotAutoEnable";
public static final String PROPERTY_COIOTREFRESH = "coiotAutoRefresh";
// Relay
public static final String CHANNEL_GROUP_RELAY_CONTROL = "relay";
public static final String CHANNEL_OUTPUT_NAME = "outputName";
public static final String CHANNEL_OUTPUT = "output";
public static final String CHANNEL_INPUT = "input";
public static final String CHANNEL_INPUT1 = "input1";
public static final String CHANNEL_INPUT2 = "input2";
public static final String CHANNEL_BRIGHTNESS = "brightness";
public static final String CHANNEL_TIMER_AUTOON = "autoOn";
public static final String CHANNEL_TIMER_AUTOOFF = "autoOff";
public static final String CHANNEL_TIMER_ACTIVE = "timerActive";
// Roller
public static final String CHANNEL_GROUP_ROL_CONTROL = "roller";
public static final String CHANNEL_ROL_CONTROL_CONTROL = "control";
public static final String CHANNEL_ROL_CONTROL_POS = "rollerpos";
public static final String CHANNEL_ROL_CONTROL_TIMER = "timer";
public static final String CHANNEL_ROL_CONTROL_STATE = "state";
public static final String CHANNEL_ROL_CONTROL_STOPR = "stopReason";
// Dimmer
public static final String CHANNEL_GROUP_DIMMER_CONTROL = CHANNEL_GROUP_RELAY_CONTROL;
// Power meter
public static final String CHANNEL_GROUP_METER = "meter";
public static final String CHANNEL_METER_CURRENTWATTS = "currentWatts";
public static final String CHANNEL_METER_LASTMIN = "lastPower";
public static final String CHANNEL_METER_LASTMIN1 = CHANNEL_METER_LASTMIN + "1";
public static final String CHANNEL_METER_TOTALKWH = "totalKWH";
public static final String CHANNEL_EMETER_TOTALRET = "returnedKWH";
public static final String CHANNEL_EMETER_REACTWATTS = "reactiveWatts";
public static final String CHANNEL_EMETER_VOLTAGE = "voltage";
public static final String CHANNEL_EMETER_CURRENT = "current";
public static final String CHANNEL_EMETER_PFACTOR = "powerFactor";
public static final String CHANNEL_GROUP_SENSOR = "sensors";
public static final String CHANNEL_SENSOR_TEMP = "temperature";
public static final String CHANNEL_SENSOR_HUM = "humidity";
public static final String CHANNEL_SENSOR_LUX = "lux";
public static final String CHANNEL_SENSOR_PPM = "ppm";
public static final String CHANNEL_SENSOR_ILLUM = "illumination";
public static final String CHANNEL_SENSOR_VIBRATION = "vibration";
public static final String CHANNEL_SENSOR_TILT = "tilt";
public static final String CHANNEL_SENSOR_FLOOD = "flood";
public static final String CHANNEL_SENSOR_SMOKE = "smoke";
public static final String CHANNEL_SENSOR_CONTACT = "state";
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 = "motion";
public static final String CHANNEL_SENSOR_ERROR = "lastError";
// External sensors for Shelly1/1PM
public static final String CHANNEL_ESENDOR_TEMP1 = CHANNEL_SENSOR_TEMP + "1";
public static final String CHANNEL_ESENDOR_TEMP2 = CHANNEL_SENSOR_TEMP + "2";
public static final String CHANNEL_ESENDOR_TEMP3 = CHANNEL_SENSOR_TEMP + "3";
public static final String CHANNEL_ESENDOR_HUMIDITY = CHANNEL_SENSOR_HUM;
public static final String CHANNEL_GROUP_SENSE_CONTROL = "control";
public static final String CHANNEL_SENSE_KEY = "key";
public static final String CHANNEL_GROUP_BATTERY = "battery";
public static final String CHANNEL_SENSOR_BAT_LEVEL = "batteryLevel";
public static final String CHANNEL_SENSOR_BAT_LOW = "lowBattery";
public static final String CHANNEL_GROUP_LIGHT_CONTROL = "control";
public static final String CHANNEL_LIGHT_COLOR_MODE = "mode";
public static final String CHANNEL_LIGHT_POWER = "power";
public static final String CHANNEL_LIGHT_DEFSTATE = "defaultState";
public static final String CHANNEL_GROUP_LIGHT_CHANNEL = "channel";
// Bulb/RGBW2 in color mode
public static final String CHANNEL_GROUP_COLOR_CONTROL = "color";
public static final String CHANNEL_COLOR_PICKER = "hsb";
public static final String CHANNEL_COLOR_FULL = "full";
public static final String CHANNEL_COLOR_RED = "red";
public static final String CHANNEL_COLOR_GREEN = "green";
public static final String CHANNEL_COLOR_BLUE = "blue";
public static final String CHANNEL_COLOR_WHITE = "white";
public static final String CHANNEL_COLOR_GAIN = "gain";
public static final String CHANNEL_COLOR_EFFECT = "effect";
// Bulb/RGBW2/Dup in White Mode
public static final String CHANNEL_GROUP_WHITE_CONTROL = "white";
public static final String CHANNEL_COLOR_TEMP = "temperature";
// Device Status
public static final String CHANNEL_GROUP_DEV_STATUS = "device";
public static final String CHANNEL_DEVST_NAME = "deviceName";
public static final String CHANNEL_DEVST_UPTIME = "uptime";
public static final String CHANNEL_DEVST_HEARTBEAT = "heartBeat";
public static final String CHANNEL_DEVST_RSSI = "wifiSignal";
public static final String CHANNEL_DEVST_ITEMP = "internalTemp";
public static final String CHANNEL_DEVST_WAKEUP = "wakeupReason";
public static final String CHANNEL_DEVST_ALARM = "alarm";
public static final String CHANNEL_DEVST_ACCUWATTS = "accumulatedWatts";
public static final String CHANNEL_DEVST_ACCUTOTAL = "accumulatedWTotal";
public static final String CHANNEL_DEVST_ACCURETURNED = "accumulatedReturned";
public static final String CHANNEL_DEVST_CHARGER = "charger";
public static final String CHANNEL_DEVST_UPDATE = "updateAvailable";
public static final String CHANNEL_DEVST_SELFTTEST = "selfTest";
public static final String CHANNEL_LED_STATUS_DISABLE = "statusLed";
public static final String CHANNEL_LED_POWER_DISABLE = "powerLed";
// Button/xi3
public static final String CHANNEL_GROUP_STATUS = "status";
public static final String CHANNEL_STATUS_EVENTTYPE = "lastEvent";
public static final String CHANNEL_STATUS_EVENTCOUNT = "eventCount";
// General
public static final String CHANNEL_LAST_UPDATE = "lastUpdate";
public static final String CHANNEL_EVENT_TRIGGER = "event";
public static final String CHANNEL_BUTTON_TRIGGER = "button";
public static final String SERVICE_TYPE = "_http._tcp.local.";
public static final String SHELLY_API_MIN_FWVERSION = "v1.5.7";// v1.5.7+
public static final String SHELLY_API_MIN_FWCOIOT = "v1.6";// v1.6.0+
public static final String SHELLY_API_FWCOIOT2 = "v1.8";// CoAP 2 with FW 1.8+
// Alarm types/messages
public static final String ALARM_TYPE_NONE = "NONE";
public static final String ALARM_TYPE_RESTARTED = "RESTARTED";
public static final String ALARM_TYPE_OVERTEMP = "OVERTEMP";
public static final String ALARM_TYPE_OVERPOWER = "OVERPOWER";
public static final String ALARM_TYPE_OVERLOAD = "OVERLOAD";
public static final String ALARM_TYPE_LOADERR = "LOAD_ERROR";
public static final String ALARM_TYPE_LOW_BATTERY = "LOW_BATTERY";
// Event types
public static final String EVENT_TYPE_RELAY = "relay";
public static final String EVENT_TYPE_ROLLER = "roller";
public static final String EVENT_TYPE_LIGHT = "light";
public static final String EVENT_TYPE_SENSORDATA = "report";
// URI for the EventServlet
public static final String SHELLY_CALLBACK_URI = "/shelly/event";
public static final int DIM_STEPSIZE = 5;
// Formatting: Number of scaling digits
public static final int DIGITS_NONE = 0;
public static final int DIGITS_WATT = 1;
public static final int DIGITS_KWH = 3;
public static final int DIGITS_VOLT = 1;
public static final int DIGITS_TEMP = 1;
public static final int DIGITS_LUX = 1;
public static final int DIGITS_PERCENT = 1;
public static final int SHELLY_API_TIMEOUT_MS = 5000;
public static final int UPDATE_STATUS_INTERVAL_SECONDS = 3; // check for updates every x sec
public static final int UPDATE_SKIP_COUNT = 20; // update every x triggers or when a key was pressed
public static final int UPDATE_MIN_DELAY = 15;// update every x triggers or when a key was pressed
public static final int UPDATE_SETTINGS_INTERVAL_SECONDS = 60; // check for updates every x sec
public static final int HEALTH_CHECK_INTERVAL_SEC = 300; // Health check interval, 5min
}

View File

@@ -0,0 +1,172 @@
/**
* Copyright (c) 2010-2020 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;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ConcurrentHashSet;
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.ShellyProtectedHandler;
import org.openhab.binding.shelly.internal.handler.ShellyRelayHandler;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.binding.shelly.internal.util.ShellyUtils;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.net.HttpServiceUtil;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.reactivex.annotations.NonNull;
/**
* The {@link ShellyHandlerFactory} is responsible for creating things and thing handlers.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@Component(service = { ThingHandlerFactory.class, ShellyHandlerFactory.class }, configurationPid = "binding.shelly")
public class ShellyHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(ShellyHandlerFactory.class);
private final HttpClient httpClient;
private final ShellyTranslationProvider messages;
private final ShellyCoapServer coapServer;
private final Set<ShellyBaseHandler> deviceListeners = new ConcurrentHashSet<>();
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = ShellyBindingConstants.SUPPORTED_THING_TYPES_UIDS;
private ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
private String localIP = "";
private int httpPort = -1;
/**
* Activate the bundle: save properties
*
* @param componentContext
* @param configProperties set of properties from cfg (use same names as in
* thing config)
*/
@Activate
public ShellyHandlerFactory(@Reference NetworkAddressService networkAddressService,
@Reference LocaleProvider localeProvider, @Reference TranslationProvider i18nProvider,
@Reference HttpClientFactory httpClientFactory, ComponentContext componentContext,
Map<String, Object> configProperties) {
logger.debug("Activate Shelly HandlerFactory");
super.activate(componentContext);
messages = new ShellyTranslationProvider(bundleContext.getBundle(), i18nProvider, localeProvider);
localIP = ShellyUtils.getString(networkAddressService.getPrimaryIpv4HostAddress());
if (localIP.isEmpty()) {
logger.warn("{}", messages.get("message.init.noipaddress"));
}
this.httpClient = httpClientFactory.getCommonHttpClient();
httpPort = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext());
if (httpPort == -1) {
httpPort = 8080;
}
logger.debug("Using OH HTTP port {}", httpPort);
this.coapServer = new ShellyCoapServer();
// Save bindingConfig & pass it to all registered listeners
bindingConfig.updateFromProperties(configProperties);
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
String thingType = thingTypeUID.getId();
ShellyBaseHandler handler = null;
if (thingType.equals(THING_TYPE_SHELLYPROTECTED_STR)) {
logger.debug("{}: Create new thing of type {} using ShellyProtectedHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyProtectedHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort,
httpClient);
} else if (thingType.equals(THING_TYPE_SHELLYBULB.getId()) || thingType.equals(THING_TYPE_SHELLYDUO.getId())
|| thingType.equals(THING_TYPE_SHELLYRGBW2_COLOR.getId())
|| thingType.equals(THING_TYPE_SHELLYRGBW2_WHITE.getId())) {
logger.debug("{}: Create new thing of type {} using ShellyLightHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyLightHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort, httpClient);
} else if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
logger.debug("{}: Create new thing of type {} using ShellyRelayHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyRelayHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort, httpClient);
}
if (handler != null) {
deviceListeners.add(handler);
return handler;
}
logger.debug("Unable to create Thing Handler instance!");
return null;
}
/**
* Remove handler of things.
*/
@Override
protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) {
if (thingHandler instanceof ShellyBaseHandler) {
deviceListeners.remove(thingHandler);
}
}
/**
* Dispatch event to registered devices.
*
* @param deviceName
* @param componentIndex Index of component, e.g. 2 for relay2
* @param eventType Type of event, e.g. light
* @param parameters Input parameters from URL, e.g. on sensor reports
*/
public void onEvent(String ipAddress, String deviceName, String componentIndex, String eventType,
Map<String, String> parameters) {
logger.trace("{}: Dispatch event to thing handler", deviceName);
for (ShellyBaseHandler listener : deviceListeners) {
if (listener.onEvent(ipAddress, deviceName, componentIndex, eventType, parameters)) {
// event processed
return;
}
}
}
public ShellyBindingConfiguration getBindingConfig() {
return bindingConfig;
}
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) 2010-2020 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.api;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonSyntaxException;
/**
* The {@link CarNetException} implements an extension to the standard Exception class. This allows to keep also the
* result of the last API call (e.g. including the http status code in the message).
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyApiException extends Exception {
private static final long serialVersionUID = -5809459454769761821L;
private ShellyApiResult apiResult = new ShellyApiResult();
private static String NONE = "none";
public ShellyApiException(Exception exception) {
super(exception);
}
public ShellyApiException(String message) {
super(message);
}
public ShellyApiException(ShellyApiResult res) {
super(NONE);
apiResult = res;
}
public ShellyApiException(String message, Exception exception) {
super(message, exception);
}
public ShellyApiException(ShellyApiResult result, Exception exception) {
super(exception);
apiResult = result;
}
@Override
public String getMessage() {
return isEmpty() ? "" : nonNullString(super.getMessage());
}
@Override
public String toString() {
String message = nonNullString(super.getMessage());
String cause = getCauseClass().toString();
if (!isEmpty()) {
if (isUnknownHost()) {
String[] string = message.split(": "); // java.net.UnknownHostException: api.rach.io
message = MessageFormat.format("Unable to connect to {0} (Unknown host / Network down / Low signal)",
string[1]);
} else if (isMalformedURL()) {
message = MessageFormat.format("Invalid URL: {0}", apiResult.getUrl());
} else if (isTimeout()) {
message = MessageFormat.format("Device unreachable or API Timeout ({0})", apiResult.getUrl());
} else {
message = MessageFormat.format("{0} ({1})", message, cause);
}
} else {
message = apiResult.toString();
}
return message;
}
public boolean isApiException() {
return getCauseClass() == ShellyApiException.class;
}
public boolean isTimeout() {
Class<?> extype = !isEmpty() ? getCauseClass() : null;
return (extype != null) && ((extype == TimeoutException.class) || (extype == ExecutionException.class)
|| (extype == InterruptedException.class) || getMessage().toLowerCase().contains("timeout"));
}
public boolean isHttpAccessUnauthorized() {
return apiResult.isHttpAccessUnauthorized();
}
public boolean isUnknownHost() {
return getCauseClass() == MalformedURLException.class;
}
public boolean isMalformedURL() {
return getCauseClass() == UnknownHostException.class;
}
public boolean isJSONException() {
return getCauseClass() == JsonSyntaxException.class;
}
public ShellyApiResult getApiResult() {
return apiResult;
}
private boolean isEmpty() {
return nonNullString(super.getMessage()).equals(NONE);
}
private static String nonNullString(@Nullable String s) {
return s != null ? s : "";
}
private Class<?> getCauseClass() {
Throwable cause = getCause();
if (getCause() != null) {
return cause.getClass();
}
return ShellyApiException.class;
}
}

View File

@@ -0,0 +1,96 @@
/**
* Copyright (c) 2010-2020 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.api;
import static org.eclipse.jetty.http.HttpStatus.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
/**
* The {@link ShellyApiResult} wraps up the API result and provides some more information like url, http code, received
* response etc.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyApiResult {
public String url = "";
public String method = "";
public String response = "";
public int httpCode = -1;
public String httpReason = "";
public ShellyApiResult() {
}
public ShellyApiResult(String method, String url) {
this.method = method;
this.url = url;
}
public ShellyApiResult(ContentResponse contentResponse) {
fillFromResponse(contentResponse);
}
public String getUrl() {
return !url.isEmpty() ? method + " " + url : "";
}
public String getHttpResponse() {
return response;
}
@Override
public String toString() {
return getUrl() + " > " + getHttpResponse();
}
public boolean isHttpOk() {
return httpCode == OK_200;
}
public boolean isHttpAccessUnauthorized() {
return (httpCode == UNAUTHORIZED_401 || response.contains(SHELLY_APIERR_UNAUTHORIZED));
}
public boolean isHttpTimeout() {
return httpCode == -1 || response.toUpperCase().contains(SHELLY_APIERR_TIMEOUT.toLowerCase());
}
public boolean isHttpServerError() {
return httpCode == INTERNAL_SERVER_ERROR_500;
}
public boolean isNotCalibrtated() {
return getHttpResponse().contains(SHELLY_APIERR_NOT_CALIBRATED);
}
private void fillFromResponse(@Nullable ContentResponse contentResponse) {
if (contentResponse != null) {
String r = contentResponse.getContentAsString();
response = r != null ? r : "";
httpCode = contentResponse.getStatus();
httpReason = contentResponse.getReason();
Request request = contentResponse.getRequest();
if (request != null) {
url = request.getURI().toString();
method = request.getMethod();
}
}
}
}

View File

@@ -0,0 +1,280 @@
/**
* Copyright (c) 2010-2020 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.api;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
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;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* The {@link ShellyDeviceProfile} creates a device profile based on the settings returned from the API's /settings
* call. This is used to be more dynamic in controlling the device, but also to overcome some issues in the API (e.g.
* RGBW2 returns "no meter" even it has one)
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyDeviceProfile {
private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class);
public boolean initialized = false; // true when initialized
public String thingName = "";
public String deviceType = "";
public String settingsJson = "";
public ShellySettingsGlobal settings = new ShellySettingsGlobal();
public ShellySettingsStatus status = new ShellySettingsStatus();
public String hostname = "";
public String mode = "";
public boolean discoverable = true;
public String hwRev = "";
public String hwBatchId = "";
public String mac = "";
public String fwId = "";
public String fwVersion = "";
public String fwDate = "";
public boolean hasRelays = false; // true if it has at least 1 power meter
public int numRelays = 0; // number of relays/outputs
public int numRollers = 0; // number of Rollers, usually 1
public boolean isRoller = false; // true for Shelly2 in roller mode
public boolean isDimmer = false; // true for a Shelly Dimmer (SHDM-1)
public int numMeters = 0;
public boolean isEMeter = false; // true for ShellyEM/3EM
public boolean isLight = false; // true if it is a Shelly Bulb/RGBW2
public boolean isBulb = false; // true only if it is a Bulb
public boolean isDuo = false; // true only if it is a Duo
public boolean isRGBW2 = false; // true only if it a a RGBW2
public boolean inColor = false; // true if bulb/rgbw2 is in color mode
public boolean isSensor = false; // true for HT & Smoke
public boolean hasBattery = false; // true if battery device
public boolean isSense = false; // true if thing is a Shelly Sense
public boolean isHT = false; // true for H&T
public boolean isDW = false; // true for Door Window sensor
public boolean isButton = false; // true for a Shelly Button 1
public boolean isIX3 = false; // true for a Shelly IX
public int minTemp = 0; // Bulb/Duo: Min Light Temp
public int maxTemp = 0; // Bulb/Duo: Max Light Temp
public int updatePeriod = 2 * UPDATE_SETTINGS_INTERVAL_SECONDS + 10;
public Map<String, String> irCodes = new HashMap<>(); // Sense: list of stored IR codes
public ShellyDeviceProfile() {
}
public ShellyDeviceProfile initialize(String thingType, String json) throws ShellyApiException {
Gson gson = new Gson();
initialized = false;
try {
initFromThingType(thingType);
settingsJson = json;
settings = gson.fromJson(json, ShellySettingsGlobal.class);
} catch (IllegalArgumentException | JsonSyntaxException e) {
throw new ShellyApiException(
thingName + ": Unable to transform settings JSON " + e.toString() + ", json='" + json + "'", e);
}
// General settings
deviceType = getString(settings.device.type);
mac = getString(settings.device.mac);
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() : "";
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, "/", "@");
fwId = substringAfter(settings.fw, "@");
discoverable = (settings.discoverable == null) || settings.discoverable;
inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
numRelays = !isLight ? getInteger(settings.device.numOutputs) : 0;
if ((numRelays > 0) && (settings.relays == null)) {
numRelays = 0;
}
isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2);
hasRelays = (numRelays > 0) || isDimmer;
numRollers = getInteger(settings.device.numRollers);
isEMeter = settings.emeters != null;
numMeters = !isEMeter ? getInteger(settings.device.numMeters) : getInteger(settings.device.numEMeters);
if ((numMeters == 0) && isLight) {
// RGBW2 doesn't report, but has one
numMeters = inColor ? 1 : getInteger(settings.device.numOutputs);
}
isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER);
if (settings.sleepMode != null) {
// Sensor, usally 12h
updatePeriod = getString(settings.sleepMode.unit).equalsIgnoreCase("m") ? settings.sleepMode.period * 60 // minutes
: settings.sleepMode.period * 3600; // hours
updatePeriod += 600; // give 10min extra
} else if ((settings.coiot != null) && (settings.coiot.updatePeriod != null)) {
// Derive from CoAP update interval, usually 2*15+5s=50sec -> 70sec
updatePeriod = Math.max(UPDATE_SETTINGS_INTERVAL_SECONDS, 3 * getInteger(settings.coiot.updatePeriod)) + 10;
} else {
updatePeriod = 2 * UPDATE_SETTINGS_INTERVAL_SECONDS + 10;
}
initialized = true;
return this;
}
public boolean containsEventUrl(String eventType) {
return containsEventUrl(settingsJson, eventType);
}
public boolean containsEventUrl(String json, String eventType) {
String settings = json.toLowerCase();
return settings.contains((eventType + SHELLY_EVENTURL_SUFFIX).toLowerCase());
}
public boolean isInitialized() {
return initialized;
}
public void initFromThingType(String name) {
String thingType = (name.contains("-") ? substringBefore(name, "-") : name).toLowerCase().trim();
if (thingType.isEmpty()) {
return;
}
isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR);
isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR);
isRGBW2 = thingType.startsWith(THING_TYPE_SHELLYRGBW2_PREFIX);
isLight = isBulb || isDuo || isRGBW2;
if (isLight) {
minTemp = isBulb ? MIN_COLOR_TEMP_BULB : MIN_COLOR_TEMP_DUO;
maxTemp = isBulb ? MAX_COLOR_TEMP_BULB : MAX_COLOR_TEMP_DUO;
}
boolean isFlood = thingType.equals(THING_TYPE_SHELLYFLOOD_STR);
boolean isSmoke = thingType.equals(THING_TYPE_SHELLYSMOKE_STR);
boolean isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR);
isHT = thingType.equals(THING_TYPE_SHELLYHT_STR);
isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR);
isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR);
isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_STR);
isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR);
isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isSense;
hasBattery = isHT || isFlood || isDW || isSmoke || isButton; // we assume that Sense is connected to // the
// charger
}
public String getControlGroup(int i) {
if (i < 0) {
logger.debug("{}: Invalid index {} for getControlGroup()", thingName, i);
return "";
}
int idx = i + 1;
if (isDimmer) {
return CHANNEL_GROUP_DIMMER_CONTROL;
} else if (isRoller) {
return numRollers == 1 ? CHANNEL_GROUP_ROL_CONTROL : CHANNEL_GROUP_ROL_CONTROL + idx;
} else if (hasRelays) {
return numRelays == 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx;
} else if (isLight) {
return numRelays == 1 ? CHANNEL_GROUP_LIGHT_CONTROL : CHANNEL_GROUP_LIGHT_CONTROL + idx;
} else if (isButton) {
return CHANNEL_GROUP_STATUS;
} else if (isSensor) {
return CHANNEL_GROUP_SENSOR;
}
// e.g. ix3
return numRelays == 1 ? CHANNEL_GROUP_STATUS : CHANNEL_GROUP_STATUS + idx;
}
public String getInputGroup(int i) {
int idx = i + 1; // group names are 1-based
if (isRGBW2) {
return CHANNEL_GROUP_LIGHT_CONTROL;
} else if (isIX3) {
return CHANNEL_GROUP_STATUS + idx;
} else if (isButton) {
return CHANNEL_GROUP_STATUS;
} else if (isRoller) {
return numRelays <= 2 ? CHANNEL_GROUP_ROL_CONTROL : CHANNEL_GROUP_ROL_CONTROL + idx;
} else {
// Device has 1 input per relay: 0=off, 1+2 depend on switch mode
return numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx;
}
}
public String getInputChannel(int i) {
int idx = i + 1; // channel names are 1-based
if (isRGBW2 || isIX3) {
return CHANNEL_INPUT; // RGBW2 has only 1 channel
} else if (hasRelays) {
return CHANNEL_INPUT + idx;
}
return CHANNEL_INPUT;
}
public boolean inButtonMode(int idx) {
if (idx < 0) {
logger.debug("{}: Invalid index {} for inButtonMode()", thingName, idx);
return false;
}
String btnType = "";
if (isButton) {
return true;
} else if (isIX3) {
if ((settings.inputs != null) && (idx >= 0) && (idx < settings.inputs.size())) {
ShellySettingsInput input = settings.inputs.get(idx);
btnType = input.btnType;
}
} else if (isDimmer) {
if ((settings.dimmers != null) && (idx >= 0) && (idx < settings.dimmers.size())) {
ShellySettingsDimmer dimmer = settings.dimmers.get(idx);
btnType = dimmer.btnType;
}
} else if ((settings.relays != null) && (idx >= 0) && (idx < settings.relays.size())) {
ShellySettingsRelay relay = settings.relays.get(idx);
btnType = relay.btnType;
}
if (btnType.equals(SHELLY_BTNT_MOMENTARY) || btnType.equals(SHELLY_BTNT_MOM_ON_RELEASE)
|| btnType.equals(SHELLY_BTNT_DETACHED) || btnType.equals(SHELLY_BTNT_ONE_BUTTON)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,132 @@
/**
* Copyright (c) 2010-2020 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.api;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TreeMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ShellyEventServlet} implements a servlet. which is called by the Shelly device to signnal events (button,
* relay output, sensor data). The binding automatically sets those vent urls on startup (when not disabled in the thing
* config).
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@Component(service = HttpServlet.class, configurationPolicy = ConfigurationPolicy.OPTIONAL, immediate = true)
public class ShellyEventServlet extends HttpServlet {
private static final long serialVersionUID = 549582869577534569L;
private final Logger logger = LoggerFactory.getLogger(ShellyEventServlet.class);
private final HttpService httpService;
private final ShellyHandlerFactory handlerFactory;
@Activate
public ShellyEventServlet(@Reference HttpService httpService, @Reference ShellyHandlerFactory handlerFactory,
Map<String, Object> config) {
this.httpService = httpService;
this.handlerFactory = handlerFactory;
try {
httpService.registerServlet(SHELLY_CALLBACK_URI, this, null, httpService.createDefaultHttpContext());
logger.debug("ShellyEventServlet started at '{}'", SHELLY_CALLBACK_URI);
} catch (NamespaceException | ServletException | IllegalArgumentException e) {
logger.warn("Could not start CallbackServlet", e);
}
}
@Deactivate
protected void deactivate() {
httpService.unregister(SHELLY_CALLBACK_URI);
logger.debug("ShellyEventServlet stopped");
}
@Override
protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse resp)
throws ServletException, IOException, IllegalArgumentException {
String path = "";
String deviceName = "";
String index = "";
String type = "";
if ((request == null) || (resp == null)) {
logger.debug("request or resp must not be null!");
return;
}
try {
path = request.getRequestURI().toLowerCase();
String ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
if (ipAddress == null) {
ipAddress = request.getRemoteAddr();
}
Map<String, String[]> parameters = request.getParameterMap();
logger.debug("CallbackServlet: {} Request from {}:{}{}?{}", request.getProtocol(), ipAddress,
request.getRemotePort(), path, parameters.toString());
if (!path.toLowerCase().startsWith(SHELLY_CALLBACK_URI) || !path.contains("/event/shelly")) {
logger.warn("CallbackServlet received unknown request: path = {}", path);
return;
}
// URL looks like
// <ip address>:<remote port>/shelly/event/shellyrelay-XXXXXX/relay/n?xxxxx or
// <ip address>:<remote port>/shelly/event/shellyrelay-XXXXXX/roller/n?xxxxx or
// <ip address>:<remote port>/shelly/event/shellyht-XXXXXX/sensordata?hum=53,temp=26.50
deviceName = substringBetween(path, "/event/", "/").toLowerCase();
if (path.contains("/" + EVENT_TYPE_RELAY + "/") || path.contains("/" + EVENT_TYPE_ROLLER + "/")
|| path.contains("/" + EVENT_TYPE_LIGHT + "/")) {
index = substringAfterLast(path, "/").toLowerCase();
type = substringBetween(path, deviceName + "/", "/" + index);
} else {
index = "";
type = substringAfterLast(path, "/").toLowerCase();
}
logger.trace("{}: Process event of type type={}, index={}", deviceName, type, index);
Map<String, String> parms = new TreeMap<>();
for (Map.Entry<String, String[]> p : parameters.entrySet()) {
parms.put(p.getKey(), p.getValue()[0]);
}
handlerFactory.onEvent(ipAddress, deviceName, index, type, parms);
} catch (IllegalArgumentException e) {
logger.debug("{}: Exception processing callback: path={}; index={}, type={}, parameters={}", deviceName,
path, index, type, request.getParameterMap().toString());
} finally {
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
resp.getWriter().write("");
}
}
}

View File

@@ -0,0 +1,575 @@
/**
* Copyright (c) 2010-2020 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.api;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyList;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLight;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.core.library.unit.ImperialUnits;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import tec.uom.se.unit.Units;
/**
* {@link ShellyHttpApi} wraps the Shelly REST API and provides various low level function to access the device api (not
* cloud api).
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyHttpApi {
public static final String HTTP_HEADER_AUTH = "Authorization";
public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8";
private final Logger logger = LoggerFactory.getLogger(ShellyHttpApi.class);
private final HttpClient httpClient;
private ShellyThingConfiguration config = new ShellyThingConfiguration();
private String thingName;
private final Gson gson = new Gson();
private int timeoutErrors = 0;
private int timeoutsRecovered = 0;
private ShellyDeviceProfile profile = new ShellyDeviceProfile();
public ShellyHttpApi(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
this.httpClient = httpClient;
this.thingName = thingName;
setConfig(thingName, config);
profile.initFromThingType(thingName);
}
public void setConfig(String thingName, ShellyThingConfiguration config) {
this.thingName = thingName;
this.config = config;
}
public ShellySettingsDevice getDevInfo() throws ShellyApiException {
return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
}
/**
* Initialize the device profile
*
* @param thingType Type of DEVICE as returned from the thing properties (based on discovery)
* @return Initialized ShellyDeviceProfile
* @throws ShellyApiException
*/
public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
String json = request(SHELLY_URL_SETTINGS);
if (json.contains("\"type\":\"SHDM-")) {
logger.trace("{}: Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]", thingName);
json = fixDimmerJson(json);
}
// Map settings to device profile for Light and Sense
profile.initialize(thingType, json);
// 2nd level initialization
profile.thingName = profile.hostname;
if (profile.isLight && (profile.numMeters == 0)) {
logger.debug("{}: Get number of meters from light status", thingName);
ShellyStatusLight status = getLightStatus();
profile.numMeters = status.meters != null ? status.meters.size() : 0;
}
if (profile.isSense) {
profile.irCodes = getIRCodeList();
logger.debug("{}: Sense stored key list loaded, {} entries.", thingName, profile.irCodes.size());
}
return profile;
}
public boolean isInitialized() {
return profile.initialized;
}
/**
* Get generic device settings/status. Json returned from API will be mapped to a Gson object
*
* @return Device settings/status as ShellySettingsStatus object
* @throws ShellyApiException
*/
public ShellySettingsStatus getStatus() throws ShellyApiException {
String json = "";
try {
json = request(SHELLY_URL_STATUS);
// Dimmer2 returns invalid json type for loaderror :-(
json = json.replace("\"loaderror\":0,", "\"loaderror\":false,");
json = json.replace("\"loaderror\":1,", "\"loaderror\":true,");
ShellySettingsStatus status = gson.fromJson(json, ShellySettingsStatus.class);
status.json = json;
return status;
} catch (JsonSyntaxException e) {
throw new ShellyApiException("Unable to parse JSON: " + json, e);
}
}
public ShellyStatusRelay getRelayStatus(Integer relayIndex) throws ShellyApiException {
return callApi(SHELLY_URL_STATUS_RELEAY + "/" + relayIndex.toString(), ShellyStatusRelay.class);
}
public ShellyShortLightStatus setRelayTurn(Integer id, String turnMode) throws ShellyApiException {
return callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
ShellyShortLightStatus.class);
}
public void setBrightness(Integer id, Integer brightness, boolean autoOn) throws ShellyApiException {
String turn = autoOn ? SHELLY_LIGHT_TURN + "=" + SHELLY_API_ON + "&" : "";
request(getControlUriPrefix(id) + "?" + turn + "brightness=" + brightness.toString());
}
public ShellyControlRoller getRollerStatus(Integer rollerIndex) throws ShellyApiException {
String uri = SHELLY_URL_CONTROL_ROLLER + "/" + rollerIndex.toString() + "/pos";
return callApi(uri, ShellyControlRoller.class);
}
public void setRollerTurn(Integer relayIndex, String turnMode) throws ShellyApiException {
request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?go=" + turnMode);
}
public void setRollerPos(Integer relayIndex, Integer position) throws ShellyApiException {
request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?go=to_pos&roller_pos="
+ position.toString());
}
public void setRollerTimer(Integer relayIndex, Integer timer) throws ShellyApiException {
request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?timer=" + timer.toString());
}
public ShellyShortLightStatus getLightStatus(Integer index) throws ShellyApiException {
return callApi(getControlUriPrefix(index), ShellyShortLightStatus.class);
}
public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
ShellyStatusSensor status = callApi(SHELLY_URL_STATUS, ShellyStatusSensor.class);
if (profile.isSense) {
// complete reported data, map C to F or vice versa: C=(F - 32) * 0.5556;
status.tmp.tC = status.tmp.units.equals(SHELLY_TEMP_CELSIUS) ? status.tmp.value
: ImperialUnits.FAHRENHEIT.getConverterTo(Units.CELSIUS).convert(getDouble(status.tmp.value))
.doubleValue();
status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value
: Units.CELSIUS.getConverterTo(ImperialUnits.FAHRENHEIT).convert(getDouble(status.tmp.value))
.doubleValue();
}
if ((status.charger == null) && (status.externalPower != null)) {
// SHelly H&T uses external_power, Sense uses charger
status.charger = status.externalPower != 0;
}
return status;
}
public void setTimer(Integer index, String timerName, Double 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();
request(uri);
}
public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
request(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
}
public ShellySettingsLight getLightSettings() throws ShellyApiException {
return callApi(SHELLY_URL_SETTINGS_LIGHT, ShellySettingsLight.class);
}
public ShellyStatusLight getLightStatus() throws ShellyApiException {
return callApi(SHELLY_URL_STATUS, ShellyStatusLight.class);
}
public void setLightSetting(String parm, String value) throws ShellyApiException {
request(SHELLY_URL_SETTINGS + "?" + parm + "=" + value);
}
/**
* Change between White and Color Mode
*
* @param mode
* @throws ShellyApiException
*/
public void setLightMode(String mode) throws ShellyApiException {
if (!mode.isEmpty() && !profile.mode.equals(mode)) {
setLightSetting(SHELLY_API_MODE, mode);
profile.mode = mode;
profile.inColor = profile.isLight && profile.mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
}
}
/**
* Set a single light parameter
*
* @param lightIndex Index of the light, usually 0 for Bulb and 0..3 for RGBW2.
* @param parm Name of the parameter (see API spec)
* @param value The value
* @throws ShellyApiException
*/
public void setLightParm(Integer lightIndex, String parm, String value) throws ShellyApiException {
// Bulb, RGW2: /<color mode>/<light id>?parm?value
// Dimmer: /light/<light id>?parm=value
request(getControlUriPrefix(lightIndex) + "?" + parm + "=" + value);
}
public void setLightParms(Integer lightIndex, Map<String, String> parameters) throws ShellyApiException {
String url = getControlUriPrefix(lightIndex) + "?";
int i = 0;
for (String key : parameters.keySet()) {
if (i > 0) {
url = url + "&";
}
url = url + key + "=" + parameters.get(key);
i++;
}
request(url);
}
/**
* Retrieve the IR Code list from the Shelly Sense device. The list could be customized by the user. It defines the
* symbolic key code, which gets
* map into a PRONTO code
*
* @return Map of key codes
* @throws ShellyApiException
*/
public Map<String, String> getIRCodeList() throws ShellyApiException {
String result = request(SHELLY_URL_LIST_IR);
// take pragmatic approach to make the returned JSon into named arrays for Gson parsing
String keyList = substringAfter(result, "[");
keyList = substringBeforeLast(keyList, "]");
keyList = keyList.replaceAll(java.util.regex.Pattern.quote("\",\""), "\", \"name\": \"");
keyList = keyList.replaceAll(java.util.regex.Pattern.quote("["), "{ \"id\":");
keyList = keyList.replaceAll(java.util.regex.Pattern.quote("]"), "} ");
String json = "{\"key_codes\" : [" + keyList + "] }";
ShellySendKeyList codes = gson.fromJson(json, ShellySendKeyList.class);
Map<String, String> list = new HashMap<>();
for (ShellySenseKeyCode key : codes.keyCodes) {
list.put(key.id, key.name);
}
return list;
}
/**
* Sends a IR key code to the Shelly Sense.
*
* @param keyCode A keyCoud could be a symbolic name (as defined in the key map on the device) or a PRONTO Code in
* plain or hex64 format
*
* @throws ShellyApiException
* @throws IllegalArgumentException
*/
public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
String type = "";
if (profile.irCodes.containsKey(keyCode)) {
type = SHELLY_IR_CODET_STORED;
} else if ((keyCode.length() > 4) && keyCode.contains(" ")) {
type = SHELLY_IR_CODET_PRONTO;
} else {
type = SHELLY_IR_CODET_PRONTO_HEX;
}
String url = SHELLY_URL_SEND_IR + "?type=" + type;
if (type.equals(SHELLY_IR_CODET_STORED)) {
url = url + "&" + "id=" + keyCode;
} else if (type.equals(SHELLY_IR_CODET_PRONTO)) {
String code = Base64.getEncoder().encodeToString(keyCode.getBytes(StandardCharsets.UTF_8));
if (code == null) {
throw new IllegalArgumentException("Unable to BASE64 encode the pronto code: " + keyCode);
}
url = url + "&" + SHELLY_IR_CODET_PRONTO + "=" + code;
} else if (type.equals(SHELLY_IR_CODET_PRONTO_HEX)) {
url = url + "&" + SHELLY_IR_CODET_PRONTO_HEX + "=" + keyCode;
}
request(url);
}
public void setSenseSetting(String setting, String value) throws ShellyApiException {
request(SHELLY_URL_SETTINGS + "?" + setting + "=" + value);
}
/**
* Set event callback URLs. Depending on the device different event types are supported. In fact all of them will be
* redirected to the binding's servlet and act as a trigger to schedule a status update
*
* @param ShellyApiException
* @throws ShellyApiException
*/
public void setActionURLs() throws ShellyApiException {
setRelayEvents();
setDimmerEvents();
setSensorEventUrls();
}
private void setRelayEvents() throws ShellyApiException {
if (profile.settings.relays != null) {
int num = profile.isRoller ? profile.numRollers : profile.numRelays;
for (int i = 0; i < num; i++) {
setEventUrls(i);
}
}
}
private void setDimmerEvents() throws ShellyApiException {
if (profile.settings.dimmers != null) {
for (int i = 0; i < profile.settings.dimmers.size(); i++) {
setEventUrls(i);
}
} else if (profile.isLight) {
setEventUrls(0);
}
}
/**
* Set sensor Action URLs
*
* @throws ShellyApiException
*/
private void setSensorEventUrls() throws ShellyApiException, ShellyApiException {
if (profile.isSensor) {
logger.debug("{}: Set Sensor Reporting URL", thingName);
setEventUrl(config.eventsSensorReport, SHELLY_EVENT_SENSORREPORT, SHELLY_EVENT_DARK, SHELLY_EVENT_TWILIGHT,
SHELLY_EVENT_FLOOD_DETECTED, SHELLY_EVENT_FLOOD_GONE, SHELLY_EVENT_OPEN, SHELLY_EVENT_CLOSE,
SHELLY_EVENT_VIBRATION, SHELLY_EVENT_ALARM_MILD, SHELLY_EVENT_ALARM_HEAVY, SHELLY_EVENT_ALARM_OFF,
SHELLY_EVENT_TEMP_OVER, SHELLY_EVENT_TEMP_UNDER);
}
}
/**
* Set/delete Relay/Roller/Dimmer Action URLs
*
* @param index Device Index (0-based)
* @throws ShellyApiException
*/
private void setEventUrls(Integer index) throws ShellyApiException {
if (profile.isRoller) {
setEventUrl(EVENT_TYPE_ROLLER, 0, config.eventsRoller, SHELLY_EVENT_ROLLER_OPEN, SHELLY_EVENT_ROLLER_CLOSE,
SHELLY_EVENT_ROLLER_STOP);
} else if (profile.isDimmer) {
// 2 set of URLs
setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsButton, SHELLY_EVENT_BTN1_ON, SHELLY_EVENT_BTN1_OFF,
SHELLY_EVENT_BTN2_ON, SHELLY_EVENT_BTN2_OFF);
setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH1, SHELLY_EVENT_LONGPUSH1,
SHELLY_EVENT_SHORTPUSH2, SHELLY_EVENT_LONGPUSH2);
// Relay output
setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
} else if (profile.hasRelays) {
// Standard relays: btn_xxx, out_xxx, short/longpush URLs
setEventUrl(EVENT_TYPE_RELAY, index, config.eventsButton, SHELLY_EVENT_BTN_ON, SHELLY_EVENT_BTN_OFF);
setEventUrl(EVENT_TYPE_RELAY, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH, SHELLY_EVENT_LONGPUSH);
setEventUrl(EVENT_TYPE_RELAY, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
} else if (profile.isLight) {
// Duo, Bulb
setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
}
}
private void setEventUrl(boolean enabled, String... eventTypes) throws ShellyApiException {
if (config.localIp.isEmpty()) {
throw new ShellyApiException(thingName + ": Local IP address was not detected, can't build Callback URL");
}
for (String eventType : eventTypes) {
if (profile.containsEventUrl(eventType)) {
// H&T adds the type=xx to report_url itself, so we need to ommit here
String eclass = profile.isSensor ? EVENT_TYPE_SENSORDATA : eventType;
String urlParm = eventType.contains("temp") || profile.isHT ? "" : "?type=" + eventType;
String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
+ profile.thingName + "/" + eclass + urlParm;
String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
String testUrl = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
if (!enabled && !profile.settingsJson.contains(testUrl)) {
// Don't set URL to null when the current one doesn't point to this OH
// Don't interfere with a 3rd party App
continue;
}
if (!profile.settingsJson.contains(testUrl)) {
// Current Action URL is != new URL
logger.debug("{}: Set new url for event type {}: {}", thingName, eventType, newUrl);
request(SHELLY_URL_SETTINGS + "?" + mkEventUrl(eventType) + "=" + urlEncode(newUrl));
}
}
}
}
private void setEventUrl(String deviceClass, Integer index, boolean enabled, String... eventTypes)
throws ShellyApiException {
for (String eventType : eventTypes) {
if (profile.containsEventUrl(eventType)) {
String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
+ profile.thingName + "/" + deviceClass + "/" + index + "?type=" + eventType;
String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
String test = "\"" + mkEventUrl(eventType) + "\":\"" + callBackUrl + "\"";
if (!enabled && !profile.settingsJson.contains(test)) {
// Don't set URL to null when the current one doesn't point to this OH
// Don't interfere with a 3rd party App
continue;
}
test = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
if (!profile.settingsJson.contains(test)) {
// Current Action URL is != new URL
logger.debug("{}: Set URL for type {} to {}", thingName, eventType, newUrl);
request(SHELLY_URL_SETTINGS + "/" + deviceClass + "/" + index + "?" + mkEventUrl(eventType) + "="
+ urlEncode(newUrl));
}
}
}
}
private static String mkEventUrl(String eventType) {
return eventType + SHELLY_EVENTURL_SUFFIX;
}
/**
* Submit GET request and return response, check for invalid responses
*
* @param uri: URI (e.g. "/settings")
*/
public <T> T callApi(String uri, Class<T> classOfT) throws ShellyApiException {
try {
String json = request(uri);
return gson.fromJson(json, classOfT);
} catch (JsonSyntaxException e) {
throw new ShellyApiException("Unable to convert JSON", e);
}
}
private String request(String uri) throws ShellyApiException {
ShellyApiResult apiResult = new ShellyApiResult();
int retries = 3;
boolean timeout = false;
while (retries > 0) {
try {
apiResult = innerRequest(HttpMethod.GET, uri);
if (timeout) {
logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
apiResult.getUrl());
timeoutsRecovered++;
}
return apiResult.response; // successful
} catch (ShellyApiException e) {
if ((!e.isTimeout() && !apiResult.isHttpServerError()) || profile.hasBattery || (retries == 0)) {
// Sensor in sleep mode or API exception for non-battery device or retry counter expired
throw e; // non-timeout exception
}
timeout = true;
retries--;
timeoutErrors++; // count the retries
logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
}
}
throw new ShellyApiException("Inconsistent API result or Timeout"); // successful
}
private ShellyApiResult innerRequest(HttpMethod method, String uri) throws ShellyApiException {
Request request = null;
String url = "http://" + config.deviceIp + uri;
ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
try {
request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
if (!config.userId.isEmpty()) {
String value = config.userId + ":" + config.password;
request.header(HTTP_HEADER_AUTH,
HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
}
request.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON);
logger.trace("{}: HTTP {} for {}", thingName, method, url);
// Do request and get response
ContentResponse contentResponse = request.send();
apiResult = new ShellyApiResult(contentResponse);
String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
// validate response, API errors are reported as Json
if (contentResponse.getStatus() != HttpStatus.OK_200) {
throw new ShellyApiException(apiResult);
}
if (response == null || response.isEmpty() || !response.startsWith("{") && !response.startsWith("[")) {
throw new ShellyApiException("Unexpected response: " + response);
}
} catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
ShellyApiException ex = new ShellyApiException(apiResult, e);
if (!ex.isTimeout()) { // will be handled by the caller
logger.trace("{}: API call returned exception", thingName, ex);
}
throw ex;
}
return apiResult;
}
public String getControlUriPrefix(Integer id) {
String uri = "";
if (profile.isLight || profile.isDimmer) {
if (profile.isDuo || profile.isDimmer) {
// Duo + Dimmer
uri = SHELLY_URL_CONTROL_LIGHT;
} else {
// Bulb + RGBW2
uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
}
} else {
// Roller, Relay
uri = SHELLY_URL_CONTROL_RELEAY;
}
uri = uri + "/" + id;
return uri;
}
public int getTimeoutErrors() {
return timeoutErrors;
}
public int getTimeoutsRecovered() {
return timeoutsRecovered;
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 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.coap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
import org.openhab.core.types.State;
/**
* The {@link ShellyCoapListener} describes the listening interface to process Coap responses
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public interface ShellyCoIoTInterface {
public int getVersion();
public CoIotDescrSen fixDescription(CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap);
public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
Map<String, State> updates);
}

View File

@@ -0,0 +1,355 @@
/**
* Copyright (c) 2010-2020 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.coap;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ShellyCoIoTProtocol} implements common functions for the CoIoT implementations
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyCoIoTProtocol {
private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTProtocol.class);
protected final String thingName;
protected final ShellyBaseHandler thingHandler;
protected final ShellyDeviceProfile profile;
protected final Map<String, CoIotDescrBlk> blkMap;
protected final Map<String, CoIotDescrSen> sensorMap;
// Due to the fact that the device reports only the current/last status, but no real events, we need to distinguish
// between a real update or just a repeated status on periodic updates
protected int lastCfgCount = -1;
protected int[] lastEventCount = { -1, -1, -1, -1, -1, -1, -1, -1 }; // 4Pro has 4 relays, so 8 should be fine
protected String[] inputEvent = { "", "", "", "", "", "", "", "" };
public ShellyCoIoTProtocol(String thingName, ShellyBaseHandler thingHandler, Map<String, CoIotDescrBlk> blkMap,
Map<String, CoIotDescrSen> sensorMap) {
this.thingName = thingName;
this.thingHandler = thingHandler;
this.blkMap = blkMap;
this.sensorMap = sensorMap;
this.profile = thingHandler.getProfile();
}
protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
Map<String, State> updates) {
// Process status information and convert into channel updates
// Integer rIndex = Integer.parseInt(sen.links) + 1;
// String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
// : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
int rIndex = getIdFromBlk(sen);
String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
: CHANNEL_GROUP_RELAY_CONTROL + rIndex;
switch (sen.type.toLowerCase()) {
case "b": // BatteryLevel +
updateChannel(updates, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
toQuantityType(s.value, DIGITS_PERCENT, SmartHomeUnits.PERCENT));
break;
case "h" /* Humidity */:
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
toQuantityType(s.value, DIGITS_PERCENT, SmartHomeUnits.PERCENT));
break;
case "m" /* Motion */:
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "l": // Luminosity +
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
toQuantityType(s.value, DIGITS_LUX, SmartHomeUnits.LUX));
break;
case "s": // CatchAll
switch (sen.desc.toLowerCase()) {
case "state": // Relay status +
case "output":
updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
break;
case "input":
handleInput(sen, s, rGroup, updates);
break;
case "brightness":
// already handled by state/output
break;
case "overtemp": // ++
if (s.value == 1) {
thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
}
break;
case "position":
// work around: Roller reports 101% instead max 100
double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType(SHELLY_MAX_ROLLER_POS - pos, SmartHomeUnits.PERCENT));
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
toQuantityType(pos, SmartHomeUnits.PERCENT));
break;
case "flood":
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "vibration": // DW with FW1.6.5+
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "luminositylevel": // +
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM, getStringType(s.valueStr));
break;
case "charger": // Sense
updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
// RGBW2/Bulb
case "red":
updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_RED,
ShellyColorUtils.toPercent((int) s.value));
break;
case "green":
updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GREEN,
ShellyColorUtils.toPercent((int) s.value));
break;
case "blue":
updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_BLUE,
ShellyColorUtils.toPercent((int) s.value));
break;
case "white":
updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_WHITE,
ShellyColorUtils.toPercent((int) s.value));
break;
case "gain":
updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GAIN,
ShellyColorUtils.toPercent((int) s.value, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN));
break;
case "sensorerror": // +
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
break;
default:
// Unknown
return false;
}
break;
default:
// Unknown type
return false;
}
return true;
}
protected boolean updateChannel(Map<String, State> updates, String group, String channel, State value) {
updates.put(mkChannelId(group, channel), value);
return true;
}
protected void handleInput(CoIotDescrSen sen, CoIotSensor s, String rGroup, Map<String, State> updates) {
int idx = getSensorNumber(sen.desc, sen.id) - 1;
String iGroup = profile.getInputGroup(idx);
String iChannel = profile.getInputChannel(idx);
updateChannel(updates, iGroup, iChannel, s.value == 0 ? OnOffType.OFF : OnOffType.ON);
}
protected void handleInputEvent(CoIotDescrSen sen, String type, Integer count, Map<String, State> updates) {
int idx = getSensorNumber(sen.desc, sen.id) - 1;
String group = profile.getInputGroup(idx);
if (count == -1) {
// event type
updateChannel(updates, group, CHANNEL_STATUS_EVENTTYPE, new StringType(type));
inputEvent[idx] = type;
} else {
// event count
updateChannel(updates, group, CHANNEL_STATUS_EVENTCOUNT, getDecimal(count));
if (profile.inButtonMode(idx) && ((profile.hasBattery && (count == 1)) || (count != lastEventCount[idx]))) {
if (profile.isButton || (lastEventCount[idx] != -1)) { // skip the first one if binding was restarted
thingHandler.triggerButton(group, inputEvent[idx]);
}
lastEventCount[idx] = count;
}
}
}
/**
*
* Handles the combined updated of the brightness channel:
* brightness$Switch is the OnOffType (power state)
* brightness&Value is the brightness value
*
* @param profile Device profile, required to select the channel group and name
* @param updates List of updates. updatePower will add brightness$Switch and brightness&Value if changed
* @param id Sensor id from the update
* @param sen Sensor description from the update
* @param s New sensor value
* @param allUpdatesList of updates. This is required, because we need to update both values at the same time
*/
protected void updatePower(ShellyDeviceProfile profile, Map<String, State> updates, int id, CoIotDescrSen sen,
CoIotSensor s, List<CoIotSensor> allUpdates) {
String group = "";
String channel = CHANNEL_BRIGHTNESS;
String checkL = ""; // RGBW-white uses 4 different Power, Brightness, VSwitch values
if (profile.isLight || profile.isDimmer) {
if (profile.isBulb || profile.inColor) {
group = CHANNEL_GROUP_LIGHT_CONTROL;
channel = CHANNEL_LIGHT_POWER;
} else if (profile.isDuo) {
group = CHANNEL_GROUP_WHITE_CONTROL;
} else if (profile.isDimmer) {
group = CHANNEL_GROUP_RELAY_CONTROL;
} else if (profile.isRGBW2) {
group = CHANNEL_GROUP_LIGHT_CHANNEL + id;
checkL = String.valueOf(id - 1); // id is 1-based, L is 0-based
logger.trace("{}: updatePower() for L={}", thingName, checkL);
}
// We need to update brigthtess and on/off state at the same time to avoid "flipping brightness slider" in
// the UI
Double brightness = -1.0;
Double power = -1.0;
for (CoIotSensor update : allUpdates) {
CoIotDescrSen d = fixDescription(sensorMap.get(update.id), blkMap);
if (!checkL.isEmpty() && !d.links.equals(checkL)) {
// continue until we find the correct one
continue;
}
if (d.desc.equalsIgnoreCase("brightness")) {
brightness = new Double(update.value);
} else if (d.desc.equalsIgnoreCase("output") || d.desc.equalsIgnoreCase("state")) {
power = new Double(update.value);
}
}
if (power != -1) {
updateChannel(updates, group, channel + "$Switch", power == 1 ? OnOffType.ON : OnOffType.OFF);
}
if (brightness != -1) {
updateChannel(updates, group, channel + "$Value",
toQuantityType(power == 1 ? brightness : 0, DIGITS_NONE, SmartHomeUnits.PERCENT));
}
} else if (profile.hasRelays) {
group = profile.numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + id;
updateChannel(updates, group, CHANNEL_OUTPUT, s.value == 1 ? OnOffType.ON : OnOffType.OFF);
} else if (profile.isSensor) {
// Sensor state
if (profile.isDW) { // Door Window has item type Contact
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
s.value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
} else {
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
}
}
}
/**
* Find index of Input id, which is required to map to channel name
*
* @parm sensorDesc D field from sensor update
* @param sensorId The id from the sensor update
* @return Index of found entry (+1 will be the suffix for the channel name) or null if sensorId is not found
*/
protected int getSensorNumber(String sensorDesc, String sensorId) {
int idx = 0;
for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
CoIotDescrSen sen = se.getValue();
if (sen.desc.equalsIgnoreCase(sensorDesc)) {
idx++; // iterate from input1..2..n
}
if (sen.id.equalsIgnoreCase(sensorId) && blkMap.containsKey(sen.links)) {
int id = getIdFromBlk(sen);
if (id != -1) {
return id;
}
}
if (sen.id.equalsIgnoreCase(sensorId)) {
return idx;
}
}
logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
return -1;
}
protected int getIdFromBlk(CoIotDescrSen sen) {
int idx = -1;
if (blkMap.containsKey(sen.links)) {
CoIotDescrBlk blk = blkMap.get(sen.links);
String desc = blk.desc.toLowerCase();
if (desc.startsWith(SHELLY_CLASS_RELAY) || desc.startsWith(SHELLY_CLASS_ROLLER)
|| desc.startsWith(SHELLY_CLASS_EMETER)) {
if (desc.contains("_")) { // CoAP v2
idx = Integer.parseInt(substringAfter(desc, "_"));
} else { // CoAP v1
if (desc.substring(0, 5).equalsIgnoreCase(SHELLY_CLASS_RELAY)) {
idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_RELAY));
}
if (desc.substring(0, 6).equalsIgnoreCase(SHELLY_CLASS_ROLLER)) {
idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_ROLLER));
}
if (desc.substring(0, SHELLY_CLASS_EMETER.length()).equalsIgnoreCase(SHELLY_CLASS_EMETER)) {
idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_EMETER));
}
}
idx = idx + 1; // make it 1-based (sen.L is 0-based)
}
}
return idx;
}
/**
*
* Get matching sensorId for updates on "External Temperature" - there might be more than 1 sensor.
*
* @param sensorId sensorId to map into a channel index
* @return Index of the corresponding channel (e.g. 0 build temperature1, 1->temperagture2...)
*/
protected int getExtTempId(String sensorId) {
int idx = 0;
for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
CoIotDescrSen sen = se.getValue();
if (sen.desc.equalsIgnoreCase("external_temperature") || sen.desc.equalsIgnoreCase("external temperature c")
|| (sen.desc.equalsIgnoreCase("extTemp") && !sen.unit.equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT))) {
idx++; // iterate from temperature1..2..n
}
if (sen.id.equalsIgnoreCase(sensorId)) {
return idx;
}
}
logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
return -1;
}
protected ShellyDeviceProfile getProfile() {
return profile;
}
public CoIotDescrSen fixDescription(CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
return sen;
}
}

View File

@@ -0,0 +1,362 @@
/**
* Copyright (c) 2010-2020 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.coap;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tec.uom.se.unit.Units;
/**
* The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 1
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyCoIoTVersion1 extends ShellyCoIoTProtocol implements ShellyCoIoTInterface {
private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTVersion1.class);
public ShellyCoIoTVersion1(String thingName, ShellyBaseHandler thingHandler, Map<String, CoIotDescrBlk> blkMap,
Map<String, CoIotDescrSen> sensorMap) {
super(thingName, thingHandler, blkMap, sensorMap);
}
@Override
public int getVersion() {
return ShellyCoapJSonDTO.COIOT_VERSION_1;
}
/**
* Process CoIoT status update message. If a status update is received, but the device description has not been
* received yet a GET is send to query device description.
*
* @param devId device id included in the status packet
* @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
* @param serial Serial for this request. If this the the same as last serial
* the update was already sent and processed so this one gets
* ignored.
*/
@Override
public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
Map<String, State> updates) {
// first check the base implementation
if (super.handleStatusUpdate(sensorUpdates, sen, s, updates)) {
// process by the base class
return true;
}
// Process status information and convert into channel updates
Integer rIndex = Integer.parseInt(sen.links) + 1;
String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
: CHANNEL_GROUP_RELAY_CONTROL + rIndex;
switch (sen.type.toLowerCase()) {
case "t": // Temperature +
Double value = getDouble(s.value);
switch (sen.desc.toLowerCase()) {
case "temperature": // Sensor Temp
if (getString(getProfile().settings.temperatureUnits)
.equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT)) {
value = ImperialUnits.FAHRENHEIT.getConverterTo(Units.CELSIUS).convert(getDouble(s.value))
.doubleValue();
}
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
break;
case "temperature f": // Device Temp -> ignore (we use C only)
break;
case "temperature c": // Device Temp in C
// Device temperature
updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
break;
case "external temperature f": // Shelly 1/1PM external temp sensors
// ignore F, we use C only
break;
case "external temperature c": // Shelly 1/1PM external temp sensors
case "external_temperature":
int idx = getExtTempId(sen.id);
if (idx > 0) {
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP + idx,
toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
} else {
logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type,
sen.desc);
}
break;
default:
logger.debug("{}: Unknown temperatur type: {}", thingName, sen.desc);
}
break;
case "p": // Power/Watt
// 3EM uses 1-based meter IDs, other 0-based
String mGroup = profile.numMeters == 1 ? CHANNEL_GROUP_METER
: CHANNEL_GROUP_METER + (profile.isEMeter ? sen.links : rIndex);
updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
toQuantityType(s.value, DIGITS_WATT, SmartHomeUnits.WATT));
updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
break;
case "s" /* CatchAll */:
switch (sen.desc.toLowerCase()) {
case "overtemp":
if (s.value == 1) {
thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
}
break;
case "energy counter 0 [w-min]":
updateChannel(updates, rGroup, CHANNEL_METER_LASTMIN1,
toQuantityType(s.value, DIGITS_WATT, SmartHomeUnits.WATT));
break;
case "energy counter 1 [w-min]":
case "energy counter 2 [w-min]":
// we don't use them
break;
case "energy counter total [w-h]": // 3EM reports W/h
case "energy counter total [w-min]":
Double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
updateChannel(updates, rGroup, CHANNEL_METER_TOTALKWH,
toQuantityType(total, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
break;
case "voltage":
updateChannel(updates, rGroup, CHANNEL_EMETER_VOLTAGE,
toQuantityType(getDouble(s.value), DIGITS_VOLT, SmartHomeUnits.VOLT));
break;
case "current":
updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
toQuantityType(getDouble(s.value), DIGITS_VOLT, SmartHomeUnits.AMPERE));
break;
case "pf":
updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
break;
case "position":
// work around: Roller reports 101% instead max 100
double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType(SHELLY_MAX_ROLLER_POS - pos, SmartHomeUnits.PERCENT));
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
toQuantityType(pos, SmartHomeUnits.PERCENT));
break;
case "input event": // Shelly Button 1
handleInputEvent(sen, getString(s.valueStr), -1, updates);
break;
case "input event counter": // Shelly Button 1/ix3
handleInputEvent(sen, "", getInteger((int) s.value), updates);
break;
case "flood":
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "tilt": // DW with FW1.6.5+ //+
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
toQuantityType(s.value, DIGITS_NONE, SmartHomeUnits.DEGREE_ANGLE));
break;
case "vibration": // DW with FW1.6.5+
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "temp": // Shelly Bulb
case "colortemperature": // Shelly Duo
updateChannel(updates,
profile.inColor ? CHANNEL_GROUP_COLOR_CONTROL : CHANNEL_GROUP_WHITE_CONTROL,
CHANNEL_COLOR_TEMP,
ShellyColorUtils.toPercent((int) s.value, profile.minTemp, profile.maxTemp));
break;
case "sensor state": // Shelly Gas
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
break;
case "alarm state": // Shelly Gas
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
getStringType(s.valueStr));
break;
case "self-test state":// Shelly Gas
updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
getStringType(s.valueStr));
break;
case "concentration":// Shelly Gas
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, getDecimal(s.value));
break;
case "sensorerror":
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
break;
default:
// Unknown
return false;
}
break;
default:
// Unknown type
return false;
}
return true;
}
/**
*
* Depending on the device type and firmware release there are significant bugs or incosistencies in the CoIoT
* Device Description returned by the discovery request. Shelly is even not following it's own speicifcation. All of
* that has been reported to Shelly and acknowledged. Firmware 1.6 brought significant improvements. However, the
* old mapping stays in to support older firmware releases.
*
* @param sen Sensor description received from device
* @return fixed Sensor description (sen)
*/
@Override
public CoIotDescrSen fixDescription(CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
// Shelly1: reports null descr+type "Switch" -> map to S
// Shelly1PM: reports null descr+type "Overtemp" -> map to O
// Shelly1PM: reports null descr+type "W" -> add description
// Shelly1PM: reports temp senmsors without desc -> add description
// Shelly Dimmer: sensors are reported without descriptions -> map to S
// SHelly Sense: multiple issues: Description should not be lower case, invalid type for Motion and Battery
// Shelly Sense: Battery is reported with Desc "battery", but type "H" instead of "B"
// Shelly Sense: Motion is reported with Desc "battery", but type "H" instead of "B"
// Shelly Bulb: Colors are coded with Type="Red" etc. rather than Type="S" and color as Descr
// Shelly RGBW2 is reporting Brightness, Power, VSwitch for each channel, but all with L=0
if (sen.desc == null) {
sen.desc = "";
}
String desc = sen.desc.toLowerCase();
// RGBW2 reports Power_0, Power_1, Power_2, Power_3; same for VSwitch and Brightness, all of them linkted to L:0
// we break it up to Power with L:0, Power with L:1...
if (desc.contains("_") && (desc.contains("power") || desc.contains("vswitch") || desc.contains("brightness"))) {
String newDesc = substringBefore(sen.desc, "_");
String newLink = substringAfter(sen.desc, "_");
sen.desc = newDesc;
sen.links = newLink;
if (!blkMap.containsKey(sen.links)) {
// auto-insert a matching blk entry
CoIotDescrBlk blk = new CoIotDescrBlk();
CoIotDescrBlk blk0 = blkMap.get("0"); // blk 0 is always there
blk.id = sen.links;
blk.desc = blk0.desc + "_" + blk.id;
blkMap.put(blk.id, blk);
}
}
switch (sen.type.toLowerCase()) {
case "w": // old devices/firmware releases use "W", new ones "P"
sen.type = "P";
sen.desc = "Power";
break;
case "tc":
sen.type = "T";
sen.desc = "Temperature C";
break;
case "tf":
sen.type = "T";
sen.desc = "Temperature F";
break;
case "overtemp":
sen.type = "S";
sen.desc = "Overtemp";
break;
case "relay0":
case "switch":
case "vswitch":
sen.type = "S";
sen.desc = "State";
break;
}
switch (sen.desc.toLowerCase()) {
case "motion": // fix acc to spec it's T=M
sen.type = "M";
sen.desc = "Motion";
break;
case "battery": // fix: type is B not H
sen.type = "B";
sen.desc = "Battery";
break;
case "overtemp":
sen.type = "S";
sen.desc = "Overtemp";
break;
case "relay0":
case "switch":
case "vswitch":
sen.type = "S";
sen.desc = "State";
break;
case "e cnt 0 [w-min]": // 4 Pro
case "e cnt 1 [w-min]":
case "e cnt 2 [w-min]":
case "e cnt total [w-min]": // 4 Pro
sen.desc = sen.desc.toLowerCase().replace("e cnt", "energy counter");
break;
}
if (sen.desc.isEmpty()) {
switch (sen.type.toLowerCase()) {
case "p":
sen.desc = "Power";
break;
case "T":
sen.desc = "Temperature";
break;
case "input":
sen.type = "S";
sen.desc = "Input";
break;
case "output":
sen.type = "S";
sen.desc = "Output";
break;
case "brightness":
sen.type = "S";
sen.desc = "Brightness";
break;
case "red":
case "green":
case "blue":
case "white":
case "gain":
case "temp": // Bulb: Color temperature
sen.desc = sen.type;
sen.type = "S";
break;
case "vswitch":
// it seems that Shelly tends to break their own spec: T is the description and D is no longer
// included -> map D to sen.T and set CatchAll for T
sen.desc = sen.type;
sen.type = "S";
break;
// Default: set no description
// (there are no T values defined in the CoIoT spec)
case "tostate":
default:
sen.desc = "";
}
}
return sen;
}
}

View File

@@ -0,0 +1,298 @@
/**
* Copyright (c) 2010-2020 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
*/
/**
* The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 1
*
* @author Markus Michels - Initial contribution
*/
package org.openhab.binding.shelly.internal.coap;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 2
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCoIoTInterface {
private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTVersion2.class);
public ShellyCoIoTVersion2(String thingName, ShellyBaseHandler thingHandler, Map<String, CoIotDescrBlk> blkMap,
Map<String, CoIotDescrSen> sensorMap) {
super(thingName, thingHandler, blkMap, sensorMap);
}
@Override
public int getVersion() {
return ShellyCoapJSonDTO.COIOT_VERSION_2;
}
/**
* Process CoIoT status update message. If a status update is received, but the device description has not been
* received yet a GET is send to query device description.
*
* @param devId device id included in the status packet
* @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
* @param serial Serial for this request. If this the the same as last serial
* the update was already sent and processed so this one gets
* ignored.
*/
@Override
public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
Map<String, State> updates) {
// first check the base implementation
if (super.handleStatusUpdate(sensorUpdates, sen, s, updates)) {
// process by the base class
return true;
}
// Process status information and convert into channel updates
// Integer rIndex = Integer.parseInt(sen.links) + 1;
int rIndex = getIdFromBlk(sen);
String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
: CHANNEL_GROUP_RELAY_CONTROL + rIndex;
String mGroup = profile.numMeters == 1 ? CHANNEL_GROUP_METER
: CHANNEL_GROUP_METER + (profile.isEMeter ? getIdFromBlk(sen) : rIndex);
boolean processed = true;
double value = getDouble(s.value);
String reason = "";
switch (sen.id) {
case "3103": // H, humidity, 0-100 percent, unknown 999
case "3106": // L, luminosity, lux, U32, -1
case "3109": // S, tilt, 0-180deg, -1
case "3110": // S, luminosityLevel, dark/twilight/bright, "unknown"=unknown
case "3111": // B, battery, 0-100%, unknown -1
case "3112": // S, charger, 0/1
case "3115": // S, sensorError, 0/1
case "5101": // S, brightness, 1-100%
// processed by base handler
break;
case "6109": // P, overpowerValue, W, U32
case "9101":
// Relay: S, mode, relay/roller or
// Dimmer: S, mode, color/white
// skip, could check against thing mode...
break;
case "1101": // S, output, 0/1
updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
break;
case "1102": // roler_0: S, roller, open/close/stop -> roller state
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STATE, getStringType(s.valueStr));
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));
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType(new Double(SHELLY_MAX_ROLLER_POS - pos), SmartHomeUnits.PERCENT));
break;
case "1105": // S, valvle, closed/opened/not_connected/failure/closing/opening/checking or unbknown
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE, getStringType(s.valueStr));
break;
case "2101": // Input_0: S, input, 0/1
case "2201": // Input_1: S, input, 0/1
case "2301": // Input_2: S, input, 0/1
case "2401": // Input_3: S, input, 0/1
handleInput(sen, s, rGroup, updates);
break;
case "2102": // Input_0: EV, inputEvent, S/SS/SSS/L
case "2202": // Input_1: EV, inputEvent
case "2302": // Input_2: EV, inputEvent
case "2402": // Input_3: EV, inputEvent
handleInputEvent(sen, getString(s.valueStr), -1, updates);
break;
case "2103": // EVC, inputEventCnt, U16
case "2203": // EVC, inputEventCnt, U16
case "2303": // EVC, inputEventCnt, U16
case "2403": // EVC, inputEventCnt, U16
handleInputEvent(sen, "", getInteger((int) s.value), updates);
break;
case "3101": // sensor_0: T, extTemp, C, -55/125; unknown 999
case "3201": // sensor_1: T, extTemp, C, -55/125; unknown 999
case "3301": // sensor_2: T, extTemp, C, -55/125; unknown 999
int idx = getExtTempId(sen.id);
if (idx >= 0) {
// H&T, Fllod, DW only have 1 channel, 1/1PM with Addon have up to to 3 sensors
String channel = profile.isSensor ? CHANNEL_SENSOR_TEMP : CHANNEL_SENSOR_TEMP + idx;
updateChannel(updates, CHANNEL_GROUP_SENSOR, channel,
toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
} else {
logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type, sen.desc);
}
break;
case "3104": // T, deviceTemp, Celsius -40/300; 999=unknown
updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
break;
case "3102": // sensor_0: T, extTemp, F, -67/257, unknown 999
case "3202": // sensor_1: T, extTemp, F, -67/257, unknown 999
case "3302": // sensor_2: T, extTemp, F, -67/257, unknown 999
case "3105": // T, deviceTemp, Fahrenheit -40/572
// skip, we use only C
break;
case "3107": // C, Gas concentration, U16
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, getDecimal(s.value));
break;
case "3108": // DW: S, dwIsOpened, 0/1, -1=unknown
if (value != -1) {
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
} else {
logger.debug("{}: Sensor error reported, check device, battery and installation", thingName);
}
break;
case "3113": // S, sensorOp, warmup/normal/fault
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
break;
case "3114": // S, selfTest, not_completed/completed/running/pending
updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST, getStringType(s.valueStr));
break;
case "3117": // S, extInput, 0/1
handleInput(sen, s, rGroup, updates);
break;
case "4101": // relay_0: P, power, W
case "4201": // relay_1: P, power, W
case "4301": // relay_2: P, power, W
case "4401": // relay_3: P, power, W
case "4105": // emeter_0: P, power, W
case "4205": // emeter_1: P, power, W
case "4305": // emeter_2: P, power, W
case "4102": // roller_0: P, rollerPower, W, 0-2300, unknown -1
case "4202": // roller_1: P, rollerPower, W, 0-2300, unknown -1
updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
toQuantityType(s.value, DIGITS_WATT, SmartHomeUnits.WATT));
updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
break;
case "4103": // relay_0: E, energy, Wmin, U32
case "4203": // relay_1: E, energy, Wmin, U32
case "4303": // relay_2: E, energy, Wmin, U32
case "4403": // relay_3: E, energy, Wmin, U32
case "4104": // roller_0: E, rollerEnergy, Wmin, U32, -1
case "4204": // roller_0: E, rollerEnergy, Wmin, U32, -1
case "4106": // emeter_0: E, energy, Wh, U32
case "4206": // emeter_1: E, energy, Wh, U32
case "4306": // emeter_2: E, energy, Wh, U32
double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
updateChannel(updates, mGroup, CHANNEL_METER_TOTALKWH,
toQuantityType(total, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
break;
case "4107": // emeter_0: E, energyReturned, Wh, U32, -1
case "4207": // emeter_1: E, energyReturned, Wh, U32, -1
case "4307": // emeter_2: E, energyReturned, Wh, U32, -1
updateChannel(updates, mGroup, CHANNEL_EMETER_TOTALRET,
toQuantityType(getDouble(s.value) / 1000, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
break;
case "4108": // emeter_0: V, voltage, 0-265V, U32, -1
case "4208": // emeter_1: V, voltage, 0-265V, U32, -1
case "4308": // emeter_2: V, voltage, 0-265V, U32, -1
updateChannel(updates, mGroup, CHANNEL_EMETER_VOLTAGE,
toQuantityType(getDouble(s.value), DIGITS_VOLT, SmartHomeUnits.VOLT));
break;
case "4109": // emeter_0: A, current, 0/120A, -1
case "4209": // emeter_1: A, current, 0/120A, -1
case "4309": // emeter_2: A, current, 0/120A, -1
updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
toQuantityType(getDouble(s.value), DIGITS_VOLT, SmartHomeUnits.AMPERE));
break;
case "4110": // emeter_0: S, powerFactor, 0/1, -1
case "4210": // emeter_1: S, powerFactor, 0/1, -1
case "4310": // emeter_2: S, powerFactor, 0/1, -1
updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
break;
case "6101": // A, overtemp, 0/1
if (s.value == 1) {
thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
}
break;
case "6102": // relay_0: A, overpower, 0/1
case "6202": // relay_1: A, overpower, 0/1
case "6302": // relay_2: A, overpower, 0/1
case "6402": // relay_3: A, overpower, 0/1
if (s.value == 1) {
thingHandler.postEvent(ALARM_TYPE_OVERPOWER, true);
}
break;
case "6104": // relay_0: A, loadError, 0/1
case "6204": // relay_1: A, loadError, 0/1
case "6304": // relay_2: A, loadError, 0/1
case "6404": // relay_3: A, loadError, 0/1
if (s.value == 1) {
thingHandler.postEvent(ALARM_TYPE_LOADERR, true);
}
break;
case "6103": // roller_0: A, rollerStopReason, normal/safety_switch/obstacle/overpower
reason = getString(s.valueStr);
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STOPR, getStringType(reason));
if (!reason.isEmpty() && !reason.equalsIgnoreCase(SHELLY_API_STOPR_NORMAL)) {
thingHandler.postEvent("ROLLER_" + reason.toUpperCase(), true);
}
case "6106": // A, flood, 0/1, -1
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "6108": // A, gas, none/mild/heavy/test or unknown
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE, getStringType(s.valueStr));
break;
case "6110": // A, vibration, 0/1, -1=unknown
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "9102": // EV, wakeupEvent, battery/button/periodic/poweron/sensor/ext_power, "unknown"=unknown
thingHandler.updateWakeupReason(s.valueArray);
break;
case "9103": // EVC, cfgChanged, U16
if ((lastCfgCount != -1) && (lastCfgCount != s.value)) {
thingHandler.requestUpdates(1, true); // refresh config
}
lastCfgCount = (int) s.value;
break;
default:
processed = false;
}
return processed;
}
@Override
public CoIotDescrSen fixDescription(CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
return sen;
}
}

View File

@@ -0,0 +1,570 @@
/**
* Copyright (c) 2010-2020 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.coap;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.net.UnknownHostException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.coap.CoAP.Code;
import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.core.coap.CoAP.Type;
import org.eclipse.californium.core.coap.MessageObserverAdapter;
import org.eclipse.californium.core.coap.Option;
import org.eclipse.californium.core.coap.OptionNumberRegistry;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescrTypeAdapter;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescription;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotGenericSensorList;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensorTypeAdapter;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
/**
* The {@link ShellyCoapHandler} handles the CoIoT/CoAP registration and events.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyCoapHandler implements ShellyCoapListener {
private static final byte[] EMPTY_BYTE = new byte[0];
private final Logger logger = LoggerFactory.getLogger(ShellyCoapHandler.class);
private final ShellyBaseHandler thingHandler;
private ShellyThingConfiguration config = new ShellyThingConfiguration();
private final GsonBuilder gsonBuilder = new GsonBuilder();
private final Gson gson;
private String thingName;
private boolean coiotBound = false;
private ShellyCoIoTInterface coiot;
private int coiotVers = -1;
private final ShellyCoapServer coapServer;
private @Nullable CoapClient statusClient;
private Request reqDescription = new Request(Code.GET, Type.CON);
private Request reqStatus = new Request(Code.GET, Type.CON);
private boolean discovering = false;
private int lastSerial = -1;
private String lastPayload = "";
private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
private Map<String, CoIotDescrSen> sensorMap = new LinkedHashMap<>();
private final ShellyDeviceProfile profile;
public ShellyCoapHandler(ShellyBaseHandler thingHandler, ShellyCoapServer coapServer) {
this.thingHandler = thingHandler;
this.thingName = thingHandler.thingName;
this.coapServer = coapServer;
this.coiot = new ShellyCoIoTVersion1(thingName, thingHandler, blkMap, sensorMap); // Default
gsonBuilder.registerTypeAdapter(CoIotDevDescription.class, new CoIotDevDescrTypeAdapter());
gsonBuilder.registerTypeAdapter(CoIotGenericSensorList.class, new CoIotSensorTypeAdapter());
gson = gsonBuilder.create();
profile = thingHandler.getProfile();
}
/**
* Initialize CoAP access, send discovery packet and start Status server
*
* @parm thingName Thing name derived from Thing Type/hostname
* @parm config ShellyThingConfiguration
* @thows ShellyApiException
*/
public synchronized void start(String thingName, ShellyThingConfiguration config) throws ShellyApiException {
try {
this.thingName = thingName;
this.config = config;
if (isStarted()) {
logger.trace("{}: CoAP Listener was already started", thingName);
stop();
}
logger.debug("{}: Starting CoAP Listener", thingName);
coapServer.start(config.localIp, this);
statusClient = new CoapClient(completeUrl(config.deviceIp, COLOIT_URI_DEVSTATUS))
.setTimeout((long) SHELLY_API_TIMEOUT_MS).useNONs().setEndpoint(coapServer.getEndpoint());
discover();
} catch (UnknownHostException e) {
logger.debug("{}: CoAP Exception", thingName, e);
throw new ShellyApiException("Unknown Host: " + config.deviceIp, e);
}
}
public boolean isStarted() {
return statusClient != null;
}
/**
* Process an inbound Response (or mapped Request): decode CoAP options. handle discovery result or status updates
*
* @param response The Response packet
*/
@Override
public void processResponse(@Nullable Response response) {
if (response == null) {
return; // other device instance
}
String ip = response.getSourceContext().getPeerAddress().toString();
if (!ip.contains(config.deviceIp)) {
return;
}
String payload = "";
String devId = "";
String uri = "";
// int validity = 0;
int serial = -1;
try {
if (logger.isDebugEnabled()) {
logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName,
response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString());
}
if (response.isCanceled() || response.isDuplicate() || response.isRejected()) {
logger.debug("{} ({}): Packet was canceled, rejected or is a duplicate -> discard", thingName, devId);
return;
}
if (response.getCode() == ResponseCode.CONTENT) {
payload = response.getPayloadString();
List<Option> options = response.getOptions().asSortedList();
int i = 0;
while (i < options.size()) {
Option opt = options.get(i);
switch (opt.getNumber()) {
case OptionNumberRegistry.URI_PATH:
uri = COLOIT_URI_BASE + opt.getStringValue();
break;
case COIOT_OPTION_GLOBAL_DEVID:
devId = opt.getStringValue();
String sVersion = substringAfterLast(devId, "#");
int iVersion = Integer.parseInt(sVersion);
if (coiotBound && (coiotVers != iVersion)) {
logger.debug(
"{}: CoIoT versopm has changed from {} to {}, maybe the firmware was upgraded",
thingName, coiotVers, iVersion);
thingHandler.reinitializeThing();
coiotBound = false;
}
if (!coiotBound) {
thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
if (iVersion == COIOT_VERSION_1) {
coiot = new ShellyCoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
} else if (iVersion == COIOT_VERSION_2) {
coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap);
} else {
logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
return;
}
coiotVers = iVersion;
coiotBound = true;
}
break;
case COIOT_OPTION_STATUS_VALIDITY:
// validity = o.getIntegerValue();
break;
case COIOT_OPTION_STATUS_SERIAL:
serial = opt.getIntegerValue();
break;
default:
logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId,
opt.getNumber(), opt.getValue());
}
i++;
}
// If we received a CoAP message successful the thing must be online
thingHandler.setThingOnline();
// The device changes the serial on every update, receiving a message with the same serial is a
// duplicate, excep for battery devices! Those reset the serial every time when they wake-up
if ((serial == lastSerial) && payload.equals(lastPayload)
&& (!profile.hasBattery || ((serial & 0xFF) != 0))) {
logger.debug("{}: Serial {} was already processed, ignore update", thingName, serial);
return;
}
// fixed malformed JSON :-(
payload = fixJSON(payload);
if (uri.equalsIgnoreCase(COLOIT_URI_DEVDESC) || (uri.isEmpty() && payload.contains(COIOT_TAG_BLK))) {
handleDeviceDescription(devId, payload);
} else if (uri.equalsIgnoreCase(COLOIT_URI_DEVSTATUS)
|| (uri.isEmpty() && payload.contains(COIOT_TAG_GENERIC))) {
handleStatusUpdate(devId, payload, serial);
}
} else {
// error handling
logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, response.getCode(),
response.getPayloadString());
}
if (!discovering) {
// Observe Status Updates
reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
discovering = true;
}
} catch (IllegalArgumentException | NullPointerException e) {
logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
resetSerial();
}
}
/**
* Process a CoIoT device description message. This includes definitions on device units (Relay0, Relay1, Sensors
* etc.) as well as a definition of sensors and actors. This information needs to be stored allowing to map ids from
* status updates to the device units and matching the correct thing channel.
*
* @param devId The device id reported in the CoIoT message.
* @param payload Device desciption in JSon format, example:
* {"blk":[{"I":0,"D":"Relay0"}],"sen":[{"I":112,"T":"Switch","R":"0/1","L":0}],"act":[{"I":211,"D":"Switch","L":0,"P":[{"I":2011,"D":"ToState","R":"0/1"}]}]}
*/
private void handleDeviceDescription(String devId, String payload) {
logger.debug("{}: CoIoT Device Description for {}: {}", thingName, devId, payload);
try {
boolean valid = true;
// Decode Json
CoIotDevDescription descr = gson.fromJson(payload, CoIotDevDescription.class);
for (int i = 0; i < descr.blk.size(); i++) {
CoIotDescrBlk blk = descr.blk.get(i);
logger.debug("{}: id={}: {}", thingName, blk.id, blk.desc);
if (!blkMap.containsKey(blk.id)) {
blkMap.put(blk.id, blk);
} else {
blkMap.replace(blk.id, blk);
}
if ((blk.type != null) && !blk.type.isEmpty()) {
// in fact it is a sen entry - that's vioaling the Spec
logger.trace("{}: fix: auto-create sensor definition for id {}/{}!", thingName, blk.id,
blk.desc);
CoIotDescrSen sen = new CoIotDescrSen();
sen.id = blk.id;
sen.desc = blk.desc;
sen.type = blk.type;
sen.range = blk.range;
sen.links = blk.links;
valid &= addSensor(sen);
}
}
// Save to thing properties
thingHandler.updateProperties(PROPERTY_COAP_DESCR, payload);
logger.debug("{}: Adding {} sensor definitions", thingName, descr.sen.size());
if (descr.sen != null) {
for (int i = 0; i < descr.sen.size(); i++) {
valid &= addSensor(descr.sen.get(i));
}
}
if (!valid) {
logger.debug(
"{}: Incompatible device description detected for CoIoT version {} (id length mismatch), discarding!",
thingName, coiot.getVersion());
thingHandler.updateProperties(PROPERTY_COAP_DESCR, "");
discover();
return;
}
} catch (JsonSyntaxException e) {
logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload);
} catch (NullPointerException | IllegalArgumentException e) {
logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload, e);
}
}
/**
* Add a new sensor to the sensor table
*
* @param sen CoIotDescrSen of the sensor
*/
private synchronized boolean addSensor(CoIotDescrSen sen) {
logger.debug("{}: id {}: {}, Type={}, Range={}, Links={}", thingName, sen.id, sen.desc, sen.type, sen.range,
sen.links);
// CoIoT version 2 changes from 3 digit IDs to 4 digit IDs
// We need to make sure that the persisted device description matches,
// otherwise the stored one is discarded and a new discovery is triggered
// This happens on firmware up/downgrades (version 1.8 brings CoIoT v2 with 4 digit IDs)
int vers = coiot.getVersion();
if (((vers == COIOT_VERSION_1) && (sen.id.length() > 3))
|| ((vers >= COIOT_VERSION_2) && (sen.id.length() < 4))) {
return false;
}
try {
CoIotDescrSen fixed = coiot.fixDescription(sen, blkMap);
if (!sensorMap.containsKey(fixed.id)) {
sensorMap.put(sen.id, fixed);
} else {
sensorMap.replace(sen.id, fixed);
}
} catch (NullPointerException | IllegalArgumentException e) { // depending on firmware release the CoAP device
// description is buggy
logger.debug("{}: Unable to decode sensor definition -> skip", thingName, e);
}
return true;
}
/**
* Process CoIoT status update message. If a status update is received, but the device description has not been
* received yet a GET is send to query device description.
*
* @param devId device id included in the status packet
* @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
* @param serial Serial for this request. If this the the same as last serial
* the update was already sent and processed so this one gets
* ignored.
*/
private void handleStatusUpdate(String devId, String payload, int serial) {
logger.debug("{}: CoIoT Sensor data {} (serial={})", thingName, payload, serial);
if (blkMap.isEmpty()) {
// send discovery packet
resetSerial();
discover();
// try to uses description from last initialization
String savedDescr = thingHandler.getProperty(PROPERTY_COAP_DESCR);
if (savedDescr.isEmpty()) {
logger.debug("{}: Device description not yet received, trigger auto-initialization", thingName);
return;
}
// simulate received device description to create element table
logger.debug("{}: Device description for {} restored: {}", thingName, devId, savedDescr);
handleDeviceDescription(devId, savedDescr);
}
// Parse Json,
CoIotGenericSensorList list = gson.fromJson(fixJSON(payload), CoIotGenericSensorList.class);
if (list.generic == null) {
logger.debug("{}: Sensor list has invalid format! Payload: {}", devId, payload);
return;
}
List<CoIotSensor> sensorUpdates = list.generic;
Map<String, State> updates = new TreeMap<String, State>();
logger.debug("{}: {} CoAP sensor updates received", thingName, sensorUpdates.size());
int failed = 0;
for (int i = 0; i < sensorUpdates.size(); i++) {
try {
CoIotSensor s = sensorUpdates.get(i);
if (!sensorMap.containsKey(s.id)) {
logger.debug("{}: Invalid id in sensor description: {}, index {}", thingName, s.id, i);
failed++;
continue;
}
CoIotDescrSen sen = sensorMap.get(s.id);
// find matching sensor definition from device description, use the Link ID as index
sen = coiot.fixDescription(sen, blkMap);
if (!blkMap.containsKey(sen.links)) {
logger.debug("{}: Invalid CoAP description: sen.links({}", thingName, getString(sen.links));
continue;
}
if (!blkMap.containsKey(sen.links)) {
logger.debug("{}: Unable to find BLK for link {} from sen.id={}", thingName, sen.links, sen.id);
continue;
}
CoIotDescrBlk element = blkMap.get(sen.links);
logger.trace("{}: Sensor value[{}]: id={}, Value={} ({}, Type={}, Range={}, Link={}: {})", thingName,
i, s.id, getString(s.valueStr).isEmpty() ? s.value : s.valueStr, sen.desc, sen.type, sen.range,
sen.links, element.desc);
if (!coiot.handleStatusUpdate(sensorUpdates, sen, s, updates)) {
logger.debug("{}: CoIoT data for id {}, type {}/{} not processed, value={}; payload={}", thingName,
sen.id, sen.type, sen.desc, s.value, payload);
}
} catch (NullPointerException | IllegalArgumentException e) {
// even the processing of one value failed we continue with the next one (sometimes this is caused by
// buggy formats provided by the device
logger.debug("{}: Unable to process data from sensor[{}], devId={}, payload={}", thingName, i, devId,
payload, e);
}
}
if (!updates.isEmpty()) {
int updated = 0;
for (Map.Entry<String, State> u : updates.entrySet()) {
updated += thingHandler.updateChannel(u.getKey(), u.getValue(), false) ? 1 : 0;
}
if (updated > 0) {
logger.debug("{}: {} channels updated from CoIoT status, serial={}", thingName, updated, serial);
if (profile.isSensor || profile.isRoller) {
// CoAP is currently lacking the lastUpdate info, so we use host timestamp
thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
}
}
// Old firmware release are lacking various status values, which are not updated using CoIoT.
// In this case we keep a refresh so it gets polled using REST. Beginning with Firmware 1.6 most
// of the values are available
if ((!thingHandler.autoCoIoT && (thingHandler.scheduledUpdates <= 1))
|| (thingHandler.autoCoIoT && !profile.isLight && !profile.hasBattery)) {
thingHandler.requestUpdates(1, false);
}
} else {
if (failed == sensorUpdates.size()) {
logger.debug("{}: Device description problem detected, re-discover", thingName);
coiotBound = false;
discover();
}
}
// Remember serial, new packets with same serial will be ignored
lastSerial = serial;
lastPayload = payload;
}
private void discover() {
reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON);
}
/**
* Fix malformed JSON - stupid, but the devices sometimes return malformed JSON with then causes a
* JsonSyntaxException
*
* @param json to be checked/fixed
*/
private static String fixJSON(String payload) {
String json = payload;
json = json.replace("}{", "},{");
json = json.replace("][", "],[");
json = json.replace("],,[", "],[");
return json;
}
/**
* Send a new request (Discovery to get Device Description). Before a pending
* request will be canceled.
*
* @param request The current request (this will be canceled an a new one will
* be created)
* @param ipAddress Device's IP address
* @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
* @param con true: send as CON, false: send as NON
* @return new packet
*/
private Request sendRequest(@Nullable Request request, String ipAddress, String uri, Type con) {
if ((request != null) && !request.isCanceled()) {
request.cancel();
}
resetSerial();
return newRequest(ipAddress, uri, con).send();
}
/**
* Allocate a new Request structure. A message observer will be added to get the
* callback when a response has been received.
*
* @param ipAddress IP address of the device
* @param uri URI to be addressed
* @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
* @param con true: send as CON, false: send as NON
* @return new packet
*/
private Request newRequest(String ipAddress, String uri, Type con) {
// We need to build our own Request to set an empty Token
Request request = new Request(Code.GET, con);
request.setURI(completeUrl(ipAddress, uri));
request.setToken(EMPTY_BYTE);
request.addMessageObserver(new MessageObserverAdapter() {
@Override
public void onResponse(@Nullable Response response) {
processResponse(response);
}
@Override
public void onCancel() {
logger.debug("{}: CoAP Request was canceled", thingName);
}
@Override
public void onTimeout() {
logger.debug("{}: CoAP Request timed out", thingName);
}
});
return request;
}
/**
* Reset serial and payload used to detect duplicate messages, which have to be ignored.
* We can't rely that the device manages serials correctly all the time. There are firmware releases sending updated
* sensor information with the serial from the last packet, which is wrong. We bypass this problem by comparing also
* the payload.
*/
private void resetSerial() {
lastSerial = -1;
lastPayload = "";
}
public int getVersion() {
return coiotVers;
}
/**
* Cancel pending requests and shutdown the client
*/
public synchronized void stop() {
if (isStarted()) {
logger.debug("{}: Stopping CoAP Listener", thingName);
coapServer.stop(this);
if (statusClient != null) {
statusClient.shutdown();
statusClient = null;
}
if (!reqDescription.isCanceled()) {
reqDescription.cancel();
}
if (!reqStatus.isCanceled()) {
reqStatus.cancel();
}
}
resetSerial();
coiotBound = false;
}
public void dispose() {
stop();
}
private static String completeUrl(String ipAddress, String uri) {
return "coap://" + ipAddress + ":" + COIOT_PORT + uri;
}
}

View File

@@ -0,0 +1,331 @@
/**
* Copyright (c) 2010-2020 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.coap;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.SerializedName;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
/**
* The {@link ShellyCoapJSonDTO} helps the CoIoT Json into Java objects
*
* @author Markus Michels - Initial contribution
*/
public class ShellyCoapJSonDTO {
// Coap
public static final int COIOT_VERSION_1 = 1;
public static final int COIOT_VERSION_2 = 2;
public static final int COIOT_PORT = 5683;
public static final String COAP_MULTICAST_ADDRESS = "224.0.1.187";
public static final String COLOIT_URI_BASE = "/cit/";
public static final String COLOIT_URI_DEVDESC = COLOIT_URI_BASE + "d";
public static final String COLOIT_URI_DEVSTATUS = COLOIT_URI_BASE + "s";
public static final int COIOT_OPTION_GLOBAL_DEVID = 3332;
public static final int COIOT_OPTION_STATUS_VALIDITY = 3412;
public static final int COIOT_OPTION_STATUS_SERIAL = 3420;
public static final String COIOT_TAG_BLK = "blk";
public static final String COIOT_TAG_SEN = "sen";
public static final String COIOT_TAG_ACT = "act";
public static final String COIOT_TAG_GENERIC = "G";
public static class CoIotDescrBlk {
@SerializedName("I")
String id; // ID
@SerializedName("D")
String desc; // Description
// Sometimes sen entries are part of the blk array - not conforming the Spec!
@SerializedName("T")
public String type; // Type
@SerializedName("R")
public String range; // Range
@SerializedName("L")
public String links; // Links
}
public static class CoIotDescrSen {
@SerializedName("I")
String id; // ID
@SerializedName("D")
String desc; // Description
@SerializedName("T")
public String type; // Type
@SerializedName("R")
public String range; // Range
@SerializedName("L")
public String links; // Links
@SerializedName("U")
public String unit; // Unit
}
public static class CoIotDescrP {
@SerializedName("I")
String id; // ID
@SerializedName("D")
String desc; // Description
@SerializedName("R")
public String range; // Range
}
public static class CoIotDescrAct {
@SerializedName("I")
String id; // ID
@SerializedName("D")
String desc; // Description
@SerializedName("L")
public String links; // Links
@SerializedName("P")
public List<CoIotDescrP> pTag; // ?
}
public static class CoIotDevDescription {
public List<CoIotDescrBlk> blk;
public List<CoIotDescrSen> sen;
// public List<CoIotDescrAct> act;
public CoIotDevDescription() {
blk = new ArrayList<>();
sen = new ArrayList<>();
}
}
public static class CoIotSensor {
@SerializedName("index")
public String id; // id
public double value; // value
public String valueStr; // value
public List<Object> valueArray;
}
public static class CoIotGenericSensorList {
@SerializedName("G")
public List<CoIotSensor> generic;
public CoIotGenericSensorList() {
generic = new ArrayList<>();
}
}
protected static class CoIotDevDescrTypeAdapter extends TypeAdapter<CoIotDevDescription> {
@Override
public CoIotDevDescription read(final JsonReader in) throws IOException {
CoIotDevDescription descr = new CoIotDevDescription();
/*
* parse JSON like
* "blk": [
* { "I": 0, "D": "Relay0"},
* { "I": 1, "D": "Sensors"} ],
* "sen": [
* { "I": 111, "T": "P", "D": "Power","R": "0/3500","L": 0},
* { "I": 112,"T": "S","D": "Switch","R": "0/1","L": 0}
* ]
*/
in.beginObject();
String name = in.nextName();
if (name.equalsIgnoreCase(COIOT_TAG_BLK)) {
in.beginArray();
while (in.hasNext()) {
CoIotDescrBlk blk = new CoIotDescrBlk();
in.beginObject();
while (in.hasNext()) {
switch (in.nextName().toUpperCase()) {
case "I":
blk.id = in.nextString();
break;
case "D":
blk.desc = in.nextString();
break;
default:
// skip data
in.nextNull();
}
}
in.endObject();
descr.blk.add(blk);
}
in.endArray();
name = in.nextName();
}
if (name.equalsIgnoreCase(COIOT_TAG_SEN)) {
/*
* parse sensor list, e.g.
* "sen":[
* { "I":111,"T":"Red","R":"0/255","L":0},
* { "I":121,"T":"Green","R":"0/255","L":0},
* ]
*/
in.beginArray();
while (in.hasNext()) {
CoIotDescrSen sen = new CoIotDescrSen();
in.beginObject();
while (in.hasNext()) {
String tag = in.nextName();
switch (tag.toUpperCase()) {
case "I":
sen.id = in.nextString();
break;
case "D":
sen.desc = in.nextString();
break;
case "T":
sen.type = in.nextString();
break;
case "R":
JsonToken token = in.peek();
if (token == JsonToken.BEGIN_ARRAY) {
// must be v2: an array
in.beginArray();
sen.range = "";
while (in.hasNext()) {
String value = in.nextString();
sen.range += sen.range.isEmpty() ? value : ";" + value;
}
in.endArray();
} else {
sen.range = in.nextString();
}
break;
case "L":
sen.links = String.valueOf(in.nextInt());
break;
case "U": // New in CoAPv2: unit"
sen.unit = in.nextString();
break;
default:
// skip data
in.nextNull();
}
}
in.endObject();
descr.sen.add(sen);
}
in.endArray();
}
in.endObject();
return descr;
}
@Override
public void write(final JsonWriter out, final CoIotDevDescription descr) throws IOException {
out.beginObject();
if (descr != null) {
out.name(COIOT_TAG_BLK).beginArray();
for (int i = 0; i < descr.blk.size(); i++) {
CoIotDescrBlk blk = descr.blk.get(i);
out.beginArray();
out.value(blk.id);
out.value(blk.desc);
out.endArray();
}
out.endArray();
out.name(COIOT_TAG_SEN).beginArray();
for (int i = 0; i < descr.sen.size(); i++) {
// Create element, e.g. {“I”:66, “D”:“lux”, “T”:“L”, “R”:“0/100000”, “L”:1},
CoIotDescrSen sen = descr.sen.get(i);
out.beginArray();
out.value(sen.id);
out.value(sen.desc);
out.value(sen.type);
out.value(sen.range);
out.value(sen.links);
if (sen.unit != null) {
out.value(sen.unit);
}
out.endArray();
}
out.endArray();
}
out.endObject();
}
}
protected static class CoIotSensorTypeAdapter extends TypeAdapter<CoIotGenericSensorList> {
@Override
public CoIotGenericSensorList read(final JsonReader in) throws IOException {
CoIotGenericSensorList list = new CoIotGenericSensorList();
in.beginObject();
String generic = in.nextName();
if (generic.equals(COIOT_TAG_GENERIC)) {
in.beginArray();
while (in.hasNext()) {
CoIotSensor sensor = new CoIotSensor();
in.beginArray();
in.nextInt(); // alway 0
sensor.id = Integer.toString(in.nextInt());
JsonToken token = in.peek();
if (token == JsonToken.STRING) {
// handle as string
sensor.valueStr = in.nextString();
sensor.value = -1;
} else if (token == JsonToken.NUMBER) {
// handle as double
sensor.value = in.nextDouble();
sensor.valueStr = "";
} else if (token == JsonToken.BEGIN_ARRAY) {
sensor.valueArray = new ArrayList<>();
in.beginArray();
while (in.hasNext()) {
if (in.peek() == JsonToken.STRING) {
sensor.valueArray.add(in.nextString());
} else {
// skip
in.nextNull();
}
}
in.endArray();
}
in.endArray();
list.generic.add(sensor);
}
in.endArray();
}
in.endObject();
return list;
}
@Override
public void write(final JsonWriter out, final CoIotGenericSensorList o) throws IOException {
CoIotGenericSensorList sensors = o;
out.beginObject();
if (sensors != null) {
out.name(COIOT_TAG_GENERIC).beginArray();
for (int i = 0; i < sensors.generic.size(); i++) {
out.beginArray();
out.value(0);
out.value(sensors.generic.get(i).id);
out.value(sensors.generic.get(i).value);
out.endArray();
}
out.endArray();
}
out.endObject();
}
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 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.coap;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link ShellyCoapListener} describes the listening interface to process Coap responses
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public interface ShellyCoapListener {
public void processResponse(@Nullable Response response);
}

View File

@@ -0,0 +1,145 @@
/**
* Copyright (c) 2010-2020 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.coap;
import static org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.COIOT_PORT;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Set;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.CoAP.Code;
import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.core.network.Exchange;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.californium.elements.UdpMulticastConnector;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ShellyCoapServer} implements the UDP listener and status event processor (for /cit/s messages)
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyCoapServer {
private final Logger logger = LoggerFactory.getLogger(ShellyCoapServer.class);
boolean started = false;
private CoapEndpoint statusEndpoint = new CoapEndpoint.Builder().build();
private @Nullable UdpMulticastConnector statusConnector;
private final CoapServer server = new CoapServer(NetworkConfig.getStandard(), COIOT_PORT);;
private final Set<ShellyCoapListener> coapListeners = new ConcurrentHashSet<>();
protected class ShellyStatusListener extends CoapResource {
private ShellyCoapServer listener;
public ShellyStatusListener(String uri, ShellyCoapServer listener) {
super(uri, true);
getAttributes().setTitle("ShellyCoapListener");
this.listener = listener;
}
@Override
public void handleRequest(@Nullable final Exchange exchange) {
if (exchange != null) {
Request request = exchange.getRequest();
Code code = exchange.getRequest().getCode();
switch (code) {
case CUSTOM_30:
listener.processResponse(createResponse(request));
break;
default:
super.handleRequest(exchange);
}
}
}
}
public synchronized void start(String localIp, ShellyCoapListener listener) throws UnknownHostException {
if (!started) {
logger.debug("Initializing CoIoT listener (local IP={}:{})", localIp, COIOT_PORT);
NetworkConfig nc = NetworkConfig.getStandard();
InetAddress localAddr = InetAddress.getByName(localIp);
InetSocketAddress localPort = new InetSocketAddress(COIOT_PORT);
// Join the multicast group on the selected network interface
statusConnector = new UdpMulticastConnector(localAddr, localPort, CoAP.MULTICAST_IPV4); // bind UDP listener
statusEndpoint = new CoapEndpoint.Builder().setNetworkConfig(nc).setConnector(statusConnector).build();
server.addEndpoint(statusEndpoint);
CoapResource cit = new ShellyStatusListener("cit", this);
CoapResource s = new ShellyStatusListener("s", this);
cit.add(s);
server.add(cit);
started = true;
}
if (!coapListeners.contains(listener)) {
coapListeners.add(listener);
}
}
protected void processResponse(Response response) {
coapListeners.forEach(listener -> listener.processResponse(response));
}
public static Response createResponse(Request request) {
Response response = Response.createResponse(request, ResponseCode.CONTENT);
response.setType(request.getType());
response.setSourceContext(request.getSourceContext());
response.setMID(request.getMID());
response.setOptions(request.getOptions());
response.setPayload(request.getPayload());
return response;
}
@Nullable
public CoapEndpoint getEndpoint() {
return statusEndpoint;
}
/**
* Cancel pending requests and shutdown the client
*/
public void stop(ShellyCoapListener listener) {
coapListeners.remove(listener);
if (coapListeners.isEmpty()) {
stop();
}
}
private synchronized void stop() {
if (started) {
// Last listener
server.stop();
statusEndpoint.stop();
coapListeners.clear();
started = false;
logger.debug("CoAP Listener stopped");
}
}
public void dispose() {
stop();
}
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2020 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.config;
import java.util.Collections;
import java.util.Dictionary;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ShellyBindingConfiguration} class contains fields mapping binding configuration parameters.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyBindingConfiguration {
// Binding Configuration Properties
public static final String CONFIG_DEF_HTTP_USER = "defaultUserId";
public static final String CONFIG_DEF_HTTP_PWD = "defaultPassword";
public static final String CONFIG_AUTOCOIOT = "autoCoIoT";
public String defaultUserId = ""; // default for http basic user id
public String defaultPassword = ""; // default for http basic auth password
public boolean autoCoIoT = true;
public void updateFromProperties(Map<String, Object> properties) {
for (Map.Entry<String, Object> e : properties.entrySet()) {
if (e.getValue() == null) {
continue;
}
switch (e.getKey()) {
case CONFIG_DEF_HTTP_USER:
defaultUserId = (String) e.getValue();
break;
case CONFIG_DEF_HTTP_PWD:
defaultPassword = (String) e.getValue();
break;
case CONFIG_AUTOCOIOT:
autoCoIoT = (boolean) e.getValue();
break;
}
}
}
public void updateFromProperties(Dictionary<String, Object> properties) {
List<String> keys = Collections.list(properties.keys());
Map<String, Object> dictCopy = keys.stream().collect(Collectors.toMap(Function.identity(), properties::get));
updateFromProperties(dictCopy);
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ShellyThingConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyThingConfiguration {
public String deviceIp = ""; // ip address of thedevice
public String userId = ""; // userid for http basic auth
public String password = ""; // password for http basic auth
public int updateInterval = 60; // schedule interval for the update job
public int lowBattery = 15; // threshold for battery value
public boolean brightnessAutoOn = true; // true: turn on device if brightness > 0 is set
public boolean eventsButton = false; // true: register for Relay btn_xxx events
public boolean eventsSwitch = true; // true: register for device out_xxx events
public boolean eventsPush = true; // true: register for short/long push events
public boolean eventsRoller = true; // true: register for short/long push events
public boolean eventsSensorReport = true; // true: register for sensor events
public boolean eventsCoIoT = false; // true: use CoIoT events (based on COAP)
public String localIp = ""; // local ip addresses used to create callback url
public String localPort = "8080";
}

View File

@@ -0,0 +1,220 @@
/**
* Copyright (c) 2010-2020 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.discovery;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import static org.openhab.core.thing.Thing.PROPERTY_MODEL_ID;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
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.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class identifies Shelly devices by their mDNS service information.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@Component(service = MDNSDiscoveryParticipant.class, immediate = true)
public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(ShellyDiscoveryParticipant.class);
private final ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
private final ShellyTranslationProvider messages;
private final HttpClient httpClient;
private final ConfigurationAdmin configurationAdmin;
/**
* OSGI Service Activation
*
* @param componentContext
* @param localeProvider
*/
@Activate
public ShellyDiscoveryParticipant(@Reference ConfigurationAdmin configurationAdmin,
@Reference HttpClientFactory httpClientFactory, @Reference LocaleProvider localeProvider,
@Reference TranslationProvider i18nProvider, ComponentContext componentContext) {
logger.debug("Activating ShellyDiscovery service");
this.configurationAdmin = configurationAdmin;
this.messages = new ShellyTranslationProvider(componentContext.getBundleContext().getBundle(), i18nProvider,
localeProvider);
this.httpClient = httpClientFactory.getCommonHttpClient();
bindingConfig.updateFromProperties(componentContext.getProperties());
}
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES_UIDS;
}
@Override
public String getServiceType() {
return SERVICE_TYPE;
}
/**
* Process updates to Binding Config
*
* @param componentContext
*/
@Modified
protected void modified(final ComponentContext componentContext) {
logger.debug("Shelly Binding Configuration refreshed");
bindingConfig.updateFromProperties(componentContext.getProperties());
}
@Nullable
@Override
public DiscoveryResult createResult(final ServiceInfo service) {
String name = service.getName().toLowerCase(); // Duao: Name starts with" Shelly" rather than "shelly"
if (!name.startsWith("shelly")) {
return null;
}
String address = "";
try {
String mode = "";
String model = "unknown";
String deviceName = "";
ThingUID thingUID = null;
ShellyDeviceProfile profile = null;
Map<String, Object> properties = new TreeMap<>();
name = service.getName().toLowerCase();
address = service.getHostAddress();
if ((address == null) || address.isEmpty()) {
logger.trace("{}: Shelly device discovered with empty IP address (service-name={})", name, service);
return null;
}
String thingType = service.getQualifiedName().contains(SERVICE_TYPE) && name.contains("-")
? substringBeforeLast(name, "-")
: name;
logger.debug("{}: Shelly device discovered: IP-Adress={}, type={}", name, address, thingType);
// Get device settings
Configuration serviceConfig = configurationAdmin.getConfiguration("binding.shelly");
if (serviceConfig.getProperties() != null) {
bindingConfig.updateFromProperties(serviceConfig.getProperties());
}
ShellyThingConfiguration config = new ShellyThingConfiguration();
config.deviceIp = address;
config.userId = bindingConfig.defaultUserId;
config.password = bindingConfig.defaultPassword;
try {
ShellyHttpApi api = new ShellyHttpApi(name, config, httpClient);
profile = api.getDeviceProfile(thingType);
logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
deviceName = getString(profile.settings.name);
model = getString(profile.settings.device.type);
mode = profile.mode;
properties = ShellyBaseHandler.fillDeviceProperties(profile);
logger.trace("{}: thingType={}, deviceType={}, mode={}, symbolic name={}", name, thingType,
profile.deviceType, mode.isEmpty() ? "<standard>" : mode, deviceName);
// get thing type from device name
thingUID = ShellyThingCreator.getThingUID(name, model, mode, false);
} catch (ShellyApiException e) {
ShellyApiResult result = e.getApiResult();
if (result.isHttpAccessUnauthorized()) {
logger.info("{}: {}", name, messages.get("discovery.protected", address));
// create shellyunknown thing - will be changed during thing initialization with valid credentials
thingUID = ShellyThingCreator.getThingUID(name, model, mode, true);
} else {
logger.info("{}: {}", name, messages.get("discovery.failed", address, e.toString()));
logger.debug("{}: Discovery failed", name, e);
}
} catch (IllegalArgumentException e) { // maybe some format description was buggy
logger.debug("{}: Discovery failed!", name, e);
}
if (thingUID != null) {
addProperty(properties, CONFIG_DEVICEIP, address);
addProperty(properties, PROPERTY_MODEL_ID, model);
addProperty(properties, PROPERTY_SERVICE_NAME, name);
addProperty(properties, PROPERTY_DEV_NAME, deviceName);
addProperty(properties, PROPERTY_DEV_TYPE, thingType);
addProperty(properties, PROPERTY_DEV_MODE, mode);
logger.debug("{}: Adding Shelly {}, UID={}", name, deviceName, thingUID.getAsString());
String thingLabel = deviceName.isEmpty() ? name + " - " + address
: deviceName + " (" + name + "@" + address + ")";
return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(thingLabel)
.withRepresentationProperty(name).build();
}
} catch (IOException | NullPointerException e) {
// maybe some format description was buggy
logger.debug("{}: Exception on processing serviceInfo '{}'", name, service.getNiceTextString(), e);
}
return null;
}
private void addProperty(Map<String, Object> properties, String key, @Nullable String value) {
properties.put(key, value != null ? value : "");
}
@Nullable
@Override
public ThingUID getThingUID(@Nullable ServiceInfo service) throws IllegalArgumentException {
logger.debug("ServiceInfo {}", service);
if (service == null) {
throw new IllegalArgumentException("service must not be null!");
}
String serviceName = service.getName();
if (serviceName == null) {
throw new IllegalArgumentException("serviceName must not be null!");
}
serviceName = serviceName.toLowerCase();
if (!serviceName.contains(VENDOR.toLowerCase())) {
logger.debug("Not a " + VENDOR + " device!");
return null;
}
return ShellyThingCreator.getThingUID(serviceName, "", "", false);
}
}

View File

@@ -0,0 +1,130 @@
/**
* Copyright (c) 2010-2020 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.discovery;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.LinkedHashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
/**
* The {@link ShellyThingCreator} maps the device id into the thing type id
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyThingCreator {
private static final Map<String, String> THING_TYPE_MAPPING = new LinkedHashMap<>();
static {
// mapping by thing type
THING_TYPE_MAPPING.put(SHELLYDT_1PM, THING_TYPE_SHELLY1PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_1, THING_TYPE_SHELLY1_STR);
THING_TYPE_MAPPING.put(SHELLYDT_3EM, THING_TYPE_SHELLY3EM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_EM, THING_TYPE_SHELLYEM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_GAS, THING_TYPE_SHELLYGAS_STR);
THING_TYPE_MAPPING.put(SHELLYDT_DW, THING_TYPE_SHELLYDOORWIN_STR);
THING_TYPE_MAPPING.put(SHELLYDT_DW2, THING_TYPE_SHELLYDOORWIN2_STR);
THING_TYPE_MAPPING.put(SHELLYDT_DUO, THING_TYPE_SHELLYDUO_STR);
THING_TYPE_MAPPING.put(SHELLYDT_BULB, THING_TYPE_SHELLYBULB_STR);
THING_TYPE_MAPPING.put(SHELLYDT_VINTAGE, THING_TYPE_SHELLYVINTAGE_STR);
THING_TYPE_MAPPING.put(SHELLYDT_DIMMER, THING_TYPE_SHELLYDIMMER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_DIMMER2, THING_TYPE_SHELLYDIMMER2_STR);
THING_TYPE_MAPPING.put(SHELLYDT_IX3, THING_TYPE_SHELLYIX3_STR);
THING_TYPE_MAPPING.put(SHELLYDT_BUTTON1, THING_TYPE_SHELLYBUTTON1_STR);
// mapping by thing type
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY1_STR, THING_TYPE_SHELLY1_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY1PM_STR, THING_TYPE_SHELLY1PM_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY4PRO_STR, THING_TYPE_SHELLY4PRO_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDIMMER2_STR, THING_TYPE_SHELLYDIMMER2_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDIMMER_STR, THING_TYPE_SHELLYDIMMER_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYIX3_STR, THING_TYPE_SHELLYIX3_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY3EM_STR, THING_TYPE_SHELLY3EM_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYEM_STR, THING_TYPE_SHELLYEM_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDUO_STR, THING_TYPE_SHELLYDUO_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYVINTAGE_STR, THING_TYPE_SHELLYVINTAGE_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYBULB_STR, THING_TYPE_SHELLYBULB_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDUO_STR, THING_TYPE_SHELLYDUO_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYHT_STR, THING_TYPE_SHELLYHT_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYSMOKE_STR, THING_TYPE_SHELLYSMOKE_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYGAS_STR, THING_TYPE_SHELLYGAS_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYFLOOD_STR, THING_TYPE_SHELLYFLOOD_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDOORWIN_STR, THING_TYPE_SHELLYDOORWIN_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDOORWIN2_STR, THING_TYPE_SHELLYDOORWIN2_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYSENSE_STR, THING_TYPE_SHELLYSENSE_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYEYE_STR, THING_TYPE_SHELLYEYE_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYBUTTON1_STR, THING_TYPE_SHELLYBUTTON1_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPROTECTED_STR, THING_TYPE_SHELLYPROTECTED_STR);
}
public static ThingUID getThingUID(String serviceName, String deviceType, String mode, boolean unknown) {
String devid = substringAfterLast(serviceName, "-");
if (devid.isEmpty()) {
throw new IllegalArgumentException("serviceName has improper format: " + serviceName);
}
return new ThingUID(!unknown ? getThingTypeUID(serviceName, deviceType, mode)
: getThingTypeUID(THING_TYPE_SHELLYPROTECTED_STR + "-" + devid, deviceType, mode), devid);
}
public static ThingTypeUID getThingTypeUID(String serviceName, String deviceType, String mode) {
return new ThingTypeUID(BINDING_ID, getThingType(serviceName, deviceType, mode));
}
public static ThingTypeUID getUnknownTTUID() {
return new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPROTECTED_STR);
}
public static String getThingType(String hostname, String deviceType, String mode) {
String name = hostname.toLowerCase();
String type = substringBefore(name, "-").toLowerCase();
String devid = substringAfterLast(name, "-");
if ((devid == null) || (type == null)) {
throw new IllegalArgumentException("Invalid device name format: " + hostname);
}
// First check for special handling
if (name.startsWith(THING_TYPE_SHELLY25_PREFIX)) { // Shelly v2.5
return mode.equals(SHELLY_MODE_RELAY) ? THING_TYPE_SHELLY25_RELAY_STR : THING_TYPE_SHELLY25_ROLLER_STR;
}
if (name.startsWith(THING_TYPE_SHELLY2_PREFIX)) { // Shelly v2
return mode.equals(SHELLY_MODE_RELAY) ? THING_TYPE_SHELLY2_RELAY_STR : THING_TYPE_SHELLY2_ROLLER_STR;
}
if (name.startsWith(THING_TYPE_SHELLYPLUG_STR)) {
// shellyplug-s needs to be mapped to shellyplugs to follow the schema
// for the thing types: <thing type>-<mode>
if (name.startsWith(THING_TYPE_SHELLYPLUGS_STR) || name.contains("-s")) {
return THING_TYPE_SHELLYPLUGS_STR;
}
return THING_TYPE_SHELLYPLUG_STR;
}
if (name.startsWith(THING_TYPE_SHELLYRGBW2_PREFIX)) {
return mode.equals(SHELLY_MODE_COLOR) ? THING_TYPE_SHELLYRGBW2_COLOR_STR : THING_TYPE_SHELLYRGBW2_WHITE_STR;
}
// Check general mapping
if (!deviceType.isEmpty() && THING_TYPE_MAPPING.containsKey(deviceType)) {
return THING_TYPE_MAPPING.get(deviceType);
}
if (THING_TYPE_MAPPING.containsKey(type)) {
return THING_TYPE_MAPPING.get(type);
}
return THING_TYPE_SHELLYUNKNOWN_STR;
}
}

View File

@@ -0,0 +1,393 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsEMeter;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsMeter;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link ShellyCHANNEL_DEFINITIONSDTO} defines channel information for dynamically created channels. Those will be
* added on the first thing status update
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyChannelDefinitionsDTO {
private static final ChannelMap CHANNEL_DEFINITIONS = new ChannelMap();
// shortcuts to avoid line breaks (make code more readable)
private static final String CHGR_DEVST = CHANNEL_GROUP_DEV_STATUS;
private static final String CHGR_RELAY = CHANNEL_GROUP_RELAY_CONTROL;
private static final String CHGR_ROLLER = CHANNEL_GROUP_ROL_CONTROL;
private static final String CHGR_STATUS = CHANNEL_GROUP_STATUS;
private static final String CHGR_METER = CHANNEL_GROUP_METER;
private static final String CHGR_SENSOR = CHANNEL_GROUP_SENSOR;
private static final String CHGR_BAT = CHANNEL_GROUP_BATTERY;
public static final String ITEM_TYPE_NUMBER = "Number";
public static final String ITEM_TYPE_STRING = "String";
public static final String ITEM_TYPE_SWITCH = "Switch";
public static final String ITEM_TYPE_CONTACT = "Contact";
public static final String ITEM_TYPE_DATETIME = "DateTime";
public static final String ITEM_TYPE_TEMP = "Number:Temperature";
public static final String ITEM_TYPE_LUX = "Number:Illuminance";
public static final String ITEM_TYPE_POWER = "Number:Power";
public static final String ITEM_TYPE_ENERGY = "Number:Energy";
public static final String ITEM_TYPE_VOLT = "Number:ElectricPotential";
public static final String ITEM_TYPE_AMP = "Number:ElectricPotential";
public static final String ITEM_TYPE_PERCENT = "Number:Dimensionless";
public static final String ITEM_TYPE_ANGLE = "Number:Angle";
public static final String PREFIX_GROUP = "definitions.shelly.group.";
public static final String PREFIX_CHANNEL = "channel-type.shelly.";
public static final String SUFFIX_LABEL = ".label";
public static final String SUFFIX_DESCR = ".description";
public ShellyChannelDefinitionsDTO(ShellyTranslationProvider m) {
// Device: Internal Temp
CHANNEL_DEFINITIONS
// Device
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_NAME, "deviceName", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ITEMP, "deviceTemp", ITEM_TYPE_TEMP))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_WAKEUP, "sensorWakeup", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ACCUWATTS, "meterAccuWatts", ITEM_TYPE_POWER))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ACCUTOTAL, "meterAccuTotal", ITEM_TYPE_POWER))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ACCURETURNED, "meterAccuReturned", ITEM_TYPE_POWER))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_CHARGER, "charger", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_LED_STATUS_DISABLE, "ledStatusDisable", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_LED_POWER_DISABLE, "ledPowerDisable", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_SELFTTEST, "selfTest", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_UPTIME, "uptime", ITEM_TYPE_NUMBER))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_HEARTBEAT, "heartBeat", ITEM_TYPE_DATETIME))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_UPDATE, "updateAvailable", ITEM_TYPE_SWITCH))
// Relay
.add(new ShellyChannel(m, CHGR_RELAY, CHANNEL_OUTPUT_NAME, "outputName", ITEM_TYPE_STRING))
// Roller
.add(new ShellyChannel(m, CHGR_ROLLER, CHANNEL_ROL_CONTROL_STATE, "rollerState", ITEM_TYPE_STRING))
// RGBW2
.add(new ShellyChannel(m, CHANNEL_GROUP_LIGHT_CONTROL, CHANNEL_INPUT, "inputState", ITEM_TYPE_SWITCH))
// Power Meter
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_METER_CURRENTWATTS, "meterWatts", ITEM_TYPE_POWER))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_METER_TOTALKWH, "meterTotal", ITEM_TYPE_ENERGY))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_METER_LASTMIN1, "lastPower1", ITEM_TYPE_ENERGY))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_LAST_UPDATE, "lastUpdate", ITEM_TYPE_DATETIME))
// EMeter
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_TOTALRET, "meterReturned", ITEM_TYPE_ENERGY))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_REACTWATTS, "meterReactive", ITEM_TYPE_POWER))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_VOLTAGE, "meterVoltage", ITEM_TYPE_VOLT))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_CURRENT, "meterCurrent", ITEM_TYPE_AMP))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_PFACTOR, "meterPowerFactor", ITEM_TYPE_NUMBER))
// Sensors
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_TEMP, "sensorTemp", ITEM_TYPE_TEMP))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_HUM, "sensorHumidity", ITEM_TYPE_PERCENT))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_LUX, "sensorLux", ITEM_TYPE_LUX))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_ILLUM, "sensorIllumination", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_CONTACT, "sensorContact", ITEM_TYPE_CONTACT))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_SSTATE, "sensorState", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_VIBRATION, "sensorVibration", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_TILT, "sensorTilt", ITEM_TYPE_ANGLE))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_MOTION, "sensorMotion", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_FLOOD, "sensorFlood", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_SMOKE, "sensorSmoke", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_PPM, "sensorPPM", ITEM_TYPE_NUMBER))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_VALVE, "sensorValve", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_ALARM_STATE, "alarmState", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_ERROR, "sensorError", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_LAST_UPDATE, "lastUpdate", ITEM_TYPE_DATETIME))
// Button/ix3
.add(new ShellyChannel(m, CHGR_STATUS, CHANNEL_INPUT, "inputState", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_STATUS, CHANNEL_STATUS_EVENTTYPE, "eventType", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_STATUS, CHANNEL_STATUS_EVENTCOUNT, "eventCount", ITEM_TYPE_NUMBER))
.add(new ShellyChannel(m, CHGR_STATUS, CHANNEL_BUTTON_TRIGGER, "system.button", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_STATUS, CHANNEL_LAST_UPDATE, "lastUpdate", ITEM_TYPE_DATETIME))
// Addon with external sensors
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP1, "sensorExtTemp", ITEM_TYPE_TEMP))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP2, "sensorExtTemp", ITEM_TYPE_TEMP))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP3, "sensorExtTemp", ITEM_TYPE_TEMP))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_ESENDOR_HUMIDITY, "sensorExtHum", ITEM_TYPE_PERCENT))
// Battery
.add(new ShellyChannel(m, CHGR_BAT, CHANNEL_SENSOR_BAT_LEVEL, "system:battery-level",
ITEM_TYPE_PERCENT))
.add(new ShellyChannel(m, CHGR_BAT, CHANNEL_SENSOR_BAT_LOW, "system:low-battery", ITEM_TYPE_SWITCH));
}
public static ShellyChannel getDefinition(String channelName) throws IllegalArgumentException {
String group = substringBefore(channelName, "#");
String channel = substringAfter(channelName, "#");
if (group.contains(CHANNEL_GROUP_METER)) {
group = CHANNEL_GROUP_METER; // map meter1..n to meter
} else if (group.contains(CHANNEL_GROUP_RELAY_CONTROL)) {
group = CHANNEL_GROUP_RELAY_CONTROL; // map meter1..n to meter
} else if (group.contains(CHANNEL_GROUP_LIGHT_CHANNEL)) {
group = CHANNEL_GROUP_LIGHT_CHANNEL;
} else if (group.contains(CHANNEL_GROUP_STATUS)) {
group = CHANNEL_GROUP_STATUS; // map status1..n to meter
}
String channelId = group + "#" + channel;
return CHANNEL_DEFINITIONS.get(channelId);
}
/**
* Auto-create relay channels depending on relay type/mode
*
* @return ArrayList<Channel> of channels to be added to the thing
*/
public static Map<String, Channel> createDeviceChannels(final Thing thing, final ShellyDeviceProfile profile,
final ShellySettingsStatus status) {
Map<String, Channel> add = new LinkedHashMap<>();
addChannel(thing, add, profile.settings.name != null, CHGR_DEVST, CHANNEL_DEVST_NAME);
if (!profile.isSensor) {
// Only some devices report the internal device temp
addChannel(thing, add, (status.tmp != null) || (status.temperature != null), CHGR_DEVST,
CHANNEL_DEVST_ITEMP);
}
// RGBW2
addChannel(thing, add, status.input != null, CHANNEL_GROUP_LIGHT_CONTROL, CHANNEL_INPUT);
// 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
&& !profile.isRGBW2) || ((status.emeters != null && status.emeters.size() > 1)));
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);
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
}
return add;
}
/**
* Auto-create relay channels depending on relay type/mode
*
* @return ArrayList<Channel> of channels to be added to the thing
*/
public static Map<String, Channel> createRelayChannels(final Thing thing, final ShellyDeviceProfile profile,
final ShellyStatusRelay relay, int idx) {
Map<String, Channel> add = new LinkedHashMap<>();
String group = profile.getControlGroup(idx);
ShellySettingsRelay rs = profile.settings.relays.get(idx);
addChannel(thing, add, rs.name != null, group, CHANNEL_OUTPUT_NAME);
// Shelly 1/1PM Addon
if (relay.extTemperature != null) {
addChannel(thing, add, relay.extTemperature.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP1);
addChannel(thing, add, relay.extTemperature.sensor2 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP2);
addChannel(thing, add, relay.extTemperature.sensor3 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP3);
}
if (relay.extHumidity != null) {
addChannel(thing, add, relay.extHumidity.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENDOR_HUMIDITY);
}
return add;
}
public static Map<String, Channel> createRollerChannels(Thing thing, final ShellyControlRoller roller) {
Map<String, Channel> add = new LinkedHashMap<>();
addChannel(thing, add, roller.state != null, CHGR_ROLLER, CHANNEL_ROL_CONTROL_STATE);
return add;
}
public static Map<String, Channel> createMeterChannels(Thing thing, final ShellySettingsMeter meter, String group) {
Map<String, Channel> newChannels = new LinkedHashMap<>();
addChannel(thing, newChannels, meter.power != null, group, CHANNEL_METER_CURRENTWATTS);
addChannel(thing, newChannels, meter.total != null, group, CHANNEL_METER_TOTALKWH);
if (meter.counters != null) {
addChannel(thing, newChannels, meter.counters[0] != null, group, CHANNEL_METER_LASTMIN1);
}
addChannel(thing, newChannels, meter.timestamp != null, group, CHANNEL_LAST_UPDATE);
return newChannels;
}
public static Map<String, Channel> createEMeterChannels(final Thing thing, final ShellySettingsEMeter emeter,
String group) {
Map<String, Channel> newChannels = new LinkedHashMap<>();
addChannel(thing, newChannels, emeter.power != null, group, CHANNEL_METER_CURRENTWATTS);
addChannel(thing, newChannels, emeter.total != null, group, CHANNEL_METER_TOTALKWH);
addChannel(thing, newChannels, emeter.totalReturned != null, group, CHANNEL_EMETER_TOTALRET);
addChannel(thing, newChannels, emeter.reactive != null, group, CHANNEL_EMETER_REACTWATTS);
addChannel(thing, newChannels, emeter.voltage != null, group, CHANNEL_EMETER_VOLTAGE);
addChannel(thing, newChannels, emeter.current != null, group, CHANNEL_EMETER_CURRENT);
addChannel(thing, newChannels, emeter.pf != null, group, CHANNEL_EMETER_PFACTOR);
addChannel(thing, newChannels, true, group, CHANNEL_LAST_UPDATE);
return newChannels;
}
public static Map<String, Channel> createSensorChannels(final Thing thing, final ShellyDeviceProfile profile,
final ShellyStatusSensor sdata) {
Map<String, Channel> newChannels = new LinkedHashMap<>();
// Sensor data
addChannel(thing, newChannels, sdata.tmp != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP);
addChannel(thing, newChannels, sdata.hum != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM);
addChannel(thing, newChannels, sdata.lux != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX);
if (sdata.accel != null) {
addChannel(thing, newChannels, sdata.accel.vibration != null, CHANNEL_GROUP_SENSOR,
CHANNEL_SENSOR_VIBRATION);
addChannel(thing, newChannels, sdata.accel.tilt != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT);
}
addChannel(thing, newChannels, sdata.flood != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD);
addChannel(thing, newChannels, sdata.smoke != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD);
addChannel(thing, newChannels, sdata.lux != null && sdata.lux.illumination != null, CHANNEL_GROUP_SENSOR,
CHANNEL_SENSOR_ILLUM);
addChannel(thing, newChannels, sdata.contact != null && sdata.contact.state != null, CHANNEL_GROUP_SENSOR,
CHANNEL_SENSOR_CONTACT);
addChannel(thing, newChannels, sdata.motion != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION);
addChannel(thing, newChannels, sdata.charger != null, CHGR_DEVST, CHANNEL_DEVST_CHARGER);
addChannel(thing, newChannels, sdata.sensorError != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR);
addChannel(thing, newChannels, sdata.actReasons != null, CHGR_DEVST, CHANNEL_DEVST_WAKEUP);
// Gas
if (sdata.gasSensor != null) {
addChannel(thing, newChannels, sdata.gasSensor.selfTestState != null, CHGR_DEVST, CHANNEL_DEVST_SELFTTEST);
addChannel(thing, newChannels, sdata.gasSensor.sensorState != null, CHANNEL_GROUP_SENSOR,
CHANNEL_SENSOR_SSTATE);
addChannel(thing, newChannels, sdata.concentration != null && sdata.concentration.ppm != null,
CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM);
addChannel(thing, newChannels, sdata.valves != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE);
addChannel(thing, newChannels, sdata.gasSensor.sensorState != null, CHANNEL_GROUP_SENSOR,
CHANNEL_SENSOR_ALARM_STATE);
}
// Battery
if (sdata.bat != null) {
addChannel(thing, newChannels, sdata.bat.value != null, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL);
addChannel(thing, newChannels, sdata.bat.value != null, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW);
}
addChannel(thing, newChannels, true, profile.getControlGroup(0), CHANNEL_LAST_UPDATE);
return newChannels;
}
private static void addChannel(Thing thing, Map<String, Channel> newChannels, boolean supported, String group,
String channelName) throws IllegalArgumentException {
if (supported) {
final String channelId = group + "#" + channelName;
final ShellyChannel channelDef = getDefinition(channelId);
final ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
final ChannelTypeUID channelTypeUID = channelDef.typeId.contains("system:")
? new ChannelTypeUID(channelDef.typeId)
: new ChannelTypeUID(BINDING_ID, channelDef.typeId);
Channel channel = ChannelBuilder.create(channelUID, channelDef.itemType).withType(channelTypeUID).build();
newChannels.put(channelId, channel);
}
}
public class ShellyChannel {
private final ShellyTranslationProvider messages;
public String group = "";
public String groupLabel = "";
public String groupDescription = "";
public String channel = "";
public String label = "";
public String description = "";
public String itemType = "";
public String typeId = "";
public String category = "";
public Set<String> tags = new HashSet<>();
public ShellyChannel(ShellyTranslationProvider messages, String group, String channel, String typeId,
String itemType, String... category) {
this.messages = messages;
this.group = group;
this.channel = channel;
this.itemType = itemType;
this.typeId = typeId;
groupLabel = getText(PREFIX_GROUP + group + SUFFIX_LABEL);
groupDescription = getText(PREFIX_GROUP + group + SUFFIX_DESCR);
label = getText(PREFIX_CHANNEL + channel + SUFFIX_LABEL);
description = getText(PREFIX_CHANNEL + channel + SUFFIX_DESCR);
}
public String getChanneId() {
return group + "#" + channel;
}
private String getText(String key) {
String text = messages.get(key);
return text != null ? text : "";
}
}
public static class ChannelMap {
private final Map<String, ShellyChannel> map = new LinkedHashMap<>();
private ChannelMap add(ShellyChannel def) {
map.put(def.getChanneId(), def);
return this;
}
public ShellyChannel get(String channelName) throws IllegalArgumentException {
ShellyChannel def = null;
if (channelName.contains("#")) {
def = map.get(channelName);
}
for (HashMap.Entry<String, ShellyChannel> entry : map.entrySet()) {
if (entry.getValue().channel.contains("#" + channelName)) {
def = entry.getValue();
break;
}
}
if (def == null) {
throw new IllegalArgumentException("Channel definition for " + channelName + " not found!");
}
return def;
}
}
}

View File

@@ -0,0 +1,186 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
/**
* The {@link ShellyColorUtils} provides some utility functions around RGBW handling.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyColorUtils {
OnOffType power = OnOffType.OFF;
String mode = "";
int red = 0;
int green = 0;
int blue = 0;
int white = 0;
PercentType percentRed = new PercentType(0);
PercentType percentGreen = new PercentType(0);
PercentType percentBlue = new PercentType(0);
PercentType percentWhite = new PercentType(0);
int gain = 0;
int brightness = 0;
int temp = 0;
int minTemp = 0;
int maxTemp = 0;
PercentType percentGain = new PercentType(0);
PercentType percentBrightness = new PercentType(0);
PercentType percentTemp = new PercentType(0);
Integer effect = 0;
public ShellyColorUtils() {
}
public ShellyColorUtils(ShellyColorUtils col) {
minTemp = col.minTemp;
maxTemp = col.maxTemp;
setRed(col.red);
setGreen(col.green);
setBlue(col.blue);
setWhite(col.white);
setGain(col.gain);
setBrightness(col.brightness);
setTemp(col.temp);
}
void setMode(String mode) {
this.mode = mode;
}
void setMinMaxTemp(int min, int max) {
minTemp = min;
maxTemp = max;
}
boolean setRGBW(int red, int green, int blue, int white) {
setRed(red);
setGreen(green);
setBlue(blue);
setWhite(white);
return true;
}
boolean setRed(int value) {
boolean changed = red != value;
red = value;
percentRed = toPercent(red);
return changed;
}
boolean setGreen(int value) {
boolean changed = green != value;
green = value;
percentGreen = toPercent(green);
return changed;
}
boolean setBlue(int value) {
boolean changed = blue != value;
blue = value;
percentBlue = toPercent(blue);
return changed;
}
boolean setWhite(int value) {
boolean changed = white != value;
white = value;
percentWhite = toPercent(white);
return changed;
}
boolean setBrightness(int value) {
boolean changed = brightness != value;
brightness = value;
percentBrightness = toPercent(brightness, SHELLY_MIN_BRIGHTNESS, SHELLY_MAX_BRIGHTNESS);
return changed;
}
boolean setGain(int value) {
boolean changed = gain != value;
gain = value;
percentGain = toPercent(gain, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN);
return changed;
}
boolean setTemp(int value) {
boolean changed = temp != value;
temp = value;
percentTemp = toPercent(temp, minTemp, maxTemp);
return changed;
}
boolean setEffect(int value) {
boolean changed = effect != value;
effect = value;
return changed;
}
public HSBType toHSB() {
return HSBType.fromRGB(red, green, blue);
}
public Integer[] fromRGBW(String rgbwString) {
Integer values[] = new Integer[4];
values[0] = values[1] = values[2] = values[3] = -1;
try {
String rgbw[] = rgbwString.split(",");
for (int i = 0; i < rgbw.length; i++) {
values[i] = Integer.parseInt(rgbw[i]);
}
} catch (NullPointerException e) { // might be a format problem
throw new IllegalArgumentException(
"Unable to convert fullColor value: " + rgbwString + ", " + e.getMessage());
}
if (values[0] != -1) {
setRed(values[0]);
}
if (values[1] != -1) {
setGreen(values[1]);
}
if (values[2] != -1) {
setBlue(values[2]);
}
if (values[3] != -1) {
setWhite(values[3]);
}
return values;
}
public static PercentType toPercent(Integer value) {
return toPercent(value, 0, SHELLY_MAX_COLOR);
}
public static PercentType toPercent(Integer _value, Integer min, Integer max) {
Double range = max.doubleValue() - min.doubleValue();
Double value = _value.doubleValue();
value = value < min ? min.doubleValue() : value;
value = value > max ? max.doubleValue() : value;
Double percent = 0.0;
if (range > 0) {
percent = new Double(Math.round((value - min) / range * 100));
}
return new PercentType(new BigDecimal(percent));
}
}

View File

@@ -0,0 +1,354 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsEMeter;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsMeter;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import tec.uom.se.unit.Units;
/***
* The{@link ShellyComponents} implements updates for supplemental components
* Meter will be used by Relay + Light; Sensor is part of H&T, Flood, Door Window, Sense
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyComponents {
/**
* Update device status
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
*/
public static boolean updateDeviceStatus(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
if (!thingHandler.areChannelsCreated()) {
thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
.createDeviceChannels(thingHandler.getThing(), thingHandler.getProfile(), status));
}
Integer rssi = getInteger(status.wifiSta.rssi);
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPTIME,
toQuantityType(new Double(getLong(status.uptime)), DIGITS_NONE, SmartHomeUnits.SECOND));
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI, mapSignalStrength(rssi));
if (status.tmp != null) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
} else if (status.temperature != null) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(getDouble(status.temperature), DIGITS_NONE, SIUnits.CELSIUS));
}
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate));
return false; // device status never triggers update
}
/**
* Update Meter channel
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
* @param status Last ShellySettingsStatus
*/
public static boolean updateMeters(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
ShellyDeviceProfile profile = thingHandler.getProfile();
double accumulatedWatts = 0.0;
double accumulatedTotal = 0.0;
double accumulatedReturned = 0.0;
boolean updated = false;
// Devices without power meters get no updates
// We need to differ
// Roler+RGBW2 have multiple meters -> aggregate consumption to the functional device
// Meter and EMeter have a different set of channels
if ((profile.numMeters > 0) && ((status.meters != null) || (status.emeters != null))) {
if (!profile.isRoller && !profile.isRGBW2) {
thingHandler.logger.trace("{}: Updating {} {}meter(s)", thingHandler.thingName, profile.numMeters,
!profile.isEMeter ? "standard " : "e-");
// In Relay mode we map eacher meter to the matching channel group
int m = 0;
if (!profile.isEMeter) {
for (ShellySettingsMeter meter : status.meters) {
Integer meterIndex = m + 1;
if (getBool(meter.isValid) || profile.isLight) { // RGBW2-white doesn't report valid flag
// correctly in white mode
String groupName = "";
if (profile.numMeters > 1) {
groupName = CHANNEL_GROUP_METER + meterIndex.toString();
} else {
groupName = CHANNEL_GROUP_METER;
}
if (!thingHandler.areChannelsCreated()) {
// skip for Shelly Bulb: JSON has a meter, but values don't get updated
if (!profile.isBulb) {
thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
.createMeterChannels(thingHandler.getThing(), meter, groupName));
}
}
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
toQuantityType(getDouble(meter.power), DIGITS_WATT, SmartHomeUnits.WATT));
accumulatedWatts += getDouble(meter.power);
// convert Watt/Min to kw/h
if (meter.total != null) {
double kwh = getDouble(meter.total) / 60 / 1000;
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
toQuantityType(kwh, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
accumulatedTotal += kwh;
}
if (meter.counters != null) {
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
toQuantityType(getDouble(meter.counters[0]), DIGITS_WATT, SmartHomeUnits.WATT));
}
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
getTimestamp(getString(profile.settings.timezone), getLong(meter.timestamp)));
}
m++;
}
} else {
for (ShellySettingsEMeter emeter : status.emeters) {
Integer meterIndex = m + 1;
if (getBool(emeter.isValid)) {
String groupName = profile.numMeters > 1 ? CHANNEL_GROUP_METER + meterIndex.toString()
: CHANNEL_GROUP_METER;
if (!thingHandler.areChannelsCreated()) {
thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
.createEMeterChannels(thingHandler.getThing(), emeter, groupName));
}
// convert Watt/Hour tok w/h
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
toQuantityType(getDouble(emeter.power), DIGITS_WATT, SmartHomeUnits.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH, toQuantityType(
getDouble(emeter.total) / 1000, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET, toQuantityType(
getDouble(emeter.totalReturned) / 1000, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_REACTWATTS,
toQuantityType(getDouble(emeter.reactive), DIGITS_WATT, SmartHomeUnits.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_VOLTAGE,
toQuantityType(getDouble(emeter.voltage), DIGITS_VOLT, SmartHomeUnits.VOLT));
if (emeter.current != null) {
// Shelly
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_CURRENT,
toQuantityType(getDouble(emeter.current), DIGITS_VOLT, SmartHomeUnits.AMPERE));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_PFACTOR,
getDecimal(emeter.pf));
}
accumulatedWatts += getDouble(emeter.power);
accumulatedTotal += getDouble(emeter.total) / 1000;
accumulatedReturned += getDouble(emeter.totalReturned) / 1000;
if (updated) {
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
}
}
m++;
}
}
} else {
// In Roller Mode we accumulate all meters to a single set of meters
thingHandler.logger.trace("{}: Updating Meter (accumulated)", thingHandler.thingName);
double currentWatts = 0.0;
double totalWatts = 0.0;
double lastMin1 = 0.0;
long timestamp = 0l;
String groupName = CHANNEL_GROUP_METER;
for (ShellySettingsMeter meter : status.meters) {
if (meter.isValid) {
currentWatts += getDouble(meter.power);
totalWatts += getDouble(meter.total);
if (meter.counters != null) {
lastMin1 += getDouble(meter.counters[0]);
}
if (getLong(meter.timestamp) > timestamp) {
timestamp = getLong(meter.timestamp); // newest one
}
}
}
// Create channels for 1 Meter
if (!thingHandler.areChannelsCreated()) {
thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
.createMeterChannels(thingHandler.getThing(), status.meters.get(0), groupName));
}
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
toQuantityType(getDouble(lastMin1), DIGITS_WATT, SmartHomeUnits.WATT));
// convert totalWatts into kw/h
totalWatts = totalWatts / (60.0 * 10000.0);
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
toQuantityType(getDouble(currentWatts), DIGITS_WATT, SmartHomeUnits.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
toQuantityType(getDouble(totalWatts), DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
if (updated) {
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
getTimestamp(getString(profile.settings.timezone), timestamp));
}
}
if (updated && !profile.isRoller && !profile.isRGBW2) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS,
toQuantityType(accumulatedWatts, DIGITS_WATT, SmartHomeUnits.WATT));
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL,
toQuantityType(accumulatedTotal, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCURETURNED,
toQuantityType(accumulatedReturned, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
}
}
return updated;
}
/**
* Update Sensor channel
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
* @param status Last ShellySettingsStatus
*
* @throws IOException
*/
public static boolean updateSensors(ShellyBaseHandler thingHandler, ShellySettingsStatus status)
throws ShellyApiException {
ShellyDeviceProfile profile = thingHandler.getProfile();
boolean updated = false;
if (profile.isSensor || profile.hasBattery || profile.isSense) {
ShellyStatusSensor sdata = thingHandler.api.getSensorStatus();
if (!thingHandler.areChannelsCreated()) {
thingHandler.logger.trace("{}: Create missing sensor channel(s)", thingHandler.thingName);
thingHandler.updateChannelDefinitions(
ShellyChannelDefinitionsDTO.createSensorChannels(thingHandler.getThing(), profile, sdata));
}
updated |= thingHandler.updateWakeupReason(sdata.actReasons);
if ((sdata.contact != null) && sdata.contact.isValid) {
// Shelly DW: “sensor”:{“state”:“open”, “is_valid”:true},
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
getString(sdata.contact.state).equalsIgnoreCase(SHELLY_API_DWSTATE_OPEN) ? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR,
getStringType(sdata.sensorError));
if (changed) {
thingHandler.postEvent(sdata.sensorError, true);
}
updated |= changed;
}
if ((sdata.tmp != null) && getBool(sdata.tmp.isValid)) {
thingHandler.logger.trace("{}: Updating temperature", thingHandler.thingName);
Double temp = getString(sdata.tmp.units).toUpperCase().equals(SHELLY_TEMP_CELSIUS)
? getDouble(sdata.tmp.tC)
: getDouble(sdata.tmp.tF);
if (getString(sdata.tmp.units).toUpperCase().equals(SHELLY_TEMP_FAHRENHEIT)) {
// convert Fahrenheit to Celsius
temp = ImperialUnits.FAHRENHEIT.getConverterTo(Units.CELSIUS).convert(temp).doubleValue();
}
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
toQuantityType(temp.doubleValue(), DIGITS_TEMP, SIUnits.CELSIUS));
}
if (sdata.hum != null) {
thingHandler.logger.trace("{}: Updating humidity", thingHandler.thingName);
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
toQuantityType(getDouble(sdata.hum.value), DIGITS_PERCENT, SmartHomeUnits.PERCENT));
}
if ((sdata.lux != null) && getBool(sdata.lux.isValid)) {
// “lux”:{“value”:30, “illumination”: “dark”, “is_valid”:true},
thingHandler.logger.trace("{}: Updating lux", thingHandler.thingName);
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
toQuantityType(getDouble(sdata.lux.value), DIGITS_LUX, SmartHomeUnits.LUX));
if (sdata.lux.illumination != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM,
getStringType(sdata.lux.illumination));
}
}
if (sdata.accel != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT, toQuantityType(
getDouble(sdata.accel.tilt.doubleValue()), DIGITS_NONE, SmartHomeUnits.DEGREE_ANGLE));
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
getInteger(sdata.accel.vibration) == 1 ? OnOffType.ON : OnOffType.OFF);
}
if (sdata.flood != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
getOnOff(sdata.flood));
}
if (sdata.smoke != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SMOKE,
getOnOff(sdata.smoke));
}
if (sdata.gasSensor != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
getStringType(sdata.gasSensor.selfTestState));
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
getStringType(sdata.gasSensor.alarmState));
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE,
getStringType(sdata.gasSensor.sensorState));
}
if ((sdata.concentration != null) && sdata.concentration.isValid) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM,
getDecimal(sdata.concentration.ppm));
}
if (sdata.bat != null) { // no update for Sense
thingHandler.logger.trace("{}: Updating battery", thingHandler.thingName);
updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
toQuantityType(getDouble(sdata.bat.value), DIGITS_PERCENT, SmartHomeUnits.PERCENT));
boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW,
getDouble(sdata.bat.value) < thingHandler.config.lowBattery ? OnOffType.ON : OnOffType.OFF);
updated |= changed;
if (changed && getDouble(sdata.bat.value) < thingHandler.config.lowBattery) {
thingHandler.postEvent(ALARM_TYPE_LOW_BATTERY, false);
}
}
if (sdata.motion != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
getOnOff(sdata.motion));
}
if (sdata.charger != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
getOnOff(sdata.charger));
}
updated |= thingHandler.updateInputs(status);
if (updated) {
thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
}
}
return updated;
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 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 java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link DeviceUpdateListener} can register on the {@link TradfriGatewayHandler} to be informed about details about
* devices.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public interface ShellyDeviceListener {
/**
* This method is called when new device information is received.
*/
public boolean onEvent(String ipAddress, String deviceName, String deviceIndex, String eventType,
Map<String, String> parameters);
}

View File

@@ -0,0 +1,512 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLightChannel;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ShellyLightHandler} handles light (Bulb, Duo and RGBW2) specific commands and status. All other commands
* will be routet of the ShellyBaseHandler.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyLightHandler extends ShellyBaseHandler {
private final Logger logger = LoggerFactory.getLogger(ShellyLightHandler.class);
private final Map<Integer, ShellyColorUtils> channelColors;
/**
* Constructor
*
* @param thing The thing passed by the HandlerFactory
* @param bindingConfig configuration of the binding
* @param coapServer coap server instance
* @param localIP local IP of the openHAB host
* @param httpPort port of the openHAB HTTP API
*/
public ShellyLightHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
int httpPort, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
channelColors = new TreeMap<>();
}
@Override
public void initialize() {
logger.debug("Thing is using {}", this.getClass());
super.initialize();
}
@Override
public boolean handleDeviceCommand(ChannelUID channelUID, Command command) throws IllegalArgumentException {
String groupName = getString(channelUID.getGroupId());
if (groupName.isEmpty()) {
throw new IllegalArgumentException("Empty groupName");
}
int lightId = getLightIdFromGroup(groupName);
logger.trace("{}: Execute command {} on channel {}, lightId={}", thingName, command, channelUID.getAsString(),
lightId);
try {
ShellyColorUtils oldCol = getCurrentColors(lightId);
oldCol.mode = profile.mode;
ShellyColorUtils col = new ShellyColorUtils(oldCol);
boolean update = true;
switch (channelUID.getIdWithoutGroup()) {
default: // non-bulb commands will be handled by the generic handler
return false;
case CHANNEL_LIGHT_POWER:
logger.debug("{}: Switch light {}", thingName, command);
api.setLightParm(lightId, SHELLY_LIGHT_TURN,
command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
col.power = (OnOffType) command;
requestUpdates(1, false);
update = false;
break;
case CHANNEL_LIGHT_COLOR_MODE:
logger.debug("{}: Select color mode {}", thingName, command);
col.setMode((OnOffType) command == OnOffType.ON ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
break;
case CHANNEL_COLOR_PICKER:
logger.debug("{}: Update colors from color picker", thingName);
update = handleColorPicker(profile, lightId, col, command);
break;
case CHANNEL_COLOR_FULL:
logger.debug("{}: Set colors to {}", thingName, command);
handleFullColor(col, command);
break;
case CHANNEL_COLOR_RED:
col.setRed(setColor(lightId, SHELLY_COLOR_RED, command, SHELLY_MAX_COLOR));
break;
case CHANNEL_COLOR_GREEN:
col.setGreen(setColor(lightId, SHELLY_COLOR_GREEN, command, SHELLY_MAX_COLOR));
break;
case CHANNEL_COLOR_BLUE:
col.setBlue(setColor(lightId, SHELLY_COLOR_BLUE, command, SHELLY_MAX_COLOR));
break;
case CHANNEL_COLOR_WHITE:
col.setWhite(setColor(lightId, SHELLY_COLOR_WHITE, command, SHELLY_MAX_COLOR));
break;
case CHANNEL_COLOR_GAIN:
col.setGain(setColor(lightId, SHELLY_COLOR_GAIN, command, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN));
break;
case CHANNEL_BRIGHTNESS: // only in white mode
if (profile.inColor && !profile.isBulb) {
logger.debug("{}: Not in white mode, brightness not available", thingName);
break;
}
int value = -1;
if (command instanceof OnOffType) { // Switch
logger.debug("{}: Switch light {}", thingName, command);
ShellyShortLightStatus light = api.setRelayTurn(lightId,
command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
col.power = getOnOff(light.ison);
col.setBrightness(light.brightness);
updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Switch", col.power);
updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Value",
toQuantityType(new Double(col.power == OnOffType.ON ? col.brightness : 0), DIGITS_NONE,
SmartHomeUnits.PERCENT));
update = false;
break;
}
if (command instanceof PercentType) {
Float percent = ((PercentType) command).floatValue();
value = percent.intValue(); // 0..100% = 0..100
logger.debug("{}: Set brightness to {}%/{}", thingName, percent, value);
} else if (command instanceof DecimalType) {
value = ((DecimalType) command).intValue();
logger.debug("{}: Set brightness to {} (Integer)", thingName, value);
}
if (value == 0) {
logger.debug("{}: Brightness=0 -> switch light OFF", thingName);
api.setRelayTurn(lightId, SHELLY_API_OFF);
update = false;
} else {
if (command instanceof IncreaseDecreaseType) {
ShellyShortLightStatus light = api.getLightStatus(lightId);
if (((IncreaseDecreaseType) command).equals(IncreaseDecreaseType.INCREASE)) {
value = Math.min(light.brightness + DIM_STEPSIZE, 100);
} else {
value = Math.max(light.brightness - DIM_STEPSIZE, 0);
}
logger.trace("{}: Change brightness from {} to {}", thingName, light.brightness, value);
}
validateRange("brightness", value, 0, 100);
logger.debug("{}: Changing brightness from {} to {}", thingName, oldCol.brightness, value);
col.setBrightness(value);
}
updateChannel(CHANNEL_GROUP_LIGHT_CONTROL, CHANNEL_LIGHT_POWER,
value > 0 ? OnOffType.ON : OnOffType.OFF);
break;
case CHANNEL_COLOR_TEMP:
Integer temp = -1;
if (command instanceof PercentType) {
logger.debug("{}: Set color temp to {}%", thingName, ((PercentType) command).floatValue());
Float percent = ((PercentType) command).floatValue() / 100;
temp = new DecimalType(col.minTemp + ((col.maxTemp - col.minTemp)) * percent).intValue();
logger.debug("{}: Converted color-temp {}% to {}K (from Percent to Integer)", thingName,
percent, temp);
} else if (command instanceof DecimalType) {
temp = ((DecimalType) command).intValue();
logger.debug("{}: Set color temp to {}K (Integer)", thingName, temp);
}
validateRange(CHANNEL_COLOR_TEMP, temp, col.minTemp, col.maxTemp);
col.setTemp(temp);
col.brightness = -1;
break;
case CHANNEL_COLOR_EFFECT:
Integer effect = ((DecimalType) command).intValue();
logger.debug("{}: Set color effect to {}", thingName, effect);
validateRange("effect", effect, SHELLY_MIN_EFFECT, SHELLY_MAX_EFFECT);
col.setEffect(effect.intValue());
}
if (update) {
// check for switching color mode
if (profile.isBulb && !col.mode.isEmpty() && !col.mode.equals(oldCol.mode)) {
logger.debug("{}: Color mode changed from {} to {}, set new mode", thingName, oldCol.mode,
col.mode);
api.setLightMode(col.mode);
}
// send changed colors to the device
sendColors(profile, lightId, oldCol, col, config.brightnessAutoOn);
}
return true;
} catch (ShellyApiException e) {
logger.debug("{}: Unable to handle command: {}", thingName, e.toString());
return false;
} catch (IllegalArgumentException e) {
logger.debug("{}: Unable to handle command", thingName, e);
return false;
}
}
private boolean handleColorPicker(ShellyDeviceProfile profile, Integer lightId, ShellyColorUtils col,
Command command) throws ShellyApiException {
boolean updated = false;
if (command instanceof HSBType) {
HSBType hsb = (HSBType) command;
logger.debug("HSB-Info={}, Hue={}, getRGB={}, toRGB={}/{}/{}", hsb, hsb.getHue(),
String.format("0x%08X", hsb.getRGB()), hsb.toRGB()[0], hsb.toRGB()[1], hsb.toRGB()[2]);
if (hsb.toString().contains("360,")) {
logger.trace("{}: need to fix the Hue value (360->0)", thingName);
HSBType fixHue = new HSBType(new DecimalType(0), hsb.getSaturation(), hsb.getBrightness());
hsb = fixHue;
}
col.setRed(getColorFromHSB(hsb.getRed()));
col.setBlue(getColorFromHSB(hsb.getBlue()));
col.setGreen(getColorFromHSB(hsb.getGreen()));
col.setBrightness(getColorFromHSB(hsb.getBrightness(), BRIGHTNESS_FACTOR));
// white, gain and temp are not part of the HSB color scheme
updated = true;
} else if (command instanceof PercentType) {
if (!profile.inColor || profile.isBulb) {
col.brightness = SHELLY_MAX_BRIGHTNESS * ((PercentType) command).intValue();
updated = true;
}
} else if (command instanceof OnOffType) {
logger.debug("{}: Switch light {}", thingName, command);
api.setLightParm(lightId, SHELLY_LIGHT_TURN,
(OnOffType) command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
col.power = (OnOffType) command;
} else if (command instanceof IncreaseDecreaseType) {
if (!profile.inColor || profile.isBulb) {
logger.debug("{}: {} brightness by {}", thingName, command, SHELLY_DIM_STEPSIZE);
PercentType percent = (PercentType) super.getChannelValue(CHANNEL_GROUP_COLOR_CONTROL,
CHANNEL_BRIGHTNESS);
if (percent != null) {
int currentBrightness = percent.intValue() * SHELLY_MAX_BRIGHTNESS;
int newBrightness = currentBrightness;
if (command == IncreaseDecreaseType.DECREASE) {
newBrightness = Math.max(currentBrightness - SHELLY_DIM_STEPSIZE, 0);
} else {
newBrightness = Math.min(currentBrightness + SHELLY_DIM_STEPSIZE, SHELLY_MAX_BRIGHTNESS);
}
col.brightness = newBrightness;
updated = currentBrightness != newBrightness;
}
}
}
return updated;
}
private boolean handleFullColor(ShellyColorUtils col, Command command) throws IllegalArgumentException {
String color = command.toString().toLowerCase();
if (color.contains(",")) {
col.fromRGBW(color);
} else if (color.equals(SHELLY_COLOR_RED)) {
col.setRGBW(SHELLY_MAX_COLOR, 0, 0, 0);
} else if (color.equals(SHELLY_COLOR_GREEN)) {
col.setRGBW(0, SHELLY_MAX_COLOR, 0, 0);
} else if (color.equals(SHELLY_COLOR_BLUE)) {
col.setRGBW(0, 0, SHELLY_MAX_COLOR, 0);
} else if (color.equals(SHELLY_COLOR_YELLOW)) {
col.setRGBW(SHELLY_MAX_COLOR, SHELLY_MAX_COLOR, 0, 0);
} else if (color.equals(SHELLY_COLOR_WHITE)) {
col.setRGBW(0, 0, 0, SHELLY_MAX_COLOR);
col.setMode(SHELLY_MODE_WHITE);
} else {
throw new IllegalArgumentException("Invalid full color selection: " + color);
}
col.setMode(color.equals(SHELLY_MODE_WHITE) ? SHELLY_MODE_WHITE : SHELLY_MODE_COLOR);
return true;
}
private ShellyColorUtils getCurrentColors(int lightId) {
ShellyColorUtils col;
if (!channelColors.containsKey(lightId)) {
col = new ShellyColorUtils(); // create a new entry
col.setMinMaxTemp(profile.minTemp, profile.maxTemp);
channelColors.put(lightId, col);
logger.trace("{}: Colors entry created for lightId {}", thingName, lightId);
} else {
col = channelColors.get(lightId);
logger.trace(
"{}: Colors loaded for lightId {}: power={}, RGBW={}/{}/{}/{}, gain={}, brightness={}, color temp={} (min={}, max={}",
thingName, lightId, col.power, col.red, col.green, col.blue, col.white, col.gain, col.brightness,
col.temp, col.minTemp, col.maxTemp);
}
return col;
}
@Override
public boolean updateDeviceStatus(ShellySettingsStatus genericStatus) throws ShellyApiException {
if (!profile.isInitialized()) {
logger.debug("{}: Device not yet initialized!", thingName);
return false;
}
if (!profile.isLight) {
logger.debug("{}: ERROR: Device is not a light. but class ShellyHandlerLight is called!", thingName);
}
ShellyStatusLight status = api.getLightStatus();
logger.trace("{}: Updating light status in {} mode, {} channel(s)", thingName, profile.mode,
status.lights.size());
// In white mode we have multiple channels
int lightId = 0;
boolean updated = false;
for (ShellyStatusLightChannel light : status.lights) {
Integer channelId = lightId + 1;
String controlGroup = buildControlGroupName(profile, channelId);
// The bulb has a combined channel set for color or white mode
// The RGBW2 uses 2 different thing types: color=1 channel, white=4 channel
if (profile.isBulb) {
updateChannel(CHANNEL_GROUP_LIGHT_CONTROL, CHANNEL_LIGHT_COLOR_MODE, getOnOff(profile.inColor));
}
ShellyColorUtils col = getCurrentColors(lightId);
col.power = getOnOff(light.ison);
// Channel control/timer
// ShellyStatusLightChannel light = status.lights.get(i);
updated |= updateChannel(controlGroup, CHANNEL_LIGHT_POWER, col.power);
updated |= updateChannel(controlGroup, CHANNEL_TIMER_AUTOON, getDecimal(light.autoOn));
updated |= updateChannel(controlGroup, CHANNEL_TIMER_AUTOOFF, getDecimal(light.autoOff));
updated |= updateInputs(controlGroup, genericStatus, lightId);
if (getBool(light.overpower)) {
postEvent(ALARM_TYPE_OVERPOWER, false);
}
if (profile.inColor) {
logger.trace("{}: update color settings", thingName);
col.setRGBW(getInteger(light.red), getInteger(light.green), getInteger(light.blue),
getInteger(light.white));
col.setGain(getInteger(light.gain));
col.setEffect(getInteger(light.effect));
String colorGroup = CHANNEL_GROUP_COLOR_CONTROL;
logger.trace("{}: Update channels for group {}: RGBW={}/{}/{}, in %:{}%/{}%/{}%, white={}%, gain={}%",
thingName, colorGroup, col.red, col.green, col.blue, col.percentRed, col.percentGreen,
col.percentBlue, col.percentWhite, col.percentGain);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_RED, col.percentRed);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_GREEN, col.percentGreen);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_BLUE, col.percentBlue);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_WHITE, col.percentWhite);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_GAIN, col.percentGain);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_EFFECT, new DecimalType(col.effect));
setFullColor(colorGroup, col);
logger.trace("{}: update {}.color picker", thingName, colorGroup);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_PICKER, col.toHSB());
}
if (!profile.inColor || profile.isBulb) {
String whiteGroup = buildWhiteGroupName(profile, channelId);
col.setBrightness(getInteger(light.brightness));
updated |= updateChannel(whiteGroup, CHANNEL_BRIGHTNESS + "$Switch", col.power);
updated |= updateChannel(whiteGroup, CHANNEL_BRIGHTNESS + "$Value",
toQuantityType(col.power == OnOffType.ON ? col.percentBrightness.doubleValue() : new Double(0),
DIGITS_NONE, SmartHomeUnits.PERCENT));
if ((profile.isBulb || profile.isDuo) && (light.temp != null)) {
col.setTemp(getInteger(light.temp));
updated |= updateChannel(whiteGroup, CHANNEL_COLOR_TEMP, col.percentTemp);
logger.trace("{}: update {}.color picker", thingName, whiteGroup);
updated |= updateChannel(whiteGroup, CHANNEL_COLOR_PICKER, col.toHSB());
}
}
// continue with next light
lightId++;
}
return updated;
}
private Integer setColor(Integer lightId, String colorName, Command command, Integer minValue, Integer maxValue)
throws ShellyApiException, IllegalArgumentException {
Integer value = -1;
logger.debug("{}: Set {} to {} ({})", thingName, colorName, command, command.getClass());
if (command instanceof PercentType) {
PercentType percent = (PercentType) command;
Double v = new Double(maxValue) * percent.doubleValue() / 100.0;
value = v.intValue();
logger.debug("{}: Value for {} is in percent: {}%={}", thingName, colorName, percent, value);
} else if (command instanceof DecimalType) {
value = ((DecimalType) command).intValue();
logger.debug("Value for {} is a number: {}", colorName, value);
} else if (command instanceof OnOffType) {
value = ((OnOffType) command).equals(OnOffType.ON) ? SHELLY_MAX_COLOR : SHELLY_MIN_COLOR;
logger.debug("{}: Value for {} of type OnOff was converted to {}", thingName, colorName, value);
} else {
throw new IllegalArgumentException(
"Invalid value type for " + colorName + ": " + value + " / type " + value.getClass());
}
validateRange(colorName, value, minValue, maxValue);
return value.intValue();
}
private Integer setColor(Integer lightId, String colorName, Command command, Integer maxValue)
throws ShellyApiException, IllegalArgumentException {
return setColor(lightId, colorName, command, 0, maxValue);
}
private void setFullColor(String colorGroup, ShellyColorUtils col) {
if ((col.red == SHELLY_MAX_COLOR) && (col.green == SHELLY_MAX_COLOR) && (col.blue == 0)) {
updateChannel(colorGroup, CHANNEL_COLOR_FULL, new StringType(SHELLY_COLOR_YELLOW));
} else if ((col.red == SHELLY_MAX_COLOR) && (col.green == 0) && (col.blue == 0)) {
updateChannel(colorGroup, CHANNEL_COLOR_FULL, new StringType(SHELLY_COLOR_RED));
} else if ((col.red == 0) && (col.green == SHELLY_MAX_COLOR) && (col.blue == 0)) {
updateChannel(colorGroup, CHANNEL_COLOR_FULL, new StringType(SHELLY_COLOR_GREEN));
} else if ((col.red == 0) && (col.green == 0) && (col.blue == SHELLY_MAX_COLOR)) {
updateChannel(colorGroup, CHANNEL_COLOR_FULL, new StringType(SHELLY_COLOR_BLUE));
} else if ((col.red == 0) && (col.green == 0) && (col.blue == 0) && (col.white == SHELLY_MAX_COLOR)) {
updateChannel(colorGroup, CHANNEL_COLOR_FULL, new StringType(SHELLY_COLOR_WHITE));
}
}
private void sendColors(ShellyDeviceProfile profile, Integer lightId, ShellyColorUtils oldCol,
ShellyColorUtils newCol, boolean autoOn) throws ShellyApiException {
// boolean updated = false;
Integer channelId = lightId + 1;
Map<String, String> parms = new TreeMap<>();
logger.trace(
"{}: New color settings for channel {}: RGB {}/{}/{}, white={}, gain={}, brightness={}, color-temp={}",
thingName, channelId, newCol.red, newCol.green, newCol.blue, newCol.white, newCol.gain,
newCol.brightness, newCol.temp);
if (autoOn && (newCol.brightness >= 0)) {
parms.put(SHELLY_LIGHT_TURN, profile.inColor || newCol.brightness > 0 ? SHELLY_API_ON : SHELLY_API_OFF);
}
if (profile.inColor) {
if (oldCol.red != newCol.red || oldCol.green != newCol.green || oldCol.blue != newCol.blue
|| oldCol.white != newCol.white) {
logger.debug("{}: Setting RGBW to {}/{}/{}/{}", thingName, newCol.red, newCol.green, newCol.blue,
newCol.white);
parms.put(SHELLY_COLOR_RED, String.valueOf(newCol.red));
parms.put(SHELLY_COLOR_GREEN, String.valueOf(newCol.green));
parms.put(SHELLY_COLOR_BLUE, String.valueOf(newCol.blue));
parms.put(SHELLY_COLOR_WHITE, String.valueOf(newCol.white));
}
}
if ((!profile.inColor) && (oldCol.temp != newCol.temp)) {
logger.debug("{}: Setting color temp to {}", thingName, newCol.temp);
parms.put(SHELLY_COLOR_TEMP, String.valueOf(newCol.temp));
}
if (oldCol.gain != newCol.gain) {
logger.debug("{}: Setting gain to {}", thingName, newCol.gain);
parms.put(SHELLY_COLOR_GAIN, String.valueOf(newCol.gain));
}
if ((newCol.brightness >= 0) && (!profile.inColor || profile.isBulb)
&& (oldCol.brightness != newCol.brightness)) {
logger.debug("{}: Setting brightness to {}", thingName, newCol.brightness);
parms.put(SHELLY_COLOR_BRIGHTNESS, String.valueOf(newCol.brightness));
}
if (!oldCol.effect.equals(newCol.effect)) {
logger.debug("{}: Setting effect to {}", thingName, newCol.effect);
parms.put(SHELLY_COLOR_EFFECT, newCol.effect.toString());
}
if (parms.size() > 0) {
logger.debug("{}: Send light settings: {}", thingName, parms);
api.setLightParms(lightId, parms);
updateCurrentColors(lightId, newCol);
}
}
private void updateCurrentColors(int lightId, ShellyColorUtils col) {
channelColors.replace(lightId, col);
logger.debug("{}: Colors updated for lightId {}: RGBW={}/{}/{}/{}, Sat/Gain={}, Bright={}, Temp={} ", thingName,
lightId, col.red, col.green, col.blue, col.white, col.gain, col.brightness, col.temp);
}
private Integer getColorFromHSB(PercentType colorPercent) {
return getColorFromHSB(colorPercent, new Double(SATURATION_FACTOR));
}
private Integer getColorFromHSB(PercentType colorPercent, Double factor) {
Double value = new Double(Math.round(colorPercent.doubleValue() * factor));
logger.trace("{}: convert {}% into {}/{} (factor={})", thingName, colorPercent, value, value.intValue(),
factor);
return value.intValue();
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 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.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ShellyProtectedHandler} implements a dummy handler for password protected devices.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyProtectedHandler extends ShellyBaseHandler {
private final Logger logger = LoggerFactory.getLogger(ShellyProtectedHandler.class);
/**
* Constructor
*
* @param thing The thing passed by the HandlerFactory
* @param bindingConfig configuration of the binding
* @param coapServer coap server instance
* @param localIP local IP of the openHAB host
* @param httpPort port of the openHAB HTTP API
*/
public ShellyProtectedHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
int httpPort, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
}
@Override
public void initialize() {
super.initialize();
}
}

View File

@@ -0,0 +1,463 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRoller;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortStatusRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/***
* The{@link ShellyRelayHandler} handles light (bulb+rgbw2) specific commands and status. All other commands will be
* handled by the generic thing handler.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyRelayHandler extends ShellyBaseHandler {
private final Logger logger = LoggerFactory.getLogger(ShellyRelayHandler.class);
/**
* Constructor
*
* @param thing The thing passed by the HandlerFactory
* @param bindingConfig configuration of the binding
* @param coapServer coap server instance
* @param localIP local IP of the openHAB host
* @param httpPort port of the openHAB HTTP API
*/
public ShellyRelayHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
int httpPort, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
}
@Override
public void initialize() {
super.initialize();
}
@Override
public boolean handleDeviceCommand(ChannelUID channelUID, Command command) throws ShellyApiException {
// Process command
String groupName = getString(channelUID.getGroupId());
Integer rIndex = 0;
if (groupName.startsWith(CHANNEL_GROUP_RELAY_CONTROL)
&& groupName.length() > CHANNEL_GROUP_RELAY_CONTROL.length()) {
rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_RELAY_CONTROL)) - 1;
} else if (groupName.startsWith(CHANNEL_GROUP_ROL_CONTROL)
&& groupName.length() > CHANNEL_GROUP_ROL_CONTROL.length()) {
rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_ROL_CONTROL)) - 1;
}
switch (channelUID.getIdWithoutGroup()) {
default:
return false;
case CHANNEL_OUTPUT:
if (!profile.isRoller) {
// extract relay number of group name (relay0->0, relay1->1...)
logger.debug("{}: Set relay output to {}", thingName, command);
api.setRelayTurn(rIndex, command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
} else {
logger.debug("{}: Device is in roller mode, channel command {} ignored", thingName, channelUID);
}
break;
case CHANNEL_BRIGHTNESS: // e.g.Dimmer, Duo
handleBrightness(command, rIndex);
break;
case CHANNEL_ROL_CONTROL_POS:
case CHANNEL_ROL_CONTROL_CONTROL:
logger.debug("{}: Roller command/position {}", thingName, command);
handleRoller(command, groupName, rIndex,
channelUID.getIdWithoutGroup().equals(CHANNEL_ROL_CONTROL_CONTROL));
// request updates the next 45sec to update roller position after it stopped
requestUpdates(autoCoIoT ? 1 : 45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
break;
case CHANNEL_TIMER_AUTOON:
logger.debug("{}: Set Auto-ON timer to {}", thingName, command);
api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command));
break;
case CHANNEL_TIMER_AUTOOFF:
logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command));
break;
}
return true;
}
/**
* PaperUI Control has a combined Slider for Brightness combined with On/Off
* Brightness channel has 2 functions: Switch On/Off (OnOnType) and setting brightness (PercentType)
* There is some more logic in the control. When brightness is set to 0 the control sends also an OFF command
* When current brightness is 0 and slider will be moved the new brightness will be set, but also a ON command is
* send.
*
* @param command
* @param index
* @throws ShellyApiException
*/
private void handleBrightness(Command command, Integer index) throws ShellyApiException {
Integer value = -1;
if (command instanceof PercentType) { // Dimmer
value = ((PercentType) command).intValue();
} else if (command instanceof DecimalType) { // Number
value = ((DecimalType) command).intValue();
} else if (command instanceof OnOffType) { // Switch
logger.debug("{}: Switch output {}", thingName, command);
updateBrightnessChannel(index, (OnOffType) command, value);
return;
} else if (command instanceof IncreaseDecreaseType) {
ShellyShortLightStatus light = api.getLightStatus(index);
if (((IncreaseDecreaseType) command).equals(IncreaseDecreaseType.INCREASE)) {
value = Math.min(light.brightness + DIM_STEPSIZE, 100);
} else {
value = Math.max(light.brightness - DIM_STEPSIZE, 0);
}
logger.debug("{}: Increase/Decrease brightness from {} to {}", thingName, light.brightness, value);
}
validateRange("brightness", value, 0, 100);
// Switch light off on brightness = 0
if (value == 0) {
logger.debug("{}: Brightness=0 -> switch output OFF", thingName);
updateBrightnessChannel(index, OnOffType.OFF, 0);
} else {
logger.debug("{}: Setting dimmer brightness to {}", thingName, value);
updateBrightnessChannel(index, OnOffType.ON, value);
}
}
private void updateBrightnessChannel(int lightId, OnOffType power, int brightness) throws ShellyApiException {
if (brightness > 0) {
api.setBrightness(lightId, brightness, config.brightnessAutoOn);
} else {
api.setRelayTurn(lightId, power == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
}
updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Switch", power);
updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Value", toQuantityType(
new Double(power == OnOffType.ON ? brightness : 0), DIGITS_NONE, SmartHomeUnits.PERCENT));
}
@Override
public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiException {
// map status to channels
boolean updated = false;
updated |= updateRelays(status);
updated |= updateDimmers(status);
updated |= updateLed(status);
return updated;
}
/**
* Handle Roller Commands
*
* @param command from handleCommand()
* @param groupName relay, roller...
* @param index relay number
* @param isControl true: is the Rollershutter channel, false: rollerpos channel
* @throws ShellyApiException
*/
private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
throws ShellyApiException {
Integer position = -1;
if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
ShellyControlRoller rstatus = api.getRollerStatus(index);
if (!getString(rstatus.state).isEmpty() && !getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_STOP)) {
boolean up = command instanceof UpDownType && (UpDownType) command == UpDownType.UP;
boolean down = command instanceof UpDownType && (UpDownType) command == UpDownType.DOWN;
if ((up && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_OPEN))
|| (down && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) {
logger.debug("{}: Roller is already moving ({}), ignore command {}", thingName,
getString(rstatus.state), command);
requestUpdates(1, false);
return;
}
}
if (((command instanceof UpDownType) && UpDownType.UP.equals(command))
|| ((command instanceof OnOffType) && OnOffType.ON.equals(command))) {
logger.debug("{}: Open roller", thingName);
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
position = SHELLY_MAX_ROLLER_POS;
}
if (((command instanceof UpDownType) && UpDownType.DOWN.equals(command))
|| ((command instanceof OnOffType) && OnOffType.OFF.equals(command))) {
logger.debug("{}: Closing roller", thingName);
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
position = SHELLY_MIN_ROLLER_POS;
}
} else if ((command instanceof StopMoveType) && StopMoveType.STOP.equals(command)) {
logger.debug("{}: Stop roller", thingName);
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_STOP);
} else {
logger.debug("{}: Set roller to position {}", thingName, command);
if (command instanceof PercentType) {
PercentType p = (PercentType) command;
position = p.intValue();
} else if (command instanceof DecimalType) {
DecimalType d = (DecimalType) command;
position = d.intValue();
} else {
throw new IllegalArgumentException(
"Invalid value type for roller control/posiution" + 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 1:1 from position channel
position = isControl ? SHELLY_MAX_ROLLER_POS - position : position;
validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS);
logger.debug("{}: Changing roller position to {}", thingName, position);
api.setRollerPos(index, position);
}
if (position != -1) {
// make sure both are in sync
if (isControl) {
int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS));
updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
} else {
updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
}
}
}
/**
* Auto-create relay channels depending on relay type/mode
*/
private void createRelayChannels(ShellyStatusRelay relay, int idx) {
if (!areChannelsCreated()) {
updateChannelDefinitions(ShellyChannelDefinitionsDTO.createRelayChannels(getThing(), profile, relay, idx));
}
}
private void createRollerChannels(ShellyControlRoller roller) {
if (!areChannelsCreated()) {
updateChannelDefinitions(ShellyChannelDefinitionsDTO.createRollerChannels(getThing(), roller));
}
}
/**
* Update Relay/Roller channels
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
* @param status Last ShellySettingsStatus
*
* @throws ShellyApiException
*/
public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
boolean updated = false;
// Check for Relay in Standard Mode
if (profile.hasRelays && !profile.isRoller && !profile.isDimmer) {
logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
int i = 0;
ShellyStatusRelay rstatus = api.getRelayStatus(i);
for (ShellyShortStatusRelay relay : rstatus.relays) {
createRelayChannels(rstatus, i);
if ((relay.isValid == null) || relay.isValid) {
String groupName = profile.getControlGroup(i);
ShellySettingsRelay rs = profile.settings.relays.get(i);
updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(rs.name));
if (getBool(relay.overpower)) {
postEvent(ALARM_TYPE_OVERPOWER, false);
}
updated |= updateChannel(groupName, CHANNEL_OUTPUT, getOnOff(relay.ison));
updated |= updateChannel(groupName, CHANNEL_TIMER_ACTIVE, getOnOff(relay.hasTimer));
if (rstatus.extTemperature != null) {
// Shelly 1/1PM support up to 3 external sensors
// for whatever reason those are not represented as an array, but 3 elements
if (rstatus.extTemperature.sensor1 != null) {
updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP1, toQuantityType(
getDouble(rstatus.extTemperature.sensor1.tC), DIGITS_TEMP, SIUnits.CELSIUS));
}
if (rstatus.extTemperature.sensor2 != null) {
updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP2, toQuantityType(
getDouble(rstatus.extTemperature.sensor2.tC), DIGITS_TEMP, SIUnits.CELSIUS));
}
if (rstatus.extTemperature.sensor3 != null) {
updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP3, toQuantityType(
getDouble(rstatus.extTemperature.sensor3.tC), DIGITS_TEMP, SIUnits.CELSIUS));
}
}
if ((rstatus.extHumidity != null) && (rstatus.extHumidity.sensor1 != null)) {
updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM, toQuantityType(
getDouble(rstatus.extHumidity.sensor1.hum), DIGITS_PERCENT, SmartHomeUnits.PERCENT));
}
// Update Auto-ON/OFF timer
ShellySettingsRelay rsettings = profile.settings.relays.get(i);
if (rsettings != null) {
updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
toQuantityType(getDouble(rsettings.autoOn), SmartHomeUnits.SECOND));
updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
toQuantityType(getDouble(rsettings.autoOff), SmartHomeUnits.SECOND));
}
// Update input(s) state
updated |= updateInputs(groupName, status, i);
}
i++;
}
}
// Check for Relay in Roller Mode
if (profile.hasRelays && profile.isRoller && (status.rollers != null)) {
logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
int i = 0;
for (ShellySettingsRoller roller : status.rollers) {
if (roller.isValid) {
ShellyControlRoller control = api.getRollerStatus(i);
Integer relayIndex = i + 1;
String groupName = profile.numRollers > 1 ? CHANNEL_GROUP_ROL_CONTROL + relayIndex.toString()
: CHANNEL_GROUP_ROL_CONTROL;
createRollerChannels(control);
if (control.name != null) {
updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(control.name));
}
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));
updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType(new Double(SHELLY_MAX_ROLLER_POS - pos), SmartHomeUnits.PERCENT));
updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
toQuantityType(new Double(pos), SmartHomeUnits.PERCENT));
scheduledUpdates = 1; // one more poll and then stop
}
updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STATE, new StringType(state));
updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STOPR, getStringType(control.stopReason));
updated |= updateInputs(groupName, status, i);
i++;
}
}
}
return updated;
}
/**
* Update Relay/Roller channels
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
* @param status Last ShellySettingsStatus
*
* @throws ShellyApiException
*/
public boolean updateDimmers(ShellySettingsStatus orgStatus) throws ShellyApiException {
boolean updated = false;
if (profile.isDimmer) {
// We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
// the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
// maps to a different structure (ShellyShortLight).
Gson gson = new Gson();
ShellySettingsStatus dstatus = gson.fromJson(ShellyApiJsonDTO.fixDimmerJson(orgStatus.json),
ShellySettingsStatus.class);
logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
int l = 0;
for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
Integer r = l + 1;
String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
: CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
// On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
// and send a OFF status to the same channel.
// When the device's brightness is > 0 we send the new value to the channel and a ON command
if (dimmer.ison) {
updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value", toQuantityType(
new Double(getInteger(dimmer.brightness)), DIGITS_NONE, SmartHomeUnits.PERCENT));
} else {
updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
toQuantityType(new Double(0), DIGITS_NONE, SmartHomeUnits.PERCENT));
}
ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
if (dsettings != null) {
updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
toQuantityType(getDouble(dsettings.autoOn), SmartHomeUnits.SECOND));
updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
toQuantityType(getDouble(dsettings.autoOff), SmartHomeUnits.SECOND));
}
updated |= updateInputs(groupName, orgStatus, l);
l++;
}
}
return updated;
}
/**
* Update LED channels
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
* @param status Last ShellySettingsStatus
*/
public boolean updateLed(ShellySettingsStatus status) {
boolean updated = false;
updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
getOnOff(profile.settings.ledStatusDisable));
updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
getOnOff(profile.settings.ledPowerDisable));
return updated;
}
}

View File

@@ -0,0 +1,136 @@
/**
* Copyright (c) 2010-2020 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.util;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.mkChannelId;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ShellyChannelCache} implements a caching layer for channel updates.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyChannelCache {
private final Logger logger = LoggerFactory.getLogger(ShellyChannelCache.class);
private final ShellyBaseHandler thingHandler;
private final Map<String, State> channelData = new ConcurrentHashMap<>();
private String thingName = "";
private boolean enabled = false;
public ShellyChannelCache(ShellyBaseHandler thingHandler) {
this.thingHandler = thingHandler;
setThingName(thingHandler.thingName);
}
public void setThingName(String thingName) {
this.thingName = thingName;
}
public boolean isEnabled() {
return enabled;
}
public void enable() {
enabled = true;
}
public synchronized void disable() {
clear();
enabled = false;
}
/**
* Update one channel. Use Channel Cache to avoid unnecessary updates (and avoid
* messing up the log with those updates)
*
* @param channelId Channel id
* @param value Value (State)
* @param forceUpdate true: ignore cached data, force update; false check cache of changed data
* @return true, if successful
*/
public boolean updateChannel(String channelId, State newValue, Boolean forceUpdate) {
try {
State current = null;
if (channelData.containsKey(channelId)) {
current = channelData.get(channelId);
}
if (!enabled || forceUpdate || (current == null) || !current.equals(newValue)) {
if ((current != null) && current.getClass().isEnum() && (current == newValue)) {
return false; // special case for OnOffType
}
// For channels that support multiple types (like brightness) a suffix is added
// this gets removed to get the channelId for updateState
thingHandler.publishState(channelId, newValue);
if (current == null) {
channelData.put(channelId, newValue);
} else {
channelData.replace(channelId, newValue);
}
logger.debug("{}: Channel {} updated with {} (type {}).", thingName, channelId, newValue,
newValue.getClass());
return true;
}
} catch (IllegalArgumentException e) {
logger.debug("{}: Unable to update channel {} with {} (type {}): {} ({})", thingName, channelId, newValue,
newValue.getClass(), ShellyUtils.getMessage(e), e.getClass());
}
return false;
}
public boolean updateChannel(String group, String channel, State value) {
return updateChannel(mkChannelId(group, channel), value, false);
}
public boolean updateChannel(String channelId, State value) {
return updateChannel(channelId, value, false);
}
/**
* Get a value from the Channel Cache
*
* @param group Channel Group
* @param channel Channel Name
* @return the data from that channel
*/
public State getValue(String group, String channel) {
return getValue(mkChannelId(group, channel));
}
public State getValue(String channelId) {
if (channelData.containsKey(channelId)) {
return channelData.get(channelId);
}
return UnDefType.NULL;
}
public void resetChannel(String channelId) {
channelData.remove(channelId);
}
public void clear() {
channelData.clear();
}
}

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2020 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.util;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.osgi.framework.Bundle;
/**
* {@link ShellyTranslationProvider} provides i18n message lookup
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyTranslationProvider {
private final Bundle bundle;
private final TranslationProvider i18nProvider;
private final LocaleProvider localeProvider;
public ShellyTranslationProvider(Bundle bundle, TranslationProvider i18nProvider, LocaleProvider localeProvider) {
this.bundle = bundle;
this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider;
}
public ShellyTranslationProvider(final ShellyTranslationProvider other) {
this.bundle = other.bundle;
this.i18nProvider = other.i18nProvider;
this.localeProvider = other.localeProvider;
}
public @Nullable 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) {
try {
Locale locale = localeProvider.getLocale();
return i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments);
} catch (IllegalArgumentException e) {
return "Unable to load message for key " + key;
}
}
public @Nullable String getDefaultText(String key) {
return i18nProvider.getText(bundle, key, key, Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,255 @@
/**
* Copyright (c) 2010-2020 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.util;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* {@link ShellyUtils} provides general utility functions
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyUtils {
public static String mkChannelId(String group, String channel) {
return group + "#" + channel;
}
public static String getString(@Nullable String value) {
return value != null ? value : "";
}
public static String substringBefore(@Nullable String string, String pattern) {
if (string != null) {
int pos = string.indexOf(pattern);
if (pos > 0) {
return string.substring(0, pos);
}
}
return "";
}
public static String substringBeforeLast(@Nullable String string, String pattern) {
if (string != null) {
int pos = string.lastIndexOf(pattern);
if (pos > 0) {
return string.substring(0, pos);
}
}
return "";
}
public static String substringAfter(@Nullable String string, String pattern) {
if (string != null) {
int pos = string.indexOf(pattern);
if (pos != -1) {
return string.substring(pos + pattern.length());
}
}
return "";
}
public static String substringAfterLast(@Nullable String string, String pattern) {
if (string != null) {
int pos = string.lastIndexOf(pattern);
if (pos != -1) {
return string.substring(pos + pattern.length());
}
}
return "";
}
public static String substringBetween(@Nullable String string, String begin, String end) {
if (string != null) {
int s = string.indexOf(begin);
if (s != -1) {
// The end tag might be included before the start tag, e.g.
// when using "http://" and ":" to get the IP from http://192.168.1.1:8081/xxx
// therefore make it 2 steps
String result = string.substring(s + begin.length());
return substringBefore(result, end);
}
}
return "";
}
public static String getMessage(Exception e) {
String message = e.getMessage();
return message != null ? message : "";
}
public static Integer getInteger(@Nullable Integer value) {
return (value != null ? (Integer) value : 0);
}
public static Long getLong(@Nullable Long value) {
return (value != null ? (Long) value : 0);
}
public static Double getDouble(@Nullable Double value) {
return (value != null ? (Double) value : 0);
}
public static Boolean getBool(@Nullable Boolean value) {
return (value != null ? (Boolean) value : false);
}
// as State
public static StringType getStringType(@Nullable String value) {
return new StringType(value != null ? value : "");
}
public static DecimalType getDecimal(@Nullable Double value) {
return new DecimalType((value != null ? value : 0));
}
public static DecimalType getDecimal(@Nullable Integer value) {
return new DecimalType((value != null ? value : 0));
}
public static DecimalType getDecimal(@Nullable Long value) {
return new DecimalType((value != null ? value : 0));
}
public static Double getNumber(Command command) throws IllegalArgumentException {
if (command instanceof DecimalType) {
return ((DecimalType) command).doubleValue();
}
if (command instanceof QuantityType) {
return ((QuantityType<?>) command).doubleValue();
}
throw new IllegalArgumentException("Unable to convert number");
}
public static OnOffType getOnOff(@Nullable Boolean value) {
return (value != null ? value ? OnOffType.ON : OnOffType.OFF : OnOffType.OFF);
}
public static OnOffType getOnOff(int value) {
return value == 0 ? OnOffType.OFF : OnOffType.ON;
}
public static State toQuantityType(@Nullable Double value, int digits, Unit<?> unit) {
if (value == null) {
return UnDefType.NULL;
}
BigDecimal bd = new BigDecimal(value.doubleValue());
return toQuantityType(bd.setScale(digits, BigDecimal.ROUND_HALF_UP), unit);
}
public static State toQuantityType(@Nullable Number value, Unit<?> unit) {
return value == null ? UnDefType.NULL : new QuantityType<>(value, unit);
}
public static State toQuantityType(@Nullable PercentType value, Unit<?> unit) {
return value == null ? UnDefType.NULL : toQuantityType(value.toBigDecimal(), unit);
}
public static void validateRange(String name, Integer value, int min, int max) {
if ((value < min) || (value > max)) {
throw new IllegalArgumentException("Value " + name + " is out of range (" + min + "-" + max + ")");
}
}
public static String urlEncode(String input) throws ShellyApiException {
try {
return URLEncoder.encode(input, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
throw new ShellyApiException(
"Unsupported encoding format: " + StandardCharsets.UTF_8.toString() + ", input=" + input, e);
}
}
public static Long now() {
return System.currentTimeMillis() / 1000L;
}
public static DateTimeType getTimestamp() {
return new DateTimeType(ZonedDateTime.ofInstant(Instant.ofEpochSecond(now()), ZoneId.systemDefault()));
}
public static DateTimeType getTimestamp(String zone, long timestamp) {
try {
if (timestamp == 0) {
return getTimestamp();
}
ZoneId zoneId = !zone.isEmpty() ? ZoneId.of(zone) : ZoneId.systemDefault();
ZonedDateTime zdt = LocalDateTime.now().atZone(zoneId);
int delta = zdt.getOffset().getTotalSeconds();
return new DateTimeType(ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp - delta), zoneId));
} catch (DateTimeException e) {
// Unable to convert device's timezone, use system one
return getTimestamp();
}
}
public static Integer getLightIdFromGroup(String groupName) {
if (groupName.startsWith(CHANNEL_GROUP_LIGHT_CHANNEL)) {
return Integer.parseInt(substringAfter(groupName, CHANNEL_GROUP_LIGHT_CHANNEL)) - 1;
}
return 0; // only 1 light, e.g. bulb or rgbw2 in color mode
}
public static String buildControlGroupName(ShellyDeviceProfile profile, Integer channelId) {
return profile.isBulb || profile.isDuo || profile.inColor ? CHANNEL_GROUP_LIGHT_CONTROL
: CHANNEL_GROUP_LIGHT_CHANNEL + channelId.toString();
}
public static String buildWhiteGroupName(ShellyDeviceProfile profile, Integer channelId) {
return profile.isBulb || profile.isDuo && !profile.inColor ? CHANNEL_GROUP_WHITE_CONTROL
: CHANNEL_GROUP_LIGHT_CHANNEL + channelId.toString();
}
public static DecimalType mapSignalStrength(int dbm) {
int strength = -1;
if (dbm > -60) {
strength = 4;
} else if (dbm > -70) {
strength = 3;
} else if (dbm > -80) {
strength = 2;
} else if (dbm > -90) {
strength = 1;
} else {
strength = 0;
}
return new DecimalType(strength);
}
}

View File

@@ -0,0 +1,168 @@
/**
* Copyright (c) 2010-2020 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.util;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* {@link ShellyVersionDTO} compares 2 version strings.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyVersionDTO {
private class VersionTokenizer {
private final String versionString;
private final int length;
private int position;
private int number;
private String suffix = "";
public VersionTokenizer(@Nullable String versionString) {
if (versionString == null) {
throw new IllegalArgumentException("versionString is null");
}
this.versionString = versionString;
length = versionString.length();
}
private boolean moveNext() {
number = 0;
suffix = "";
// No more characters
if (position >= length) {
return false;
}
while (position < length) {
char c = versionString.charAt(position);
if (c < '0' || c > '9') {
break;
}
number = number * 10 + (c - '0');
position++;
}
int suffixStart = position;
while (position < length) {
char c = versionString.charAt(position);
if (c == '.') {
break;
}
position++;
}
suffix = versionString.substring(suffixStart, position);
if (position < length) {
position++;
}
return true;
}
private int getNumber() {
return number;
}
private String getSuffix() {
return suffix;
}
}
public boolean equals(String s1, String s2) {
return compare(s1, s2) == 0;
}
public int compare(String version1, String version2) {
VersionTokenizer tokenizer1 = new VersionTokenizer(version1);
VersionTokenizer tokenizer2 = new VersionTokenizer(version2);
int number1 = 0, number2 = 0;
String suffix1 = "", suffix2 = "";
while (tokenizer1.moveNext()) {
if (!tokenizer2.moveNext()) {
do {
number1 = tokenizer1.getNumber();
suffix1 = tokenizer1.getSuffix();
if (number1 != 0 || suffix1.length() != 0) {
// Version one is longer than number two, and non-zero
return 1;
}
} while (tokenizer1.moveNext());
// Version one is longer than version two, but zero
return 0;
}
number1 = tokenizer1.getNumber();
suffix1 = tokenizer1.getSuffix();
number2 = tokenizer2.getNumber();
suffix2 = tokenizer2.getSuffix();
if (number1 < number2) {
// Number one is less than number two
return -1;
}
if (number1 > number2) {
// Number one is greater than number two
return 1;
}
boolean empty1 = suffix1.length() == 0;
boolean empty2 = suffix2.length() == 0;
if (empty1 && empty2) {
continue;
} // No suffixes
if (empty1) {
return 1;
} // First suffix is empty (1.2 > 1.2b)
if (empty2) {
return -1;
} // Second suffix is empty (1.2a < 1.2)
// Lexical comparison of suffixes
int result = suffix1.compareTo(suffix2);
if (result != 0) {
return result;
}
}
while (tokenizer2.moveNext()) {
number2 = tokenizer2.getNumber();
suffix2 = tokenizer2.getSuffix();
if (number2 != 0 || suffix2.length() != 0) {
// Version one is longer than version two, and non-zero
return -1;
}
}
// Version two is longer than version one, but zero
return 0;
}
public boolean checkBeta(@Nullable String version) {
if (version == null) {
return false;
}
return version.isEmpty() || version.contains("???") || version.toLowerCase().contains("master")
|| (version.toLowerCase().contains("-rc"));
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="shelly" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Shelly Binding</name>
<description>This binding supports the Shelly series of devices.</description>
<author>Markus Michels</author>
<config-description>
<parameter name="defaultUserId" type="text">
<default>admin</default>
<label>Default User</label>
<description>Default userId to access protected Shelly devices.</description>
</parameter>
<parameter name="defaultPassword" type="text">
<default>admin</default>
<label>Default Password</label>
<description>Default password to access protected Shelly devices.</description>
</parameter>
<parameter name="autoCoIoT" type="boolean">
<default>true</default>
<label>Auto-enable CoIoT</label>
<description>True: Enable CoIoT events by default when firmware 1.6+ is detected</description>
</parameter>
</config-description>
</binding:binding>

View File

@@ -0,0 +1,298 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:shelly:relay">
<parameter name="userId" type="text" required="false">
<label>User</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="eventsButton" type="boolean" required="false">
<label>Enable Button Events</label>
<description>True if the binding should register to get the Buttons Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsSwitch" type="boolean" required="false">
<label>Enable Switch Events (Out on/off)</label>
<description>True if the binding should register to get the Relay Switch Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsPush" type="boolean" required="false">
<label>Enable Push Events (short/long)</label>
<description>True if the binding should register to get the Short/Long Push Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:roller">
<parameter name="userId" type="text" required="false">
<label>User</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="eventsRoller" type="boolean" required="false">
<label>Enable Roller Events (Roller only)</label>
<description>True if the binding should register to get Roller Events.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:dimmer">
<parameter name="userId" type="text" required="false">
<label>User</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="brightnessAutoOn" type="boolean" required="false">
<label>Brightness Auto-ON</label>
<description>true: Turn device ON if brightness>0 is set; false: don't touch power status when brightness is set.</description>
<default>true</default>
</parameter>
<parameter name="eventsButton" type="boolean" required="false">
<label>Enable Button Events</label>
<description>True if the binding should register to get the Buttons Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsSwitch" type="boolean" required="false">
<label>Enable Switch Events (Out on/off)</label>
<description>True if the binding should register to get the Relay Switch Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsPush" type="boolean" required="false">
<label>Enable Push Events (short/long)</label>
<description>True if the binding should register to get the Short/Long Push Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:light">
<parameter name="userId" type="text" required="false">
<label>User</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="brightnessAutoOn" type="boolean" required="false">
<label>Brightness Auto-ON</label>
<description>true: Turn device ON if brightness > 0 is set; false: don't touch power status when brightness is set.</description>
<default>true</default>
</parameter>
<parameter name="eventsSwitch" type="boolean" required="false">
<label>Enable Switch Events (Out on/off)</label>
<description>True if the binding should register to get the Relay Switch Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:rgbw2">
<parameter name="userId" type="text" required="false">
<label>User</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="brightnessAutoOn" type="boolean" required="false">
<label>Brightness Auto-ON</label>
<description>true: Turn device ON if brightness > 0 is set; false: don't touch power status when brightness is set.</description>
<default>true</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:battery">
<parameter name="userId" type="text" required="false">
<label>User ID</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="lowBattery" type="integer" required="false">
<label>Low Battery Threshold</label>
<description>Threshold for the battery level, alert will be signed when battery level is below. Default: 20%</description>
<default>20</default>
<unitLabel>%</unitLabel>
</parameter>
<parameter name="eventsSensorReport" type="boolean" required="false">
<label>Enable Sensor Events</label>
<description>True: Register event URL for sensor updates.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>3600</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:basic">
<parameter name="userId" type="text" required="false">
<label>User ID</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="eventsSensorReport" type="boolean" required="false">
<label>Enable Sensor Events</label>
<description>True: Register event URL for sensor updates.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>3600</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,56 @@
discovery.failed# Config status messages
config-status.error.missing-device-ip=IP address of the Shelly device is missing.
# Thing status descriptions
offline.conf-error-no-credentials = Device is password protected, but no credentials have been configured.
offline.conf-error-access-denied = Access denied, check user id and password.
offline.conf-error-wrong-mode = Device is no longer in the configured device mode {0}, required {1}. Delete the thing and re-discover the device.
offline.status-error-timeout = Device is not reachable (API timeout).
offline.status-error-unexpected-api-result = An unexpected API response. Please verify the logfile to get more detailed information.
offline.status-error-watchdog = Device is not responding, seems to be unavailable.
offline.status-error-restarted = The device has restarted and will be re-initialized.
message.versioncheck.failed = Unable to check firmware version: {0}
message.versioncheck.beta = Device is running a Beta version: {0}/{1} ({2}),make sure this is newer than {3} release build.
message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1} ({2}), required minimal {3}.
message.versioncheck.update = INFO: New firmware available: current version: {0}, new version: {1}
message.versioncheck.autocoiot = INFO: Firmware is full-filling the minimum version to auto-enable CoIoT
message.roller.calibrating = Device is not calibrated, use Shelly App to perform initial roller calibration.
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.init.protected = Device is password protected, enter correct credentials in thing configuration.
message.command.failed = ERROR: Unable to process command {0} for channel {1}
message.command.init = Thing not yet initialized, command {0} triggers initialization
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.protected = Device {0} reported 'Access defined' (missing userid/password or incorrect).
message.discovery.failed = Device discovery of device with IP address {0} failed: {1}
# Device
channel-type.shelly.deviceName.label = Device Name
channel-type.shelly.deviceName.description = Symbolic Device Name as configured in the Shelly App.
# Relay, external sensors
channel-type.shelly.outputName.label = Output Name
channel-type.shelly.outputName.description = Output/Channel Name as configured in the Shelly App.
channel-type.shelly.temperature1.label = Temperature 1
channel-type.shelly.temperature1.description = Temperature of external Sensor #1
channel-type.shelly.temperature2.label = Temperature 2
channel-type.shelly.temperature2.description = Temperature of external Sensor #2
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%)
# Roller
channel-type.shelly.rollerState.label = State
channel-type.shelly.rollerState.description = State of the roller (open/closed/stopped).
# 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.ledStatusDisable.label = Disable Status LED
channel-type.shelly.ledStatusDisable.description = ON: The WiFi status LED will be decativated

View File

@@ -0,0 +1,582 @@
# binding
binding.shelly.name = Shelly Binding
binding.shelly.description = Dieses Binding integriert Shelly-Komponenten, die über WiFi gesteuert werden können.
# Config status messages
config-status.error.missing-deviceip=Die IP-Adresse des Shelly Gerätes ist nicht konfiguriert.
# Thing status descriptions
offline.conf-error-no-credentials = Gerät ist passwortgeschützt, aber es sind keine Anmeldedaten konfiguriert.
offline.conf-error-access-denied = Der Zugriff wurde verweigert. überprüfen Sie die konfigurierten Zugangsdaten, oder setzen diese entsprechend (analog zur Shelly App).
offline.conf-error-wrong-mode = Das Gerät befindet sich nicht mehr in der erwarteten Betriebsart. Löschen Sie das Gerät und führen Sie eine erneute Geräteerkennung durch.
offline.status-error-timeout = Das Gerät ist nicht erreichbar (API Timeout).
offline.status-error-unexpected-api-result = Es trat ein unerwartetes Problem beim API-Zugriff auf. Überprüfen Sie die Logdatei für genauere Informationen.
offline.status-error-watchdog = Das Gerät antwortet nicht und ist vermutlich nicht mehr verfügbar.
offline.status-error-restarted = Das Gerät wurde neu gestartet und wird erneut initialisiert.
# Status error messages
config-status.error.missing-userid = Keine Benutzerkennung in der Thing Konfiguration
# General messages
message.versioncheck.failed = Firmware-Version konnte nicht geprüft werden: {0}
message.versioncheck.beta = Es wurde eine Betaversion erkannt: {0}/{1} ({2}), bitte sicherstellen, dass diese neuer ist als Version {3} (Release Build).
message.versioncheck.tooold = ACHTUNG: Eine alter Firmware wurde erkannt: {0}/{1} ({2}), minimal erforderlich {3}.
message.versioncheck.update = INFO: Eine neue Firmwareversion ist verfügbar, aktuell: {0}, neu: {1}
message.versioncheck.autocoiot = INFO: Die Firmware unterstützt die Anforderung, Auto-CoIoT wurde aktiviert.
message.init.noipaddress = Es konnte keine lokale IP-Adresse ermittelt werden. Bitte sicherstellen, dass IPv4 aktiviert ist und das richtige Interface in der openHAB Netzwerk-Konfiguration ausgewählt ist.
message.init.protected = Das Gerät ist passwortgeschützt, die Zugangsdaten müssen in der Thing Konfiguration hinterlegt werden.
message.command.failed = FEHLER: Der Befehl {0} für Kanal {1} kann nicht verarbeitet werden
message.command.init = Thing aktuell nicht initialisiert, der Befehl {0} führt zur Initialisierung
message.statusupdate.failed = Status konnte nicht aktualisiert werden
message.event.triggered = Event erzeugt: {0}
message.coap.init.failed = CoAP/CoIoT konnte nicht gestartet werden: {0}
message.discovery.disabled = Das Gerät ist als "nicht erkennen" markiert und wird nicht übernommen.
message.discovery.protected = Das Gerät mit der IP-Adresse {0} ist zugriffsgeschützt und keine Zugangsdaten konfiguriert.
message.discovery.failed = Erkennung des Gerätes mit der IP-Adresse {0} ist fehlgeschlagen
message.roller.calibrating = Das Gerät ist nicht kalibriert. Es ist eine Kalibrierung mit der Shelly App erforderlich.
# thing types
thing-type.shelly.shelly1.label = Shelly1 (SHSW-1)
thing-type.shelly.shelly1.description = Shelly 1 (1 Relay)
thing-type.shelly.shelly1pm.label = Shelly 1PM (SHSW-PM)
thing-type.shelly.shelly1pm.description = Shelly 1PM mit 1xRelais und Strommesser
thing-type.shelly.shellyem.label = Shelly EM (SHEM)
thing-type.shelly.shellyem.description = Shelly EM zur Energiemessung
thing-type.shelly.shellyem3.label = Shelly 3EM (SHEM-3)
thing-type.shelly.shellyem3.description = Shelly 3EM zur Energiemessung
thing-type.shelly.shelly2-relay.label = Shelly2 Relay (SHSW-21)
thing-type.shelly.shelly2-relay.description = Shelly2 im Relais-Modus (2xRelais, Strommesser)
thing-type.shelly.shelly2-roller.label = Shelly2 Relay (SHSW-21)
thing-type.shelly.shelly2-roller.description = Shelly2 im Rollladen-Modus (1xRollladen, Strommesser)
thing-type.shelly.shelly25-relay.label = Shelly2.5 Relay (SHSW-25)
thing-type.shelly.shelly25-relay.description = Shelly2.5 im Relais-Modus (2xRelais, 2 Strommesser)
thing-type.shelly.shelly25-roller.label = Shelly2.5 Roller (SHSW-25)
thing-type.shelly.shelly25-roller.description = Shelly2.5 im Rollladen-Modus (1xRollladen, Strommesser)
thing-type.shelly.shelly4pro.label = Shelly4 Pro Relay (SHSW-4)
thing-type.shelly.shelly4pro.description = Shelly 4 Pro mit 4 Relais und Strommessern
thing-type.shelly.shellyplug.label = Shelly Plug (SHPLG)
thing-type.shelly.shellyplug.description = Shelly Plug als schaltbare Steckdose
thing-type.shelly.shellyplugs.label = Shelly Plug-S (SHPLG-S)
thing-type.shelly.shellyplugs.description = Shelly Plug-S als schaltbare Steckdose
thing-type.shelly.shellydimmer.label = Shelly Dimmer (SHDM-1)
thing-type.shelly.shellydimmer.description = Shelly mit Dimmer-Funktion
thing-type.shelly.shellydimmer2.label = Shelly Dimmer (SHDM-2)
thing-type.shelly.shellydimmer2.description = Shelly mit Dimmer-Funktion, 2. Generation
thing-type.shelly.shellybutton1.label = Shelly Button 1 (SHBTN-1)
thing-type.shelly.shellybutton1.description = Shelly Button 1 (batteriebetrieben)
thing-type.shelly.shellyht.label = Shelly H&T (SHHT-1)
thing-type.shelly.shellyht.description = Shelly H&amp;T Sensor
thing-type.shelly.shellydw.label = Shelly Door/Window Sensor (SHDW-1)
thing-type.shelly.shellydw.description = Shelly Tür/Fenstersensor (batteriebetrieben)
thing-type.shelly.shellydw2.label = Shelly Door/Window Sensor 2 (SHDW-2)
thing-type.shelly.shellydw2.description = Shelly Tür/Fenstersensor (batteriebetrieben)
thing-type.shelly.shellysmoke.label = Shelly Smoke
thing-type.shelly.shellysmoke.description = Shelly Rauchmelder
thing-type.shelly.shellyflood.label = Shelly Flood (SHWT-1)
thing-type.shelly.shellyflood.description = Shelly Wassermelder
thing-type.shelly.shellysense.label = Shelly Sense (SHSEN-1)
thing-type.shelly.shellysense.description = Shelly Bewegungsmelder
thing-type.shelly.shellybulb.label = Shelly Bulb (SHBLB-1)
thing-type.shelly.shellybulb.description = Shelly Glühbirne weiß/Farbe
thing-type.shelly.shellybulbduo.label = Shelly Duo (SHBDUO-1)
thing-type.shelly.shellybulbduo.description = Shelly Duo Glühbirne
thing-type.shelly.shellyvintage.label = Shelly Vintage (SHVIN-1)
thing-type.shelly.shellyvintage.description = Shelly Vintage Glühbirne
thing-type.shelly.shellyrgbw2-color.label = Shelly RGBW2 Color Mode (SHRGBW2)
thing-type.shelly.shellyrgbw2-color.description = Shelly RGBW-Controller im Farbmodus
thing-type.shelly.shellyrgbw2-white.label = Shelly RGBW2 White Mode (SHRGBW2)
thing-type.shelly.shellyrgbw2-white.description = Shelly RGBW-Controller im Weiß-Modus, 4 Streifen
# thing config - generic
thing-type.config.shelly.generic.userId.label = Benutzer
thing-type.config.shelly.generic.userId.description = Benutzerkennung für API-Zugriff
thing-type.config.shelly.generic.password.label = Passwort
thing-type.config.shelly.generic.password.description = Passwort für API-Zugriff
thing-type.config.shelly.generic.deviceIp.label = IP Adresse
thing-type.config.shelly.generic.deviceIp.description = IP Adresse der Shelly-Komponente
thing-type.config.shelly.generic.weakSignal.label = Schwaches Signal (dBm)
thing-type.config.shelly.generic.weakSignal.description = Ein Alarm wird ausgelöst, wenn das WiFi-Signal diesen Wert unterschreitet. Voreinstellung: -80 dBm
thing-type.config.shelly.generic.eventsButton.label = Button Events
thing-type.config.shelly.generic.eventsButton.description = Aktiviert die Button Action URLS
thing-type.config.shelly.generic.eventsPush.label = Push Events
thing-type.config.shelly.generic.eventsPush.description = Aktiviert die Push Button Action URLS
thing-type.config.shelly.generic.eventsSwitch.label = Output Events
thing-type.config.shelly.generic.eventsSwitch.description = Aktiviert die Output Action URLS
thing-type.config.shelly.generic.eventsCoIoT.label = CoIoT aktivieren
thing-type.config.shelly.generic.eventsCoIoT.description = Aktiviert CoIoT-Protokoll (CoAP-basiert)
thing-type.config.shelly.generic.updateInterval.label = Status-Intervall
thing-type.config.shelly.generic.updateInterval.description = Intervall für die Hintergundaktualisierung
# thing config - light
thing-type.config.shelly.light.userId.label = Benutzer
thing-type.config.shelly.light.userId.description = Benutzerkennung für API-Zugriff
thing-type.config.shelly.light.password.label = Passwort
thing-type.config.shelly.light.password.description = Passwort für API-Zugriff
thing-type.config.shelly.light.deviceIp.label = IP Adresse
thing-type.config.shelly.light.deviceIp.description = IP Adresse der Shelly-Komponente
thing-type.config.shelly.light.weakSignal.label = Schwaches Signal (dBm)
thing-type.config.shelly.light.weakSignal.description = Ein Alarm wird ausgelöst, wenn das WiFi-Signal diesen Wert unterschreitet. Voreinstellung: -80 dBm
thing-type.config.shelly.light.brightnessAutoOn.label = Helligkeit Auto-EIN
thing-type.config.shelly.light.brightnessAutoOn.description = AN: Setzen einer Helligkeit > 0 schaltet das Gerät automatisch ein; AUS: Gerätestatus wird nicht ge#ndert
thing-type.config.shelly.light.eventsButton.label = Button Events
thing-type.config.shelly.light.eventsButton.description = Aktiviert die Button Action URLS
thing-type.config.shelly.light.eventsPush.label = Push Events
thing-type.config.shelly.light.eventsPush.description = Aktiviert die Push Button Action URLS
thing-type.config.shelly.light.eventsSwitch.label = Output Events
thing-type.config.shelly.light.eventsSwitch.description = Aktiviert die Output Action URLS
thing-type.config.shelly.light.eventsCoIoT.label = CoIoT aktivieren
thing-type.config.shelly.light.eventsCoIoT.description = Aktiviert CoIoT-Protokoll (CoAP-basiert)
thing-type.config.shelly.light.updateInterval.label = Status-Intervall
thing-type.config.shelly.light.updateInterval.description = Intervall für die Hintergrundaktualisierung
# thing config - battery
thing-type.config.shelly.battery.userId.label = Benutzer
thing-type.config.shelly.battery.userId.description = Benutzerkennung für API-Zugriff
thing-type.config.shelly.battery.password.label = Passwort
thing-type.config.shelly.battery.password.description = Passwort für API-Zugriff
thing-type.config.shelly.battery.deviceIp.label = IP Adresse
thing-type.config.shelly.battery.deviceIp.description = IP Adresse der Shelly-Komponente
thing-type.config.shelly.battery.eventsSensorReport.label = Sensor Events
thing-type.config.shelly.battery.eventsSensorReport.description = Aktiviert die Sensor Action URLS
thing-type.config.shelly.battery.eventsCoIoT.label = CoIoT aktivieren
thing-type.config.shelly.battery.eventsCoIoT.description = Aktiviert CoIoT-Protokoll (CoAP-basiert)
thing-type.config.shelly.battery.weakSignal.label = Schwaches Signal (dBm)
thing-type.config.shelly.battery.weakSignal.description = Ein Alarm wird ausgelöst, wenn das WiFi-Signal diesen Wert unterschreitet. Voreinstellung: -80 dBm
thing-type.config.shelly.battery.lowBattery.label = Batterieladung niedrig (%)
thing-type.config.shelly.battery.lowBattery.description = Ein Alarm wird ausgelöst, wenn das Gerät eine Batterieladung kleiner diesem Schwellwert meldet. Default: 20%
thing-type.config.shelly.battery.updateInterval.label = Status-Intervall
thing-type.config.shelly.battery.updateInterval.description = Intervall für die Hintergundaktualisierung
# thing config - unknown
thing-type.config.shellydevice.userId.label = Benutzer
thing-type.config.shellydevice.userId.description = Benutzerkennung für API-Zugriff
thing-type.config.shellydevice.password.label = Passwort
thing-type.config.shellydevice.password.description = Passwort für API-Zugriff
thing-type.config.shellydevice.deviceIp.label = IP Adresse
thing-type.config.shellydevice.deviceIp.description = IP Adresse der Shelly-Komponente
# channel-groups
thing-type.shelly.shelly1.group.relay.label = Relais
thing-type.shelly.shelly1.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly1.group.sensors.label = Externe Sensoren
thing-type.shelly.shelly1.group.sensors.description = Temperaturwerte der externen Sensoren (nur wenn angeschlossen)
thing-type.shelly.shelly1.group.device.label = Gerätestatus
thing-type.shelly.shelly1.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly1pm.group.relay.label = Relais
thing-type.shelly.shelly1pm.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly1pm.group.meter.label = Verbrauch
thing-type.shelly.shelly1pm.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly1pm.group.sensors.label = Externe Sensoren
thing-type.shelly.shelly1pm.group.sensors.description = Werte der externen Sensoren (nur wenn angeschlossen)
thing-type.shelly.shelly1pm.group.device.label = Gerätestatus
thing-type.shelly.shelly1pm.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyem.group.relay.label = Relais
thing-type.shelly.shellyem.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellyem.group.meter1.label = Verbrauch 1
thing-type.shelly.shellyem.group.meter1.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyem.group.meter2.label = Verbrauch 2
thing-type.shelly.shellyem.group.meter2.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyem.group.device.label = Gerätestatus
thing-type.shelly.shellyem.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyem3.group.relay.label = Relais
thing-type.shelly.shellyem3.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellyem3.group.meter1.label = Verbrauch 1
thing-type.shelly.shellyem3.group.meter1.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyem3.group.meter2.label = Verbrauch 2
thing-type.shelly.shellyem3.group.meter2.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyem3.group.meter3.label = Verbrauch 3
thing-type.shelly.shellyem3.group.meter3.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyem3.group.device.label = Gerätestatus
thing-type.shelly.shellyem3.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly2-relay.group.relay1.label = Relais 1
thing-type.shelly.shelly2-relay.group.relay1.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly2-relay.group.relay2.label = Relais 2
thing-type.shelly.shelly2-relay.group.relay2.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly2-relay.group.meter.label = Verbrauch
thing-type.shelly.shelly2-relay.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly2-relay.group.device.label = Gerätestatus
thing-type.shelly.shelly2-relay.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly2-roller.group.roller.label = Rollladen
thing-type.shelly.shelly2-roller.group.roller.description = Rollladensteuerung und Status
thing-type.shelly.shelly2-roller.group.meter.label = Verbrauch
thing-type.shelly.shelly2-roller.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly2-roller.group.device.label = Gerätestatus
thing-type.shelly.shelly2-roller.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly25-relay.group.relay1.label = Relais 1
thing-type.shelly.shelly25-relay.group.relay1.description = Relais Ein-/Ausgänge und -Funktionen
thing-type.shelly.shelly25-relay.group.relay2.label = Relais 2
thing-type.shelly.shelly25-relay.group.relay2.description = Relais Ein-/Ausgänge und -Funktionen
thing-type.shelly.shelly25-relay.group.meter1.label = Verbrauch 1
thing-type.shelly.shelly25-relay.group.meter1.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly25-relay.group.meter2.label = Verbrauch 2
thing-type.shelly.shelly25-relay.group.meter2.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly25-relay.group.device.label = Gerätestatus
thing-type.shelly.shelly25-relay.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly25-roller.group.roller.label = Rollladen
thing-type.shelly.shelly25-roller.group.roller.description = Rollladensteuerung und Status
thing-type.shelly.shelly25-roller.group.meter.label = Verbrauch
thing-type.shelly.shelly25-roller.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly25-roller.group.device.label = Gerätestatus
thing-type.shelly.shelly25-roller.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly4pro.group.relay1.label = Relais 1
thing-type.shelly.shelly4pro.group.relay1.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly4pro.group.relay2.label = Relais 2
thing-type.shelly.shelly4pro.group.relay2.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly4pro.group.relay3.label = Relais 3
thing-type.shelly.shelly4pro.group.relay3.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly4pro.group.relay4.label = Relais 4
thing-type.shelly.shelly4pro.group.relay4.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly4pro.group.meter1.label = Verbrauch 1
thing-type.shelly.shelly4pro.group.meter1.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly4pro.group.meter2.label = Verbrauch 2
thing-type.shelly.shelly4pro.group.meter2.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly4pro.group.meter3.label = Verbrauch 3
thing-type.shelly.shelly4pro.group.meter3.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly4pro.group.meter4.label = Verbrauch 4
thing-type.shelly.shelly4pro.group.meter4.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly4pro.group.device.label = Gerätestatus
thing-type.shelly.shelly4pro.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyplug.group.relay.label = Relais
thing-type.shelly.shellyplug.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellyplug.group.meter.label = Verbrauch
thing-type.shelly.shellyplug.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyplug.group.device.label = Gerätestatus
thing-type.shelly.shellyplug.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyplugs.group.relay.label = Relais
thing-type.shelly.shellyplugs.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellyplugs.group.meter.label = Verbrauch
thing-type.shelly.shellyplugs.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyplugs.group.device.label = Gerätestatus
thing-type.shelly.shellyplugs.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellydimmer.group.relay.label = Relais
thing-type.shelly.shellydimmer.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellydimmer.group.meter.label = Verbrauch
thing-type.shelly.shellydimmer.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellydimmer.group.device.label = Gerätestatus
thing-type.shelly.shellydimmer.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellydimmer2.group.relay.label = Relais
thing-type.shelly.shellydimmer2.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellydimmer2.group.meter.label = Verbrauch
thing-type.shelly.shellydimmer2.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellydimmer2.group.device.label = Gerätestatus
thing-type.shelly.shellydimmer2.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyix3.group.status1.label = Eingang #1
thing-type.shelly.shellyix3.group.status1.description = Status Informationen zum Eingang 1
thing-type.shelly.shellyix3.group.status2.label = Eingang #2
thing-type.shelly.shellyix3.group.status2.description = Status Informationen zum Eingang 2
thing-type.shelly.shellyix3.group.status3.label = Eingang #3
thing-type.shelly.shellyix3.group.status3.description = Status Informationen zum Eingang 3
thing-type.shelly.shellyix3.group.device.label = Gerätestatus
thing-type.shelly.shellyix3.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellybutton1.group.status.label = Taster-Status
thing-type.shelly.shellybutton1.group.status.description = Informationen zum Gerätestatus
thing-type.shelly.shellybutton1.group.battery.label = Batteriestatus
thing-type.shelly.shellybutton1.group.battery.description = Informationen zum Akku
thing-type.shelly.shellybutton1.group.device.label = Gerätestatus
thing-type.shelly.shellybutton1.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellybulb.group.control.label = Steuerung
thing-type.shelly.shellybulb.group.control.description = Steuerung des Lichts
thing-type.shelly.shellybulb.group.color.label = Farbmodus
thing-type.shelly.shellybulb.group.color.description = Einstellungen für den Farbmodus
thing-type.shelly.shellybulb.group.white.label = Weißwerte
thing-type.shelly.shellybulb.group.white.description = Einstellungen für den Weiß-Modus
thing-type.shelly.shellybulb.group.device.label = Gerätestatus
thing-type.shelly.shellybulb.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellybulbduo.group.control.label = Steuerung
thing-type.shelly.shellybulbduo.group.control.description = Steuerung des Lichts
thing-type.shelly.shellybulbduo.group.white.label = Weißwerte
thing-type.shelly.shellybulbduo.group.white.description = Einstellungen für den Weiß-Modus
thing-type.shelly.shellybulbduo.group.meter.label = Verbrauch
thing-type.shelly.shellybulbduo.group.meter.description = Verbrauchswerte
thing-type.shelly.shellybulbduo.group.device.label = Gerätestatus
thing-type.shelly.shellybulbduo.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyvintage.group.control.label = Steuerung
thing-type.shelly.shellyvintage.group.control.description = Steuerung des Lichts
thing-type.shelly.shellyvintage.group.white.label = Weißwerte
thing-type.shelly.shellyvintage.group.white.description = Einstellungen für den Weiß-Modus
thing-type.shelly.shellyvintage.group.meter.label = Verbrauch
thing-type.shelly.shellyvintage.group.meter.description = Verbrauchswerte
thing-type.shelly.shellyvintage.group.device.label = Gerätestatus
thing-type.shelly.shellyvintage.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyrgbw2-color.group.control.label = Steuerung
thing-type.shelly.shellyrgbw2-color.group.control.description = Steuerung des Controllers
thing-type.shelly.shellyrgbw2-color.group.color.label = Farben
thing-type.shelly.shellyrgbw2-color.group.color.description = Farbwerte und Profile
thing-type.shelly.shellyrgbw2-color.group.meter.label = Verbrauch
thing-type.shelly.shellyrgbw2-color.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyrgbw2-color.group.device.label = Gerätestatus
thing-type.shelly.shellyrgbw2-color.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyrgbw2-white.group.control.label = Steuerung
thing-type.shelly.shellyrgbw2-white.group.control.description = Lichtsteuerung
thing-type.shelly.shellyrgbw2-white.group.channel1.label = Kanal 1
thing-type.shelly.shellyrgbw2-white.group.channel1.description = Steuerung für Kanal 1
thing-type.shelly.shellyrgbw2-white.group.channel2.label = Kanal 2
thing-type.shelly.shellyrgbw2-white.group.channel2.description = Steuerung für Kanal 2
thing-type.shelly.shellyrgbw2-white.group.channel3.label = Kanal 3
thing-type.shelly.shellyrgbw2-white.group.channel3.description = Steuerung für Kanal 3
thing-type.shelly.shellyrgbw2-white.group.channel4.label = Kanal 4
thing-type.shelly.shellyrgbw2-white.group.channel4.description = Steuerung für Kanal 4
thing-type.shelly.shellyrgbw2-white.group.meter.label = Verbrauch
thing-type.shelly.shellyrgbw2-white.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyrgbw2-white.group.device.label = Gerätestatus
thing-type.shelly.shellyrgbw2-white.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyht.group.sensors.label = Sensorwerte
thing-type.shelly.shellyht.group.sensors.description = Messwerte und Status des Sensors
thing-type.shelly.shellyht.group.battery.label = Batterie-Status
thing-type.shelly.shellyht.group.battery.description = Informationen zum Akku
thing-type.shelly.shellyht.group.device.label = Gerätestatus
thing-type.shelly.shellyht.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyflood.group.sensors.label = Sensorwerte
thing-type.shelly.shellyflood.group.sensors.description = Messwerte und Status des Sensors
thing-type.shelly.shellyflood.group.battery.label = Batterie-Status
thing-type.shelly.shellyflood.group.battery.description = Informationen zum Akku
thing-type.shelly.shellyflood.group.device.label = Gerätestatus
thing-type.shelly.shellyflood.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellydw.group.sensors.label = Sensordaten
thing-type.shelly.shellydw.group.sensors.description = Messwerte und Status des Sensors
thing-type.shelly.shellydw.group.battery.label = Batterie-Status
thing-type.shelly.shellydw.group.battery.description = Informationen zum Akku
thing-type.shelly.shellydw.group.device.label = Gerätestatus
thing-type.shelly.shellydw.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellydw2.group.sensors.label = Sensordaten
thing-type.shelly.shellydw2.group.sensors.description = Messwerte und Status des Sensors
thing-type.shelly.shellydw2.group.battery.label = Batterie-Status
thing-type.shelly.shellydw2.group.battery.description = Informationen zum Akku
thing-type.shelly.shellydw2.group.device.label = Gerätestatus
thing-type.shelly.shellydw2.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellysmoke.sensors.device.label = Sensordaten
thing-type.shelly.shellysmoke.sensors.device.description = Messwerte und Status des Sensors
thing-type.shelly.shellysmoke.group.battery.label = Batterie-Status
thing-type.shelly.shellysmoke.group.battery.description = Informationen zum Akku
thing-type.shelly.shellysmoke.group.device.label = Gerätestatus
thing-type.shelly.shellysmoke.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellysense.sensors.device.label = Sensordaten
thing-type.shelly.shellysense.sensors.device.description = Messwerte und Status des Sensors
thing-type.shelly.shellysense.group.device.label = Gerätestatus
thing-type.shelly.shellysense.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellygas.group.sensors.label = Sensordaten
thing-type.shelly.shellygas.group.sensors.description = Messwerte und Status des Sensors
thing-type.shelly.shellygas.group.device.label = Gerätestatus
thing-type.shelly.shellygas.group.device.description = Informationen zum Gerätestatus
# channels
channel-type.shelly.outputName.label = Ausgangsname
channel-type.shelly.outputName.description = Name des Relais-Ausgangs/Kanals (Konfiguration innerhalb der Shelly App)
channel-type.shelly.timerAutoOn.label = Auto-EIN Timer
channel-type.shelly.timerAutoOn.description = Wenn das Relais ausgeschaltet wird, wird dieses automatisch wieder nach n Sekunden eingeschaltet
channel-type.shelly.timerAutoOff.label = Auto-AUS Timer
channel-type.shelly.timerAutoOff.description = Wenn das Relais eingeschaltet wird, wird dieses automatisch wieder nach n Sekunden ausgeschaltet
channel-type.shelly.timerActive.label = Timer aktiv
channel-type.shelly.timerActive.description = ON: Auto-On/Off Timer ist aktiv
channel-type.shelly.temperature1.label = Temperatur 1
channel-type.shelly.temperature1.description = Temperatur des externen Sensors #1
channel-type.shelly.temperature2.label = Temperatur 2
channel-type.shelly.temperature2.description = Temperatur des externen Sensors #2
channel-type.shelly.temperature3.label = Temperatur 3
channel-type.shelly.temperature3.description = Temperatur des externen Sensors #3
channel-type.shelly.humidity.label = Luftfeuchtigkeit
channel-type.shelly.humidity.description = Relative Luftfeuchtigkeit (0..100%)
channel-type.shelly.rollerShutter.label = Steuerung (0=offen, 100=geschlossen)
channel-type.shelly.rollerShutter.description = Steuerung für den Rollladen: UP, DOWN, STOP, Position (0=offen, 100=geschlossen)
channel-type.shelly.rollerPosition.label = Position (100=offen, 0=zu)
channel-type.shelly.rollerPosition.description = Invertierte Position des Rollladen: 100=offen, 0=zu
channel-type.shelly.rollerState.label = Status
channel-type.shelly.rollerState.description = Zustand des Rollladen (open/closed/stopped).
channel-type.shelly.rollerState.state.option.open = öffnet
channel-type.shelly.rollerState.state.option.close = schließt
channel-type.shelly.rollerState.state.option.stop = gestoppt
channel-type.shelly.rollerStop.label = Stoppgrund
channel-type.shelly.rollerStop.description = Letzter Grund für das Stoppen des Rollladens (normal=normaler Stopp, safety_switch=Sicherheits-Stopp, obstacle=Rollladen verhakt (Widerstand erkannt)
channel-type.shelly.inputState.label = Eingang
channel-type.shelly.inputState.description = Status des Relais-Eingangs
channel-type.shelly.inputState1.label = Eingang 1
channel-type.shelly.inputState1.description = Status des Relais-Eingangs 1
channel-type.shelly.inputState2.label = Eingang 2
channel-type.shelly.inputState2.description = Status des Relais-Eingangs 2
channel-type.shelly.inputState3.label = Eingang 3
channel-type.shelly.inputState3.description = Status des Relais-Eingangs 3
channel-type.shelly.dimmerBrightness.label = Helligkeit
channel-type.shelly.dimmerBrightness.description = Helligkeit (0-100%, 0=aus)
channel-type.shelly.whiteBrightness.label = Helligkeit
channel-type.shelly.whiteBrightness.description = Helligkeit (0-100%, 0=aus)
channel-type.shelly.meterWatts.label = Leistung
channel-type.shelly.meterWatts.description = Aktueller Stromverbrauch in Watt
channel-type.shelly.meterAccuWatts.label = Kumulierter Verbrauch
channel-type.shelly.meterAccuWatts.description = Kumulierter Verbrauch in Watt
channel-type.shelly.meterAccuTotal.label = Kumulierter Gesamtverbrauch
channel-type.shelly.meterAccuTotal.description = Kumulierter Gesamtverbrauch in kW/h
channel-type.shelly.meterAccuReturned.label = Kumulierter Einspeisung
channel-type.shelly.meterAccuReturned.description = Kumulierter Einspeisung in kW/h
channel-type.shelly.meterCurrent.label = Stromstärke
channel-type.shelly.meterCurrent.description = Aktuelle gemessene Stromstärke
channel-type.shelly.meterTotal.label = Gesamtverbrauch
channel-type.shelly.meterTotal.description = Gesamtverbrauch seit Neustart in kW/h
channel-type.shelly.meterReturned.label = Einspeisung
channel-type.shelly.meterReturned.description = Einspeisung in kW/h
channel-type.shelly.meterVoltage.label = Spannung
channel-type.shelly.meterVoltage.description = Spannung in Volt
channel-type.shelly.meterPowerFactor.label = Stromfaktor
channel-type.shelly.meterPowerFactor.description = Faktor für Strom bei Photovoltaik
channel-type.shelly.meterReactive.label = Rückstrom
channel-type.shelly.meterReactive.description = Aktueller Stromverbrauch (Watt) des Rückstroms
channel-type.shelly.lastPower1.label = Schnitt letzte Min
channel-type.shelly.lastPower1.description = Durchschnittsverbrauch der letzten Minute, 60s in Watt/Min
channel-type.shelly.timestamp.label = Letzte Aktualisierung
channel-type.shelly.timestamp.description = Zeitstempel der letzten Aktualisierung (lokale Zeitzone)
channel-type.shelly.ledStatusDisable.label = Status-LED aus
channel-type.shelly.ledStatusDisable.description = ON: Die Status-LED am Gerät ist deaktiviert
channel-type.shelly.ledPowerDisable.label = Betriebs-LED aus
channel-type.shelly.ledPowerDisable.description = ON: Die Betriebsanzeige (LED) am Gerät ist deaktiviert
channel-type.shelly.colorMode.label = Farbmodus
channel-type.shelly.colorMode.description = Betriebsart: ON: Farbe, OFF: Weiß
channel-type.shelly.colorFull.label = Voll-Farbe
channel-type.shelly.colorFull.description = Ausgewählte Farbe (red/green/blue/yellow/white) wird auf volle Intensität gesetzt (255)
channel-type.shelly.colorFull.state.option.red = Rot
channel-type.shelly.colorFull.state.option.green = Grün
channel-type.shelly.colorFull.state.option.blue = Blau
channel-type.shelly.colorFull.state.option.yellow = Gelb
channel-type.shelly.colorFull.state.option.white = Weiß
channel-type.shelly.colorRed.label = Rot
channel-type.shelly.colorRed.description = Rot-Anteil des RGB-Wertes (0-255)
channel-type.shelly.colorGreen.label = Grün
channel-type.shelly.colorGreen.description = Grün-Anteil des RGB-Wertes (0-255)
channel-type.shelly.colorBlue.label = Blau
channel-type.shelly.colorBlue.description = Blau-Anteil des RGB-Wertes (0-255)
channel-type.shelly.colorWhite.label = Weiß
channel-type.shelly.colorWhite.description = Weiß-Anteil des RGBW-Wertes (0-255)
channel-type.shelly.colorGain.label = Verstärkung
channel-type.shelly.colorGain.description = Verstärkung des Farbwertes (1-100%)
channel-type.shelly.whiteTemp.label = Lichttemperatur
channel-type.shelly.whiteTemp.description = Temperatur des Weißlichtes (Bulb/RGBW2: 3000..6500K; Duo: 2700K..6500K)
channel-type.shelly.colorEffectBulb.label = Lichteffekt
channel-type.shelly.colorEffectBulb.description = Lichteffekt: 0: keiner, 1: Meteoritenregen, 2: Verlauf, 3: Atmen, 4: Blitzen, 5: ‹bergang ein/aus, 6: Rot/Grün-Wechsel
channel-type.shelly.colorEffectBulb.option.0 = Aus
channel-type.shelly.colorEffectBulb.option.1 = Meteoritenregen
channel-type.shelly.colorEffectBulb.option.2 = Verlauf
channel-type.shelly.colorEffectBulb.option.3 = Atmen
channel-type.shelly.colorEffectBulb.option.4 = Blitzen
channel-type.shelly.colorEffectBulb.option.5 = Übergang ein/aus
channel-type.shelly.colorEffectBulb.option.6 = Rot/Grün-Wechsel
channel-type.shelly.colorEffectRGBW2.label = Farbeffekt
channel-type.shelly.colorEffectRGBW2.description = Lichteffekt: 0: keiner, 1: Meteoritenregen, 2: Farbverlauf, 3: Blitzen
channel-type.shelly.colorEffectRGBW2.option.0 = Aus
channel-type.shelly.colorEffectRGBW2.option.1 = Meteoritenregen
channel-type.shelly.colorEffectRGBW2.option.2 = Farbverlauf
channel-type.shelly.colorEffectRGBW2.option.3 = Blitzen
channel-type.shelly.sensorTemp.label = Temperatur
channel-type.shelly.sensorTemp.description = Aktuelle Temperatur des Sensors in ∞C
channel-type.shelly.sensorExtTemp.label = Temperatur
channel-type.shelly.sensorExtTemp.description = Aktuelle Temperatur des externen Sensors in ∞C
channel-type.shelly.sensorExtHum.label = Luftfeuchtigkeit
channel-type.shelly.sensorExtHum.description = Relative Luftfeuchtigkeit in %
channel-type.shelly.sensorHumidity.label = Luftfeuchtigkeit
channel-type.shelly.sensorHumidity.description = Relative Luftfeuchtigkeit in %
channel-type.shelly.sensorFlood.label = Wasseralarm
channel-type.shelly.sensorFlood.description = Alarm: Es wurde Wasser erkannt
channel-type.shelly.sensorSmoke.label = Rauchalarm
channel-type.shelly.sensorSmoke.description = Alarm: Es wurde Rauch erkannt
channel-type.shelly.sensorLux.label = Helligkeit
channel-type.shelly.sensorLux.description = Helligkeit in Lux
channel-type.shelly.sensorIllumination.label = Tageslicht
channel-type.shelly.sensorIllumination.description = Erkanntes Tageslicht (bright=taghell, twilight=Dämmerung, dark=Abend/Nacht)
channel-type.shelly.sensorIllumination.state.option.dark = Dunkel
channel-type.shelly.sensorIllumination.state.option.twilight = Dämmerung
channel-type.shelly.sensorIllumination.state.option.bright = Hell
channel-type.shelly.sensorIllumination.state.option.unknown = Unbekannt
channel-type.shelly.sensorIllumination.description = Angabe zum erkannten Tageslichtwert
channel-type.shelly.sensorPPM.label = Gas-Konzentration
channel-type.shelly.sensorPPM.description = Gemessene Konzentration in PPM
channel-type.shelly.sensorTilt.label = Öffnungswinkel
channel-type.shelly.sensorTilt.description = Öffnungswinkel in Grad (erfordert Kalibrierung in der App)
channel-type.shelly.sensorVibration.label = Vibration
channel-type.shelly.sensorVibration.description = ON: Sensor hat eine Vibration erkannt
channel-type.shelly.sensorValve.label = Ventil
channel-type.shelly.sensorValve.description = Gibt den Status des Ventils an, sofern eines angeschlossen ist.
channel-type.shelly.sensorValve.state.option.closed = geschlossen
channel-type.shelly.sensorValve.state.option.opened = geöffnet
channel-type.shelly.sensorValve.state.option.not_connected = nicht angeschlossen
channel-type.shelly.sensorValve.state.option.failure = Störung
channel-type.shelly.sensorValve.state.option.closing = schließt
channel-type.shelly.sensorValve.state.option.opening = öffnet
channel-type.shelly.sensorValve.state.option.checking = Test läuft
channel-type.shelly.sensorValve.state.option.UNKNOWN = unbekannt
channel-type.shelly.sensorCharger.label = Ladegerät
channel-type.shelly.sensorCharger.description = ON: Ein Ladegerät ist angeschlossen
channel-type.shelly.sensorContact.label = Kontakt
channel-type.shelly.sensorContact.description = Status des Sensors (OPEN=offen, CLOSED=geschlossen)
channel-type.shelly.sensorContact.state.option.OPEN = geöffnet
channel-type.shelly.sensorContact.state.option.CLOSED = geschlossen
channel-type.shelly.sensorState.label = Gerätestatus
channel-type.shelly.sensorState.description = Status der Betriebsbereitschaft (warmup/normal/fault)
channel-type.shelly.sensorState.state.option.warmup= Initialisierung
channel-type.shelly.sensorState.state.option.normal= Normal
channel-type.shelly.sensorState.state.option.fault= Fehler
channel-type.shelly.sensorState.state.option.unknown= Unbekannt
channel-type.shelly.sensorWakeup.label = Aufwachgrund
channel-type.shelly.sensorWakeup.description = Grund des letzten Aufweckens (button/battery/perodice/powerdown/sensor/alarm/ext_power)
channel-type.shelly.sensorWakeup.state.option.button = Taste
channel-type.shelly.sensorWakeup.state.option.battery = Batterie
channel-type.shelly.sensorWakeup.state.option.periodic = Intervall
channel-type.shelly.sensorWakeup.state.option.poweron = Betrieb
channel-type.shelly.sensorWakeup.state.option.sensor = Statusänderung
channel-type.shelly.sensorWakeup.state.option.alarm = Alarm
channel-type.shelly.sensorWakeup.state.option.ext_power = Ladegerät verbunden
channel-type.shelly.batVoltage.label = Batteriespannung
channel-type.shelly.batVoltage.description = Batteriespannung in Volt (V)
channel-type.shelly.charger.label = Ladegerät
channel-type.shelly.charger.description = ON: Ein Ladegerät ist angeschlossen
channel-type.shelly.senseKey.label = IR-Code
channel-type.shelly.senseKey.description = IR Code senden im Pronto- oder HEX64-Format
channel-type.shelly.deviceName.label = Gerätename
channel-type.shelly.deviceName.description = Symbolischer Name des Gerätes (Konfiguration über Shelly App)
channel-type.shelly.uptime.label = Laufzeit
channel-type.shelly.uptime.description = Anzahl Sekunden seit dem das Gerät mit Strom versorgt wurde
channel-type.shelly.heartBeat.label = Letzte Aktivität
channel-type.shelly.heartBeat.description = Zeitpunkt der letzten Aktivität. Hierbei kann es sich um einen erfolgreichen API-Aufruf, oder Sensor-Aktualisierung handeln. Dies schließt eine erfolgreiche Netzwerk-Kommunikation ein (WiFi + IP).
channel-type.shelly.updateAvailable.label = Firmwareaktualisierung vorhanden
channel-type.shelly.updateAvailable.description = ON: Es ist eine neuere Firmwareversion verfügbar (kann mit der Shelly App durchgef¸hrt werden)
channel-type.shelly.deviceTemp.label = Gerätetemperatur
channel-type.shelly.deviceTemp.description = Temperatur im Gerät. Hohe Temperaturen deuten ggf. auch ein Hitzestau/Installationsproblem hin.
channel-type.shelly.lastUpdate.label = Letzte Aktualisierung
channel-type.shelly.lastUpdate.description = Zeitstempel der letzten Aktualisierung (lokale Zeitzone)
channel-type.shelly.lastEvent.label = Letztes Ereignis
channel-type.shelly.lastEvent.description = Typ des letzten Ereignisses (S=kurz, SS=2xkurz, SSS=3xkurz, L=lang)
channel-type.shelly.lastEvent.state.option.S = 1x kurz
channel-type.shelly.lastEvent.state.option.SS = 2x kurz
channel-type.shelly.lastEvent.state.option.SSS = 3x kurz
channel-type.shelly.lastEvent.state.option.L = lang
channel-type.shelly.lastEvent.state.option.SL = kurz-lang
channel-type.shelly.lastEvent.state.option.LS = lang-kurz
channel-type.shelly.eventCount.label = Ereigniszähler
channel-type.shelly.eventCount.description = Anzahl der empfangenen Ereignisse.
channel-type.shelly.eventTrigger.label = Ereignis
channel-type.shelly.eventTrigger.description = Signalisiert Ereignisse (Trigger): ROLLER_OPEN=Rollladen geöffnet, ROLLER_CLOSE=Rollladen geschlossen, ROLLER_STOP=Rollladen angehalten
channel-type.shelly.eventTrigger.option.ROLLER_OPEN = Rollladen geöffnet
channel-type.shelly.eventTrigger.option.ROLLER_CLOSE = Rollladen geschlossen
channel-type.shelly.eventTrigger.option.ROLLER_STOP = Rollladen gestoppt
channel-type.shelly.alarmTrigger.description = Signalisiert Alarme (Trigger): NONE=kein Alarm, WEAK_SIGNAL=Schlechte WiFi-Verbindung, RESTARTED=Gerät neu gestartet, OVERTEMP=Überhitzung, OVERLOAD=Überlast, OVERPOWER=Maximale Last überschritten, LOAD_ERROR=Lastfehler, LOW_BATTERY=Batterie schwach
channel-type.shelly.alarmTrigger.option.NONE = kein Alarm
channel-type.shelly.alarmTrigger.option.WEAK_SIGNAL = Schlechte WiFi-Verbindung
channel-type.shelly.alarmTrigger.option.RESTARTED = Gerät neu gestartet
channel-type.shelly.alarmTrigger.option.OVERTEMP = Überhitzung
channel-type.shelly.alarmTrigger.option.OVERLOAD = Überlast
channel-type.shelly.alarmTrigger.option.OVERPOWER = Maximale Last überschritten
channel-type.shelly.alarmTrigger.option.LOAD_ERROR = Lastfehler
channel-type.shelly.alarmTrigger.option.LOW_BATTERY = Batterieladung schwach
channel-type.shelly.alarmState.label = Alarmstatus
channel-type.shelly.alarmState.description = Typ des Alarms (unknown/none/mild/heavy/test)
channel-type.shelly.alarmState.state.option.unknown = Unbekannt
channel-type.shelly.alarmState.state.option.none = Kein Alarm
channel-type.shelly.alarmState.state.option.mild = Leichte Konzentration
channel-type.shelly.alarmState.state.option.heavy = Hohe Konzentration
channel-type.shelly.alarmState.state.option.test = Testknopf gedrückt
channel-type.shelly.selfTest.label = Selbsttest
channel-type.shelly.selfTest.description = Status des Selbsttests
channel-type.shelly.selfTest.state.option.pending = ausstehend
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

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="deviceStatus">
<label>Device Status</label>
<description>Information about the device status</description>
<channels>
<channel id="alarm" typeId="alarmTrigger"/>
<channel id="wifiSignal" typeId="system.signal-strength"/>
<channel id="uptime" typeId="uptime"/>
</channels>
</channel-group-type>
<channel-type id="alarmTrigger">
<kind>trigger</kind>
<label>Alarm</label>
<description>Alarm Trigger, e.g. weak WiFi Signal detected or over heating.</description>
<event>
<options>
<option value="NONE">None</option>
<option value="WEAK_SIGNAL">Weak WiFi</option>
<option value="RESTARTED">Device restarted</option>
<option value="OVERTEMP">Device is overheating</option>
<option value="OVERLOAD">Device is overloaded</option>
<option value="OVERPOWER">Device is over max power</option>
<option value="LOAD_ERROR">Load error</option>
<option value="LOW_BATTERY">Low battery</option>
<option value="BATTERY">Wakeup by battery</option>
<option value="POWERON">Device was powered on</option>
<option value="BUTTON">Button was pressed</option>
<option value="SENSOR">Sensor data updated</option>
</options>
</event>
</channel-type>
<channel-type id="sensorWakeup" advanced="true">
<item-type>String</item-type>
<label>Wakeup Reason</label>
<description>Last reason, which woke up the device</description>
<state pattern="%s" readOnly="true">
<options>
<option value="button">Button</option>
<option value="battery">Battery</option>
<option value="periodic">Periodic</option>
<option value="poweron">Power-On</option>
<option value="sensor">Sensor</option>
<option value="alarm">Alarm</option>
<option value="ext_power">Charger connected</option>
</options>
</state>
</channel-type>
<channel-type id="deviceName">
<item-type>String</item-type>
<label>@text/channel-type.shelly.deviceName.label</label>
<description>@text/channel-type.shelly.deviceName.description</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="charger" advanced="true">
<item-type>Switch</item-type>
<label>Charger Connected</label>
<description>ON: Device is charging, OFF: No charger connected</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="batVoltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>Battery Voltage</label>
<description>Battery voltage in V</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="uptime" advanced="true">
<item-type>Number:Time</item-type>
<label>Uptime</label>
<description>Number of seconds since the device was powered up</description>
<state readOnly="true" pattern="%d %unit%">
</state>
</channel-type>
<channel-type id="heartBeat" advanced="true">
<item-type>DateTime</item-type>
<label>Last Heartbeat</label>
<description>Last time we received a signal from the device (API result or sensor report), This indicates that device
communication works on the network layer (WiFi + IP).</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="updateAvailable" advanced="true">
<item-type>Switch</item-type>
<label>Update available</label>
<description>ON: A firmware update is available (use Shelly App to perform update)</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="deviceTemp" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Internal Temperature</label>
<description>Internal device temperature, helps to detect overheating due to installation issues</description>
<category>Temperature</category>
<tags>
<tag>CurrentTemperature</tag>
</tags>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="selfTest">
<item-type>String</item-type>
<label>Self Test</label>
<description>Self Test Status/Result</description>
<state pattern="%s" readOnly="true">
<options>
<option value="not_completed">Not completed</option>
<option value="completed">Completed</option>
<option value="running">Running</option>
<option value="pending">Pending</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,264 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shellybulb">
<label>Shelly Bulb (SHBULB)</label>
<description>Shelly Bulb</description>
<channel-groups>
<channel-group id="control" typeId="bulbControl"/>
<channel-group id="color" typeId="colorSettingsBulb"/>
<channel-group id="white" typeId="whiteSettings"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:light"/>
</thing-type>
<thing-type id="shellybulbduo">
<label>Shelly Duo (SHBDUO-1)</label>
<description>Shelly Duo</description>
<channel-groups>
<channel-group id="control" typeId="duoControl"/>
<channel-group id="white" typeId="whiteSettings"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:light"/>
</thing-type>
<thing-type id="shellyvintage">
<label>Shelly Vintage (SHVIN-1)</label>
<description>Shelly Vintage Light Bulb</description>
<channel-groups>
<channel-group id="control" typeId="duoControl"/>
<channel-group id="white" typeId="whiteSettingsSimple"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:light"/>
</thing-type>
<thing-type id="shellyrgbw2-color">
<label>Shelly RGBW2 Color Mode (SHRGBW2)</label>
<description>Shelly RGBW2 Controller - Color Mode</description>
<category>Lightbulb</category>
<channel-groups>
<channel-group id="control" typeId="rgbw2ColorControl"/>
<channel-group id="color" typeId="colorSettingsRGBW2"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:rgbw2"/>
</thing-type>
<thing-type id="shellyrgbw2-white">
<label>Shelly RGBW2 White Mode (SHRGBW2)</label>
<description>Shelly RGBW2 Controller - White Mode</description>
<channel-groups>
<channel-group id="control" typeId="rgbw2WhiteControl"/>
<channel-group id="channel1" typeId="rgbw2Channel">
<label>Channel 1</label>
</channel-group>
<channel-group id="channel2" typeId="rgbw2Channel">
<label>Channel 2</label>
</channel-group>
<channel-group id="channel3" typeId="rgbw2Channel">
<label>Channel 3</label>
</channel-group>
<channel-group id="channel4" typeId="rgbw2Channel">
<label>Channel 4</label>
</channel-group>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:rgbw2"/>
</thing-type>
<channel-group-type id="bulbControl">
<label>Light Control</label>
<description>Control your light</description>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="mode" typeId="colorMode"/>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
</channels>
</channel-group-type>
<channel-group-type id="duoControl">
<label>Light Control</label>
<description>Control your light</description>
<channels>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
</channels>
</channel-group-type>
<channel-group-type id="rgbw2ColorControl">
<label>Light Control</label>
<description>Control your light channels</description>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
</channels>
</channel-group-type>
<channel-group-type id="rgbw2WhiteControl">
<label>White Control</label>
</channel-group-type>
<channel-group-type id="colorSettingsBulb">
<label>Colors</label>
<description>Light colors</description>
<channels>
<channel id="hsb" typeId="system.color"/>
<channel id="full" typeId="colorFull"/>
<channel id="red" typeId="colorRed"/>
<channel id="green" typeId="colorGreen"/>
<channel id="blue" typeId="colorBlue"/>
<channel id="white" typeId="colorWhite"/>
<channel id="gain" typeId="colorGain"/>
<channel id="effect" typeId="colorEffectBulb"/>
</channels>
</channel-group-type>
<channel-group-type id="colorSettingsRGBW2">
<label>Colors</label>
<description>Light colors</description>
<channels>
<channel id="hsb" typeId="system.color"/>
<channel id="full" typeId="colorFull"/>
<channel id="red" typeId="colorRed"/>
<channel id="green" typeId="colorGreen"/>
<channel id="blue" typeId="colorBlue"/>
<channel id="white" typeId="colorWhite"/>
<channel id="gain" typeId="colorGain"/>
<channel id="effect" typeId="colorEffectRGBW2"/>
</channels>
</channel-group-type>
<channel-group-type id="whiteSettings">
<label>White settings</label>
<description>Adjust colors when device is in white mode</description>
<channels>
<channel id="brightness" typeId="whiteBrightness"/>
<channel id="temperature" typeId="whiteTemp"/>
</channels>
</channel-group-type>
<channel-group-type id="whiteSettingsSimple">
<label>White settings</label>
<description>Adjust colors when device is in white mode</description>
<channels>
<channel id="brightness" typeId="whiteBrightness"/>
</channels>
</channel-group-type>
<channel-group-type id="rgbw2Channel">
<label>Control</label>
<description>Switch channel and adjust settings</description>
<channels>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
<channel id="brightness" typeId="whiteBrightness"/>
</channels>
</channel-group-type>
<channel-type id="colorMode">
<item-type>Switch</item-type>
<label>Mode (ON=color, OFF=white)</label>
<description>ON: Device is in color mode, OFF: Device is in White Mode</description>
</channel-type>
<channel-type id="colorFull">
<item-type>String</item-type>
<label>Full Color</label>
<description>Setting this channel sets the selected color to 255, all others to 0</description>
<state>
<options>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="yellow">Yellow</option>
<option value="white">White</option>
</options>
</state>
</channel-type>
<channel-type id="colorRed" advanced="true">
<item-type>Dimmer</item-type>
<label>Red</label>
<description>red, 0..255, only in Color Mode</description>
<state min="0" max="255" step="1" readOnly="false"></state>
</channel-type>
<channel-type id="colorGreen" advanced="true">
<item-type>Dimmer</item-type>
<label>Green</label>
<description>green, 0..255, applies Color Mode</description>
<state min="0" max="255" step="1" readOnly="false"></state>
</channel-type>
<channel-type id="colorBlue" advanced="true">
<item-type>Dimmer</item-type>
<label>Blue</label>
<description>blue, 0..255, only in Color Mode</description>
<state min="0" max="255" step="1" readOnly="false"></state>
</channel-type>
<channel-type id="colorWhite" advanced="true">
<item-type>Dimmer</item-type>
<label>White</label>
<description>white, 0..255, applies in Color Mode</description>
<state min="0" max="255" step="1" readOnly="false"></state>
</channel-type>
<channel-type id="colorGain">
<item-type>Dimmer</item-type>
<label>Gain</label>
<description>gain for all channels, 0..100, applies in Color Mode</description>
<state min="0" max="100" step="1" readOnly="false"></state>
</channel-type>
<channel-type id="whiteTemp">
<item-type>Dimmer</item-type>
<label>Light Temperature</label>
<description>Light Temperature 3000..6500K</description>
<state min="3000" max="6500" step="10" readOnly="false"></state>
</channel-type>
<channel-type id="whiteBrightness">
<item-type>Dimmer</item-type>
<label>Brightness</label>
<description>Brightness: 0..100%</description>
<category>DimmableLight</category>
</channel-type>
<channel-type id="colorEffectBulb">
<item-type>Number</item-type>
<label>Effect</label>
<description>Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual Change, 3: Breath, 4: Flash,
5: On/Off Gradual, 6: Red/Green Change</description>
<state>
<options>
<option value="0">Off</option>
<option value="1">Meteor Shower</option>
<option value="2">Gradual Change</option>
<option value="3">Breath</option>
<option value="4">Flash</option>
<option value="5">On/Off Gradual</option>
<option value="6">Red/Green Change</option>
</options>
</state>
</channel-type>
<channel-type id="colorEffectRGBW2">
<item-type>Number</item-type>
<label>Effect</label>
<description>Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual Change, 3: Flash</description>
<state>
<options>
<option value="0">Off</option>
<option value="1">Meteor Shower</option>
<option value="2">Gradual Change</option>
<option value="3">Flash</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,528 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shelly1">
<label>Shelly1 (SHSW-1)</label>
<description>Shelly1 device with single relay</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shelly1pm">
<label>Shelly1PM (SHSW-PM)</label>
<description>Shelly1PM device with single relay and power meter</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shellyem">
<label>ShellyEM (SHEM)</label>
<description>Shelly EM device with single relay and power meter</description>
<channel-groups>
<channel-group id="meter1" typeId="meter">
<label>Power Meter 1</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>Power Meter 2</label>
</channel-group>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<!-- Shelly 3EM - device reports wrong service name: shellyem3, see README -->
<thing-type id="shellyem3">
<label>Shelly 3EM (SHEM-3)</label>
<description>Shelly 3EM device with 3 power meters and a relay (thing name is shellyem3, see README)</description>
<channel-groups>
<channel-group id="meter1" typeId="meter">
<label>Power Meter 1</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>Power Meter 2</label>
</channel-group>
<channel-group id="meter3" typeId="meter">
<label>Power Meter 3</label>
</channel-group>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shelly2-relay">
<label>Shelly2 Relay (SHSW-21)</label>
<description>Shelly2 device with two relays</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>Relay 1</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>Relay 2</label>
</channel-group>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shelly2-roller">
<label>Shelly2 Roller (SHSW-21)</label>
<description>Shelly2 in Roller Mode</description>
<channel-groups>
<channel-group id="roller" typeId="rollerControl"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:roller"/>
</thing-type>
<thing-type id="shelly25-relay">
<label>Shelly2.5 Relay (SHSW-25)</label>
<description>Shelly2.5 device with two relays, 2 meters</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>Relay 1</label>
</channel-group>
<channel-group id="meter1" typeId="meter">
<label>Power Meter 1</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>Relay 2</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>Power Meter 2</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shelly25-roller">
<label>Shelly2.5 Roller (SHSW-25)</label>
<description>Shelly2 in Roller Mode</description>
<channel-groups>
<channel-group id="roller" typeId="rollerControl"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:roller"/>
</thing-type>
<thing-type id="shelly4pro">
<label>Shelly4 Pro Relay (SHSW-44)</label>
<description>Shelly4Pro device with 4 relays</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>Relay 1</label>
</channel-group>
<channel-group id="meter1" typeId="meter">
<label>Power Meter 1</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>Relay 2</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>Power Meter 2</label>
</channel-group>
<channel-group id="relay3" typeId="relayChannel">
<label>Relay 3</label>
</channel-group>
<channel-group id="meter3" typeId="meter">
<label>Power Meter 3</label>
</channel-group>
<channel-group id="relay4" typeId="relayChannel">
<label>Relay 4</label>
</channel-group>
<channel-group id="meter4" typeId="meter">
<label>Power Meter 4</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shellyplug">
<label>Shelly Plug</label>
<description>Shelly Plug device with relay and power meter</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannelPlug"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shellyplugs">
<label>Shelly Plug-S (SHPLG-S)</label>
<description>Shelly Plug-S with relay, meter and LED control</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannelPlug"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shellydimmer">
<label>Shelly Dimmer (SHDM-1)</label>
<description>Shelly Dimmer</description>
<category>DimmableLight</category>
<channel-groups>
<channel-group id="relay" typeId="dimmerChannel"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:dimmer"/>
</thing-type>
<thing-type id="shellydimmer2">
<label>Shelly Dimmer 2 (SHDM-2)</label>
<description>Shelly Dimmer 2</description>
<category>DimmableLight</category>
<channel-groups>
<channel-group id="relay" typeId="dimmerChannel"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:dimmer"/>
</thing-type>
<thing-type id="shellyix3">
<label>Shelly ix3 (SHIX3-1)</label>
<description>Shelly i3 Device with 3 inputs</description>
<channel-groups>
<channel-group id="status1" typeId="ix3Channel"/>
<channel-group id="status2" typeId="ix3Channel"/>
<channel-group id="status3" typeId="ix3Channel"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<channel-group-type id="relayChannel">
<label>Relay</label>
<description>A Shelly relay channel</description>
<channels>
<channel id="output" typeId="system.power"/>
<channel id="input" typeId="inputState"/>
<channel id="button" typeId="system.button"/>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
</channels>
</channel-group-type>
<channel-group-type id="relayChannelPlug">
<label>Relay</label>
<description>A Shelly relay channel</description>
<channels>
<channel id="output" typeId="system.power"/>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
</channels>
</channel-group-type>
<channel-group-type id="dimmerChannel">
<label>Dimmer</label>
<description>A Shelly Dimmer channel</description>
<channels>
<channel id="brightness" typeId="dimmerBrightness"/>
<channel id="input1" typeId="inputState1"/>
<channel id="input2" typeId="inputState2"/>
<channel id="button" typeId="system.button"/>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
</channels>
</channel-group-type>
<channel-group-type id="ix3Channel">
<label>Input</label>
<description>Input Status</description>
<channels>
<channel id="input" typeId="inputState"/>
<channel id="button" typeId="system.button"/>
<channel id="lastEvent" typeId="lastEvent"/>
<channel id="eventCount" typeId="eventCount"/>
</channels>
</channel-group-type>
<channel-group-type id="rollerControl">
<label>Roller Control</label>
<description>Controlling the roller mode</description>
<channels>
<channel id="control" typeId="rollerShutter"/>
<channel id="rollerpos" typeId="rollerPosition"/>
<channel id="state" typeId="rollerState"/>
<channel id="stopReason" typeId="rollerStop"/>
<channel id="input1" typeId="inputState1"/>
<channel id="input2" typeId="inputState2"/>
<channel id="event" typeId="eventTrigger"/>
</channels>
</channel-group-type>
<channel-group-type id="meter">
<label>Power Meter</label>
<description>Power consumption for the relay</description>
</channel-group-type>
<channel-group-type id="externalSensors">
<label>External Sensors</label>
<description>Temperatures from external sensors connected to the optional Addon</description>
</channel-group-type>
<channel-type id="outputName">
<item-type>String</item-type>
<label>@text/channel-type.shelly.outputName.label</label>
<description>@text/channel-type.shelly.outputName.description</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="timerAutoOn" advanced="true">
<item-type>Number:Time</item-type>
<label>Auto-ON Timer</label>
<description>ON: After the output was turned off it turns on automatically after xx seconds; 0 disables the timer</description>
<state min="0" step="1" pattern="%d %unit%" readOnly="false"></state>
</channel-type>
<channel-type id="timerAutoOff" advanced="true">
<item-type>Number:Time</item-type>
<label>Auto-OFF Timer</label>
<description>ON: After the output was turned on it turns off automatically after xx seconds; 0 disables the timer</description>
<state min="0" step="1" pattern="%d %unit%" readOnly="false"></state>
</channel-type>
<channel-type id="timerActive" advanced="true">
<item-type>Switch</item-type>
<label>Auto ON/OFF timer active</label>
<description>ON: A timer is active, OFF: no timer active</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="rollerShutter">
<item-type>Rollershutter</item-type>
<label>Roller Control (0=open, 100=closed))</label>
<description>Controls the roller</description>
<category>Blinds</category>
</channel-type>
<channel-type id="rollerPosition" advanced="true">
<item-type>Dimmer</item-type>
<label>Roller Position (100=open, 0=closed)</label>
<description>Position the roller (100..0 in %, where 100%=open, 0%=closed)</description>
<state readOnly="false" min="0" max="100"/>
</channel-type>
<channel-type id="rollerState">
<item-type>String</item-type>
<label>State</label>
<description>State of the roller (open/close/stop)</description>
<state readOnly="true">
<options>
<option value="open">opening</option>
<option value="close">closing</option>
<option value="stop">stopped</option>
</options>
</state>
</channel-type>
<channel-type id="rollerStop">
<item-type>String</item-type>
<label>Roller stop reason</label>
<description>Last cause for stopping: normal, safety switch, obstacle</description>
<state readOnly="true">
<options>
<option value="normal">normal</option>
<option value="safety_switch">safety switch</option>
<option value="obstacle">obstacle detected</option>
</options>
</state>
</channel-type>
<channel-type id="rollerDirection">
<item-type>String</item-type>
<label>Last Roller Direction</label>
<description>Last direction: open or close</description>
<state readOnly="true">
<options>
<option value="open">open</option>
<option value="close">close</option>
<option value="stop">stopped</option>
</options>
</state>
</channel-type>
<channel-type id="inputState">
<item-type>Switch</item-type>
<label>Input</label>
<description>Input/Button state</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="inputState1" advanced="true">
<item-type>Switch</item-type>
<label>Input #1</label>
<description>Input/Button state #1</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="inputState2" advanced="true">
<item-type>Switch</item-type>
<label>Input #2</label>
<description>Input/Button state #2</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="dimmerBrightness">
<item-type>Dimmer</item-type>
<label>Brightness</label>
<description>Light brightness in %</description>
<category>DimmableLight</category>
</channel-type>
<channel-type id="meterWatts">
<item-type>Number:Power</item-type>
<label>Watt</label>
<description>Current power consumption in Watt</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterAccuWatts" advanced="true">
<item-type>Number:Power</item-type>
<label>Accumulated Watt</label>
<description>Accumulated current power consumption in Watt from all meters</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterAccuTotal" advanced="true">
<item-type>Number:Power</item-type>
<label>Accumulated Total</label>
<description>Accumulated total power consumption in kw/h from all meters</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterAccuReturned" advanced="true">
<item-type>Number:Power</item-type>
<label>Accumulated Returned</label>
<description>Accumulated returned power consumption in kw/h from all meters</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterReactive">
<item-type>Number:Power</item-type>
<label>Reactive Watt</label>
<description>Instantaneous reactive power in Watts (W)</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="lastPower1" advanced="true">
<item-type>Number:Energy</item-type>
<label>Last Power #1</label>
<description>Last power consumption #1 - one rounded minute</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="lastPower2" advanced="true">
<item-type>Number:Energy</item-type>
<label>Last Power #2</label>
<description>Last power consumption #2 - one rounded minute</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="lastPower3" advanced="true">
<item-type>Number:Energy</item-type>
<label>Last Power #3</label>
<description>Last power consumption #3 - one rounded minute</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterTotal">
<item-type>Number:Energy</item-type>
<label>Total Energy</label>
<description>Total power consumption in kw/h</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterReturned">
<item-type>Number:Energy</item-type>
<label>Total Returned Energy (kw/h)</label>
<description>Total returned energy in kw/h</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterVoltage">
<item-type>Number:ElectricPotential</item-type>
<label>Voltage</label>
<description>RMS voltage, Volts </description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterCurrent">
<item-type>Number:ElectricPotential</item-type>
<label>Current</label>
<description>Current in A </description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterPowerFactor">
<item-type>Number</item-type>
<label>Power Factor</label>
<description></description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="timestamp">
<item-type>DateTime</item-type>
<label>Last Update</label>
<description>Timestamp of last measurement</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="ledStatusDisable">
<item-type>Switch</item-type>
<label>Disable Status LED</label>
<description>Disable LED indication for network status</description>
</channel-type>
<channel-type id="ledPowerDisable">
<item-type>Switch</item-type>
<label>Disable Power LED</label>
<description>Disable LED indication for output status</description>
</channel-type>
<channel-type id="lastUpdate" advanced="true">
<item-type>DateTime</item-type>
<label>Last Update</label>
<description>Timestamp of last status update</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="eventTrigger">
<kind>trigger</kind>
<label>Event</label>
<description>Event Trigger, e.g. button on/off, Output on/off</description>
<event>
<options>
<option value="ROLLER_OPEN">Roller is open</option>
<option value="ROLLER_CLOSE">Roller is closed</option>
<option value="ROLLER_STOP">Roller has stopped</option>
</options>
</event>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,347 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shellyht">
<label>Shelly H&amp;T (SHHT-1)</label>
<description>Shelly H&amp;T Sensor</description>
<channel-groups>
<channel-group id="sensors" typeId="htSensor"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellysmoke">
<label>Shelly Smoke</label>
<description>Shelly Smoke Sensor (battery powered)</description>
<channel-groups>
<channel-group id="sensors" typeId="smokeSensor"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellygas">
<label>Shelly GAS (SHGS-1)</label>
<description>Shelly Gas Sensor</description>
<channel-groups>
<channel-group id="sensors" typeId="gasSensor"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:basic"/>
</thing-type>
<thing-type id="shellyflood">
<label>Shelly Flood (SHWT-1)</label>
<description>Shelly Flood Sensor (battery powered)</description>
<channel-groups>
<channel-group id="sensors" typeId="floodSensor"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellydw">
<label>Shelly Door/Window (SHDW-1)</label>
<description>Shelly Door/Window Sensor (battery powered)</description>
<channel-groups>
<channel-group id="sensors" typeId="doorWinSensors"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellydw2">
<label>Shelly Door/Window (SHDW-2)</label>
<description>Shelly Door/Window 2 Sensor (battery powered)</description>
<channel-groups>
<channel-group id="sensors" typeId="doorWinSensors"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellysense">
<label>Shelly Sense (SHSEN-1)</label>
<description>Shelly Sense Remote IR Controller</description>
<channel-groups>
<channel-group id="control" typeId="senseControl"/>
<channel-group id="sensors" typeId="senseSensors"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellybutton1">
<label>Shelly Button 1 (SHBTN-1)</label>
<description>Shelly Button 1 (battery powered)</description>
<channel-groups>
<channel-group id="status" typeId="buttonState"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<channel-group-type id="htSensor">
<label>Sensor Data</label>
<description>Data from the HT Sensor</description>
</channel-group-type>
<channel-group-type id="floodSensor">
<label>Sensor Data</label>
<description>Data from the Flood Sensor</description>
</channel-group-type>
<channel-group-type id="smokeSensor">
<label>Sensor Data</label>
<description>Data from the Flood Sensor</description>
</channel-group-type>
<channel-group-type id="gasSensor">
<label>Sensor Data</label>
<description>Data from the Gas Sensor</description>
</channel-group-type>
<channel-group-type id="doorWinSensors">
<label>Sensors</label>
<description>Data from the sensors</description>
</channel-group-type>
<channel-group-type id="senseSensors">
<label>Sensors</label>
<description>Data from the Sense sensors</description>
</channel-group-type>
<channel-group-type id="batteryStatus">
<label>Battery Status</label>
</channel-group-type>
<channel-group-type id="senseControl">
<label>Sense Control</label>
<description>Sense Settings</description>
<channels>
<channel id="key" typeId="senseKey"/>
</channels>
</channel-group-type>
<channel-group-type id="buttonState">
<label>Button State</label>
<description>Status of the Button</description>
<channels>
<channel id="input" typeId="inputState"/>
<channel id="button" typeId="system.button"/>
<channel id="lastEvent" typeId="lastEvent"/>
<channel id="eventCount" typeId="eventCount"/>
</channels>
</channel-group-type>
<channel-type id="sensorContact">
<item-type>Contact</item-type>
<label>Contact</label>
<description>State of the contact: open/close</description>
<category>Contact</category>
<state pattern="%s" readOnly="true">
<options>
<option value="OPEN">Open</option>
<option value="CLOSED">Closed</option>
</options>
</state>
</channel-type>
<channel-type id="sensorState">
<item-type>String</item-type>
<label>Sensor State</label>
<description>Sensor State: normal</description>
<state pattern="%s" readOnly="true">
<options>
<option value="warmup">Warm-Up</option>
<option value="normal">Normal</option>
<option value="fault">Fault</option>
<option value="unknown">unknown</option>
</options>
</state>
</channel-type>
<channel-type id="alarmState">
<item-type>String</item-type>
<label>Alarm State</label>
<description>Alarm State: normal</description>
<state pattern="%s" readOnly="true">
<options>
<option value="unknown">Unknown</option>
<option value="none">None</option>
<option value="mild">Mild</option>
<option value="heavy">Heavy</option>
<option value="test">Test</option>
</options>
</state>
</channel-type>
<channel-type id="lastEvent">
<item-type>String</item-type>
<label>Event</label>
<description>Event Type</description>
<state pattern="%s" readOnly="true">
<options>
<option value="S">Short push</option>
<option value="SS">Double-Short push</option>
<option value="SSS">Triple-Short push</option>
<option value="L">Long push</option>
<option value="SL">Short-Long push</option>
<option value="LS">Long-Short push</option>
</options>
</state>
</channel-type>
<channel-type id="eventCount">
<item-type>Number</item-type>
<label>Event Count</label>
<description>Event Count</description>
<state pattern="%d" readOnly="true">
</state>
</channel-type>
<channel-type id="sensorTemp">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Temperature from the sensor</description>
<category>Temperature</category>
<tags>
<tag>CurrentTemperature</tag>
</tags>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="sensorExtTemp">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Temperature from the external sensor</description>
<category>Temperature</category>
<tags>
<tag>CurrentTemperature</tag>
</tags>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="sensorExtHum">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Relative humidity in % (0..100%) from external sensor</description>
<category>Humidity</category>
<tags>
<tag>CurrentHumidity</tag>
</tags>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="sensorHumidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Relative humidity in % (0..100%)</description>
<tags>
<tag>CurrentHumidity</tag>
</tags>
<state readOnly="true" min="0" max="100" pattern="%f %unit%"/>
</channel-type>
<channel-type id="sensorFlood">
<item-type>Switch</item-type>
<label>Flood Alarm</label>
<description>ON: Indicates flood condition / water detected</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="sensorSmoke">
<item-type>Switch</item-type>
<label>Smoke Alarm</label>
<description>ON: Indicates smoke detection</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="sensorLux">
<item-type>Number:Illuminance</item-type>
<label>Lux</label>
<description>Brightness from the sensor (Lux)</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="sensorIllumination">
<item-type>String</item-type>
<label>Illumination</label>
<description>Current illumination: dark/twilight/bright</description>
<state readOnly="true">
<options>
<option value="dark">Dark</option>
<option value="twilight">Twilight</option>
<option value="bright">Bright</option>
</options>
</state>
</channel-type>
<channel-type id="sensorVibration">
<item-type>Switch</item-type>
<label>Vibration</label>
<description>ON: Vibration detected</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="sensorTilt">
<item-type>Number:Angle</item-type>
<label>Tilt</label>
<description>Tilt in degrees (requires calibration)</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="sensorPPM">
<item-type>Number:Density</item-type>
<label>Concentration</label>
<description>Gas concentration in ppm</description>
<state readOnly="true" pattern="%d %unit%">
</state>
</channel-type>
<channel-type id="sensorValve">
<item-type>String</item-type>
<label>Valve</label>
<description>Current valve status: closed/opened/not_connected/failure/closing/opening/checking</description>
<state readOnly="true">
<options>
<option value="closed">closed</option>
<option value="opened">opened</option>
<option value="not_connected">not connected</option>
<option value="failure">failure</option>
<option value="closing">closing</option>
<option value="opening">opening</option>
<option value="checking">checking</option>
<option value="unknown">UNKNOWN</option>
</options>
</state>
</channel-type>
<channel-type id="sensorError">
<item-type>String</item-type>
<label>Last Error</label>
<description>Only valid in case of error</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="senseKey">
<item-type>String</item-type>
<label>IR Key to Send</label>
<description>Send a defined key code</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shellydevice">
<label>Shelly Device</label>
<description>A password protected or unknown device.</description>
<config-description>
<parameter name="userId" type="text" required="true">
<label>UserID</label>
<description>User ID for API access.</description>
</parameter>
<parameter name="password" type="text" required="true">
<label>Password</label>
<description>Password for API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>IP Address</label>
<description>IP Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
</config-description>
</thing-type>
<thing-type id="shellyunknown">
<label>Unknown Shelly Device</label>
<description>This device is currently not supported</description>
</thing-type>
</thing:thing-descriptions>