### Generation 2 Plus series
-| thing-type | Model | Vendor ID |
-| -------------------- | -------------------------------------------------------- | --------------------------------------------- |
-| shellyplus1 | Shelly Plus 1 with 1x relay | SNSW-001X16EU |
-| shellyplus1pm | Shelly Plus 1PM with 1x relay + power meter | SNSW-001P16EU |
-| shellyplus2pm-relay | Shelly Plus 2PM with 2x relay + power meter, relay mode | SNSW-002P16EU, SNSW-102P16EU |
-| shellyplus2pm-roller | Shelly Plus 2PM with 2x relay + power meter, roller mode | SNSW-002P16EU, SNSW-102P16EU |
-| shellyplusplug | Shelly Plug-S | SNPL-00112EU |
-| shellyplusplug | Shelly Plug-IT | SNPL-00110IT |
-| shellyplusplug | Shelly Plug-UK | SNPL-00112UK |
-| shellyplusplug | Shelly Plug-US | SNPL-00116US |
-| shellyplusi4 | Shelly Plus i4 with 4x AC input | SNSN-0024X |
-| shellyplusi4dc | Shelly Plus i4 with 4x DC input | SNSN-0D24X |
-| shellyplusht | Shelly Plus HT with temperature + humidity sensor | SNSN-0013A |
-| shellyplussmoke | Shelly Plus Smoke sensor | SNSN-0031Z |
+| thing-type | Model | Vendor ID |
+| -------------------- | -------------------------------------------------------- | ---------------------------- |
+| shellyplus1 | Shelly Plus 1 with 1x relay | SNSW-001X16EU |
+| shellyplus1pm | Shelly Plus 1PM with 1x relay + power meter | SNSW-001P16EU |
+| shellyplus2pm-relay | Shelly Plus 2PM with 2x relay + power meter, relay mode | SNSW-002P16EU, SNSW-102P16EU |
+| shellyplus2pm-roller | Shelly Plus 2PM with 2x relay + power meter, roller mode | SNSW-002P16EU, SNSW-102P16EU |
+| shellyplusplug | Shelly Plug-S | SNPL-00112EU |
+| shellyplusplug | Shelly Plug-IT | SNPL-00110IT |
+| shellyplusplug | Shelly Plug-UK | SNPL-00112UK |
+| shellyplusplug | Shelly Plug-US | SNPL-00116US |
+| shellyplusi4 | Shelly Plus i4 with 4x AC input | SNSN-0024X |
+| shellyplusi4dc | Shelly Plus i4 with 4x DC input | SNSN-0D24X |
+| shellyplusht | Shelly Plus HT with temperature + humidity sensor | SNSN-0013A |
+| shellyplussmoke | Shelly Plus Smoke sensor | SNSN-0031Z |
### Generation 2 Pro series
| shellypro3em | Shelly Pro 3 with 3 integrated power meters | SPEM-003CEBEU |
| shellypro4pm | Shelly Pro 4 PM with 4x relay + power meter | SPSW-004PE16EU, SPSW-104PE16EU |
+### Shelly BLU
+
+| thing-type | Model | Vendor ID |
+| ----------------- | ------------------------------------------------------ | --------- |
+| shellyblubutton | Shelly BLU Button 1 | SBBT |
+| shellybludw | Shelly BLU Door/Windows | SBDW |
+
+
## Binding Configuration
The binding has the following configuration options:
You could use Shelly Manager (doc/ShellyManager.md) to easily do the setup (configuring the openHAB host as CoAP peer address).
Keep Multicast mode if you have multiple hosts, which should receive the CoAP updates.
+### Discovery of BLU Devices
+
+The BLU devices use Bluetooth Low Energy (BLE).
+The binding can't communicate directly with the device, but the Plus/Pro series with firmware 0.14.1 or newer could be used as a gateway.
+The binding automatically installs a script on the Shelly Device (oh-blu-scanner), which forwards the BLU events to the binding using the WebSocket channel.
+
+Follow these steps to add the Shelly BLU Device to openHAB
+- Make sure a Shelly is near by the BLU device, enable Bluetooh on this device (the Bluetooth Gateway mode is not required)
+- Add this thing to openHAB, make sure thing gets online
+- Enable "BLU Gateway Support" in the thing configuration of the Shelly device acting as gateway.
+- Now press the button on your BLU device, this wakes up the device and the script forwards this event to the binding
+- As a result the corresponding thing should show up in the Inbox
+- Add the thing (at this point no channels are created), the new thing will show status CONFIG_PENDING
+- Click the device button again, the binding gets another event and creates the channels and thing changes status to ONLINE
+- Finally link the channels to the equipment in the model
+
+Note: During initialization the script 'oh-blu-scanner.js' gets installed and activated on the Shelly Gateway device.
+
+Every time an event is received sensors#lastUpdate and channels are updated with the reported values.
+device#wifiSignal indicates the Bluetooth signal strength and gets updated when the device sends an event.
+
+The binding supports multiple Shelly Plus/Pro as gateway devices unless they are added as thing and are ONLINE.
+
### Password Protected Devices
The Shelly devices can be configured to require authorization through a user id and password.
| eventsRoller | true: register event "trigger" when the roller updates status | no | true for roller devices |
| favoriteUP | 0-4: Favorite id for UP (see Roller Favorites) | no | 0 = no favorite id |
| favoriteDOWN | 0-4: Favorite id for DOWN (see Roller Favorites) | no | 0 = no favorite id |
+| enableBluGateway | true: Active BLU gateway support (install script) | no | false ]
### General Notes
| TEMP_OVER | Above "temperature over" threshold |
| VIBRATION | A vibration/tamper was detected (DW2 only) |
-Refer to section [Full Example](#full-example) for examples how to catch alarm triggers in openHAB rules
+Refer to section [Full Example](#full-example) for examples how to catch alarm triggers in openHAB rules.
## Channels
| | timerActive | Switch | yes | Relay #1: ON: An auto-on/off timer is active |
| | button | Trigger | yes | Event trigger, see section Button Events |
+## Shelly BLU Devices
+
+### Shelly BLU Button 1 (thing-type: shellyblubutton)
+
+See notes on discovery of Shelly BLU devices above.
+
+| Group | Channel | Type | read-only | Description |
+| ------- | ------------- | -------- | --------- | ----------------------------------------------------------------------------------- |
+| status | lastEvent | String | yes | Last event type (S/SS/SSS/L) |
+| | eventCount | Number | yes | Counter gets incremented every time the device issues a button event. |
+| | button | Trigger | yes | Event trigger with payload, see SHORT_PRESSED or LONG_PRESSED |
+| | lastUpdate | DateTime | yes | Timestamp of the last measurement |
+| battery | batteryLevel | Number | yes | Battery Level in % |
+| | lowBattery | Switch | yes | Low battery alert (< 20%) |
+| device | gatewayDevice | String | yes | Shelly forwarded last status update (BLU gateway), could vary from packet to packet |
+
+
+
+### Shelly BLU Door/Window Sensor (thing-type: shellybludw)
+
+See notes on discovery of Shelly BLU devices above.
+
+| Group | Channel | Type | read-only | Description |
+| ------- | ------------- | -------- | --------- | ----------------------------------------------------------------------------------- |
+| sensors | state | Contact | yes | OPEN: Contact is open, CLOSED: Contact is closed |
+| | lux | Number | yes | Brightness in Lux |
+| | tilt | Number | yes | Tilt in ° (angle), -1 indicates that the sensor is not calibrated |
+| | lastUpdate | DateTime | yes | Timestamp of the last update (any sensor value changed) |
+| battery | batteryLevel | Number | yes | Battery Level in % |
+| | lowBattery | Switch | yes | Low battery alert (< 20%) |
+| device | gatewayDevice | String | yes | Shelly forwarded last status update (BLU gateway), could vary from packet to packet |
+
## Full Example
### shelly.things
Number item=Shelly_Power
}
}
-```
THING_TYPE_SHELLYPLUSSMOKE, //
THING_TYPE_SHELLYPLUSPLUGS, //
THING_TYPE_SHELLYPLUSPLUGUS, //
+ THING_TYPE_SHELLYBLUBUTTON, //
+ THING_TYPE_SHELLYBLUDW, //
THING_TYPE_SHELLYPROTECTED, //
THING_TYPE_SHELLYUNKNOWN);
// Thing Configuration Properties
public static final String CONFIG_DEVICEIP = "deviceIp";
+ public static final String CONFIG_DEVICEADDRESS = "deviceAddress";
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_DEV_TYPE = "deviceType";
public static final String PROPERTY_DEV_MODE = "deviceMode";
public static final String PROPERTY_DEV_GEN = "deviceGeneration";
+ public static final String PROPERTY_GW_DEVICE = "gatewayDevice";
public static final String PROPERTY_HWREV = "deviceHwRev";
public static final String PROPERTY_HWBATCH = "deviceHwBatch";
public static final String PROPERTY_UPDATE_PERIOD = "devUpdatePeriod";
// 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_GATEWAY = "gatewayDevice";
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 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
public static final int VIBRATION_FILTER_SEC = 5; // Absore duplicate vibration events for xx sec
+
+ public static final String BUNDLE_RESOURCE_SNIPLETS = "sniplets"; // where to find code sniplets in the bundle
+ public static final String BUNDLE_RESOURCE_SCRIPTS = "scripts"; // where to find scrips in the bundle
}
import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
+import org.openhab.binding.shelly.internal.handler.ShellyBluSensorHandler;
import org.openhab.binding.shelly.internal.handler.ShellyLightHandler;
import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.handler.ShellyProtectedHandler;
this.coapServer = new Shelly1CoapServer();
}
+ @Activate
+ void activate() {
+ thingTable.startDiscoveryService(bundleContext);
+ }
+
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|| thingType.equals(THING_TYPE_SHELLYRGBW2_WHITE_STR)
|| thingType.equals(THING_TYPE_SHELLYRGBW2_WHITE_STR) || thingType.equals(THING_TYPE_SHELLYDUORGBW_STR)
|| thingType.equals(THING_TYPE_SHELLYVINTAGE_STR)) {
- logger.debug("{}: Create new thing of type {}Â using ShellyLightHandler", thing.getLabel(),
+ logger.debug("{}: Create new thing of type {} using ShellyLightHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyLightHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
+ } else if (thingType.startsWith("shellyblu")) {
+ logger.debug("{}: Create new thing of type {} using ShellyBluSensorHandler", thing.getLabel(),
+ thingTypeUID.toString());
+ handler = new ShellyBluSensorHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
} else if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
- logger.debug("{}: Create new thing of type {}Â using ShellyRelayHandler", thing.getLabel(),
+ logger.debug("{}: Create new thing of type {} using ShellyRelayHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyRelayHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
}
return null;
}
- public Map<String, ShellyManagerInterface> getThingHandlers() {
- Map<String, ShellyManagerInterface> table = new HashMap<>();
- for (Map.Entry<String, ShellyThingInterface> entry : thingTable.getTable().entrySet()) {
- table.put(entry.getKey(), (ShellyManagerInterface) entry.getValue());
- }
- return table;
- }
-
/**
* Remove handler of things.
*/
@Override
protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) {
if (thingHandler instanceof ShellyBaseHandler) {
+ ((ShellyBaseHandler) thingHandler).stop();
String uid = thingHandler.getThing().getUID().getAsString();
thingTable.removeThing(uid);
}
public ShellyBindingConfiguration getBindingConfig() {
return bindingConfig;
}
+
+ public Map<String, ShellyManagerInterface> getThingHandlers() {
+ Map<String, ShellyManagerInterface> table = new HashMap<>();
+ for (Map.Entry<String, ShellyThingInterface> entry : thingTable.getTable().entrySet()) {
+ table.put(entry.getKey(), (ShellyManagerInterface) entry.getValue());
+ }
+ return table;
+ }
}
string[1]);
} else if (isMalformedURL()) {
message = "Invalid URL: " + url;
+ } else if (isJsonError()) {
+ message = getString(getMessage());
} else if (isTimeout()) {
message = "API Timeout for " + url;
} else if (!isConnectionError()) {
ShellySettingsStatus getStatus() throws ShellyApiException;
- void setLedStatus(String ledName, Boolean value) throws ShellyApiException;
+ void setLedStatus(String ledName, boolean value) throws ShellyApiException;
void setSleepTime(int value) throws ShellyApiException;
void setRelayTurn(int id, String turnMode) throws ShellyApiException;
- public void resetMeterTotal(int id) throws ShellyApiException;
+ void resetMeterTotal(int id) throws ShellyApiException;
- public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException;
+ ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException;
void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException;
void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException;
- // Valve
void setValveMode(int id, boolean auto) throws ShellyApiException;
void setValveTemperature(int valveId, int value) throws ShellyApiException;
void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException;
+ void postEvent(String device, String index, String event, Map<String, String> parms) throws ShellyApiException;
+
void close();
+
+ void startScan();
}
public boolean auth = false;
public boolean alwaysOn = true;
public boolean isGen2 = false;
+ public boolean isBlu = false;
+ public String gateway = "";
public String hwRev = "";
public String hwBatchId = "";
// Shelly UNI uses ext_temperature array, reformat to avoid GSON exception
json = json.replace("ext_temperature", "ext_temperature_array");
}
+ if (json.contains("\"ext_humidity\":{\"0\":[{")) {
+ // Shelly UNI uses ext_humidity array, reformat to avoid GSON exception
+ json = json.replace("ext_humidity", "ext_humidity_array");
+ }
settingsJson = json;
settings = fromJson(gson, json, ShellySettingsGlobal.class);
return;
}
+ isBlu = thingType.startsWith("shellyblu"); // e.g. SBBT for BU Button
+
isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2);
isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR);
isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR)
boolean isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR);
boolean isUNI = thingType.equals(THING_TYPE_SHELLYUNI_STR);
isHT = thingType.equals(THING_TYPE_SHELLYHT_STR) || thingType.equals(THING_TYPE_SHELLYPLUSHT_STR);
- isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR);
+ isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR)
+ || thingType.equals(THING_TYPE_SHELLYBLUDW_STR);
isMotion = thingType.startsWith(THING_TYPE_SHELLYMOTION_STR);
isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR);
isIX = thingType.equals(THING_TYPE_SHELLYIX3_STR) || thingType.equals(THING_TYPE_SHELLYPLUSI4_STR)
|| thingType.equals(THING_TYPE_SHELLYPLUSI4DC_STR);
- isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR) || thingType.equals(THING_TYPE_SHELLYBUTTON2_STR);
+ isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR) || thingType.equals(THING_TYPE_SHELLYBUTTON2_STR)
+ || thingType.equals(THING_TYPE_SHELLYBLUBUTTON_STR);
isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isMotion || isSense || isTRV;
hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion || isTRV;
isTRV = thingType.equals(THING_TYPE_SHELLYTRV_STR);
} else if (hasRelays) {
return numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx;
} else if (isRGBW2) {
- return settings.lights == null || settings.lights.size() <= 1 ? CHANNEL_GROUP_LIGHT_CONTROL
+ return settings.lights == null || settings.lights != null && settings.lights.size() <= 1
+ ? CHANNEL_GROUP_LIGHT_CONTROL
: CHANNEL_GROUP_LIGHT_CHANNEL + idx;
} else if (isLight) {
return CHANNEL_GROUP_LIGHT_CONTROL;
HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
}
fillPostData(request, data);
- logger.trace("{}: HTTP {} for {} {}", thingName, method, url, data);
+ logger.trace("{}: HTTP {} for {} {}\n{}", thingName, method, url, data, request.getHeaders());
// Do request and get response
ContentResponse contentResponse = request.send();
apiResult = new ShellyApiResult(contentResponse);
apiResult.httpCode = contentResponse.getStatus();
String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
- logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
+ logger.trace("{}: HTTP Response {}: {}\n{}", thingName, contentResponse.getStatus(), response,
+ contentResponse.getHeaders());
if (response.contains("\"error\":{")) { // Gen2
Shelly2RpcBaseMessage message = gson.fromJson(response, Shelly2RpcBaseMessage.class);
StringContentProvider postData;
postData = new StringContentProvider(type, data, StandardCharsets.UTF_8);
request.content(postData);
- request.header(HttpHeader.CONTENT_LENGTH, Long.toString(postData.getLength()));
+ // request.header(HttpHeader.CONTENT_LENGTH, Long.toString(postData.getLength()));
}
}
public int getTimeoutsRecovered() {
return timeoutsRecovered;
}
+
+ public void postEvent(String device, String index, String event, Map<String, String> parms)
+ throws ShellyApiException {
+ }
}
public ArrayList<ShellyRollerStatus> rollers;
public ArrayList<ShellySettingsLight> lights;
public ArrayList<ShellySettingsMeter> meters;
+
public ArrayList<ShellySettingsEMeter> emeters;
+ public Double totalCurrent;
+ public Double totalPower;
+ public Double totalReturned;
+
@SerializedName("ext_temperature")
public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
@SerializedName("ext_humidity")
*/
@NonNullByDefault
public interface Shelly1CoIoTInterface {
- int getVersion();
+ public int getVersion();
- CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap);
+ public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap);
- void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap);
+ public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap);
- boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
+ public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
Map<String, State> updates, ShellyColorUtils col);
- String getLastWakeup();
+ public String getLastWakeup();
}
"{}: Check button[{}] for event trigger (inButtonMode={}, isButton={}, hasBattery={}, serial={}, count={}, lastEventCount[{}]={}",
thingName, idx, profile.inButtonMode(idx), profile.isButton, profile.hasBattery, serial, count, idx,
lastEventCount[idx]);
- if (profile.inButtonMode(idx) && ((profile.hasBattery && count == 1)
- || (lastEventCount[idx] != -1 && count != lastEventCount[idx]))) {
+ if (profile.inButtonMode(idx) && ((profile.hasBattery && count == 1) || lastEventCount[idx] == -1
+ || count != lastEventCount[idx])) {
if (!profile.isButton || (profile.isButton && (serial != 0x200))) { // skip duplicate on wake-up
logger.debug("{}: Trigger event {}", thingName, inputEvent[idx]);
thingHandler.triggerButton(group, idx, inputEvent[idx]);
List<Option> options = response.getOptions().asSortedList();
String ip = response.getSourceContext().getPeerAddress().toString();
- boolean match = ip.contains(config.deviceIp);
+ boolean match = ip.contains("/" + config.deviceIp + ":");
if (!match) {
// We can't identify device by IP, so we need to check the CoAP header's Global Device ID
for (Option opt : options) {
*/
@NonNullByDefault
public interface Shelly1CoapListener {
- void processResponse(@Nullable Response response);
+ public void processResponse(@Nullable Response response);
}
@Override
public void resetMeterTotal(int id) throws ShellyApiException {
- callApi(SHELLY_URL_STATUS_EMETER + "/" + id + "/reset_totals=1", ShellyStatusRelay.class);
+ callApi(SHELLY_URL_STATUS_EMETER + "/" + id + "?reset_totals=true", ShellyStatusRelay.class);
}
@Override
} else if (profile.isLight) {
type = SHELLY_CLASS_LIGHT;
}
- String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + (int) value;
+ String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value;
httpRequest(uri);
}
}
@Override
- public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
+ public void setLedStatus(String ledName, boolean value) throws ShellyApiException {
httpRequest(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
}
@Override
public void close() {
}
+
+ @Override
+ public void startScan() {
+ }
}
return false;
}
+ if (em.totalCurrent != null) {
+ status.totalCurrent = em.totalCurrent;
+ }
+ if (em.totalActPower != null) {
+ status.totalPower = em.totalActPower;
+ }
+ if (em.totalAprtPower != null) {
+ status.totalReturned = em.totalAprtPower;
+ }
+
ShellySettingsMeter sm = new ShellySettingsMeter();
ShellySettingsEMeter emeter = status.emeters.get(0);
sm.isValid = emeter.isValid = true;
throw new ShellyApiException("Thing/profile not initialized!");
}
- ShellyDeviceProfile getProfile() throws ShellyApiException {
+ protected ShellyDeviceProfile getProfile() throws ShellyApiException {
if (thing != null) {
return thing.getProfile();
}
public static final String SHELLYRPC_METHOD_WSSETCONFIG = "WS.SetConfig";
public static final String SHELLYRPC_METHOD_SMOKE_SETCONFIG = "Smoke.SetConfig";
public static final String SHELLYRPC_METHOD_SMOKE_MUTE = "Smoke.Mute";
+ public static final String SHELLYRPC_METHOD_SCRIPT_LIST = "Script.List";
+ public static final String SHELLYRPC_METHOD_SCRIPT_SETCONFIG = "Script.SetConfig";
+ public static final String SHELLYRPC_METHOD_SCRIPT_GETSTATUS = "Script.GetStatus";
+ public static final String SHELLYRPC_METHOD_SCRIPT_DELETE = "Script.Delete";
+ public static final String SHELLYRPC_METHOD_SCRIPT_CREATE = "Script.Create";
+ public static final String SHELLYRPC_METHOD_SCRIPT_GETCODE = "Script.GetCode";
+ public static final String SHELLYRPC_METHOD_SCRIPT_PUTCODE = "Script.PutCode";
+ public static final String SHELLYRPC_METHOD_SCRIPT_START = "Script.Start";
+ public static final String SHELLYRPC_METHOD_SCRIPT_STOP = "Script.Stop";
public static final String SHELLYRPC_METHOD_NOTIFYSTATUS = "NotifyStatus"; // inbound status
public static final String SHELLYRPC_METHOD_NOTIFYFULLSTATUS = "NotifyFullStatus"; // inbound status from bat device
public static final String SHELLY2_EVENT_WIFICONNFAILED = "sta_connect_fail";
public static final String SHELLY2_EVENT_WIFIDISCONNECTED = "sta_disconnected";
+ // BLU events
+ public static final String SHELLY2_BLU_GWSCRIPT = "oh-blu-scanner.js";
+ public static final String SHELLY2_EVENT_BLUPREFIX = "oh-blu.";
+ public static final String SHELLY2_EVENT_BLUSCAN = SHELLY2_EVENT_BLUPREFIX + "scan_result";
+ public static final String SHELLY2_EVENT_BLUDATA = SHELLY2_EVENT_BLUPREFIX + "data";
+
// Error Codes
public static final String SHELLY2_ERROR_OVERPOWER = "overpower";
public static final String SHELLY2_ERROR_OVERTEMP = "overtemp";
@SerializedName("n_current")
public Double nCurrent;
+
+ @SerializedName("total_current")
+ public Double totalCurrent;
+ @SerializedName("total_act_power")
+ public Double totalActPower;
+ @SerializedName("total_aprt_power")
+ public Double totalAprtPower;
}
public static class Shelly2DeviceStatusEmData {
// Cloud.SetConfig
public Shelly2ConfigParms config;
+ // Script
+ public String name;
+
public Shelly2RpcRequestParams withConfig() {
config = new Shelly2ConfigParms();
return this;
params.pos = pos;
return this;
}
+
+ public Shelly2RpcRequest withName(String name) {
+ params.name = name;
+ return this;
+ }
}
public static class Shelly2WsConfigResponse {
public Shelly2WsConfigResult result;
}
+ public static class ShellyScriptListResponse {
+ public static class ShellyScriptListEntry {
+ public Integer id;
+ public String name;
+ public Boolean enable;
+ public Boolean running;
+ }
+
+ public ArrayList<ShellyScriptListEntry> scripts;
+ }
+
+ public static class ShellyScriptResponse {
+ public Integer id;
+ public Boolean running;
+ public Integer len;
+ public String data;
+ }
+
+ public static class ShellyScriptPutCodeParams {
+ public Integer id;
+ public String code;
+ public Boolean append;
+ }
+
public static class Shelly2RpcBaseMessage {
// Basic message format, e.g.
// {"id":1,"src":"localweb528","method":"Shelly.GetConfig"}
public Integer id;
public String src;
public String dst;
+ public String component;
public String method;
public Object params;
+ public String event;
public Object result;
public Shelly2AuthRequest auth;
public Shelly2RpcMessageError error;
public String algorithm;
}
+ // BTHome samples
+ // BLU Button 1
+ // {"component":"script:2", "id":2, "event":"oh-blu.scan_result",
+ // "data":{"addr":"bc:02:6e:c3:a6:c7","rssi":-62,"tx_power":-128}, "ts":1682877414.21}
+ // {"component":"script:2", "id":2, "event":"oh-blu.data",
+ // "data":{"encryption":false,"BTHome_version":2,"pid":205,"Battery":100,"Button":1,"addr":"b4:35:22:fd:b3:81","rssi":-68},
+ // "ts":1682877399.22}
+ //
+ // BLU Door Window
+ // {"component":"script:2", "id":2, "event":"oh-blu.scan_result",
+ // "data":{"addr":"bc:02:6e:c3:a6:c7","rssi":-62,"tx_power":-128}, "ts":1682877414.21}
+ // {"component":"script:2", "id":2, "event":"oh-blu.data",
+ // "data":{"encryption":false,"BTHome_version":2,"pid":38,"Battery":100,"Illuminance":0,"Window":1,"Rotation":0,"addr":"bc:02:6e:c3:a6:c7","rssi":-62},
+ // "ts":1682877414.25}
+
+ public class Shelly2NotifyEventMessage {
+ public String addr;
+ public String name;
+ public Boolean encryption;
+ @SerializedName("BTHome_version")
+ public Integer bthVersion;
+ public Integer pid;
+ @SerializedName("Battery")
+ public Integer battery;
+ @SerializedName("Button")
+ public Integer buttonEvent;
+ @SerializedName("Illuminance")
+ public Integer illuminance;
+ @SerializedName("Window")
+ public Integer windowState;
+ @SerializedName("Rotation")
+ public Double rotation;
+
+ public Integer rssi;
+ public Integer tx_power;
+ }
+
public class Shelly2NotifyEvent {
public Integer id;
public Double ts;
public String component;
public String event;
+ public Shelly2NotifyEventMessage data;
public String msg;
public Integer reason;
@SerializedName("cfg_rev")
}
public static class Shelly2RpcNotifyEvent {
+ public String src;
public Double ts;
- Shelly2NotifyEventData params;
+ public Shelly2NotifyEventData params;
}
}
import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest.Shelly2RpcRequestParams;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse.Shelly2WsConfigResult;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptListResponse;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptListResponse.ShellyScriptListEntry;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptPutCodeParams;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptResponse;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
private final Logger logger = LoggerFactory.getLogger(Shelly2ApiRpc.class);
private final @Nullable ShellyThingTable thingTable;
- private boolean initialized = false;
+ protected boolean initialized = false;
private boolean discovery = false;
private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
private Shelly2AuthResponse authInfo = new Shelly2AuthResponse();
return initialized;
}
+ @Override
+ public void startScan() {
+ if (config.enableBluGateway) {
+ try {
+ installScript(SHELLY2_BLU_GWSCRIPT);
+ } catch (ShellyApiException e) {
+ }
+ }
+ }
+
+ @SuppressWarnings("null")
@Override
public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
if (!discovery) {
getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device
+
+ try {
+ logger.debug("{}: BLU Gateway support enabled for this device: {}", thingName, config.enableBluGateway);
+ if (config.enableBluGateway) {
+ if (getBool(profile.settings.bluetooth)) {
+ installScript(SHELLY2_BLU_GWSCRIPT);
+ } else {
+ logger.debug("{}: Bluetooth needs to be enabled to activate BLU Gateway mode", thingName);
+ }
+ }
+ } catch (ShellyApiException e) {
+ logger.debug("{}: Device config failed", thingName, e);
+ }
}
return profile;
}
}
+ protected void installScript(String script) throws ShellyApiException {
+ String json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_LIST));
+ ShellyScriptListResponse scriptList = gson.fromJson(json, ShellyScriptListResponse.class);
+ Integer ourId = -1;
+ String code = "";
+
+ logger.debug("{}: Install or restart script {} on Shelly Device", thingName, script);
+ boolean running = false, upload = false;
+ if (scriptList != null) {
+ for (ShellyScriptListEntry s : scriptList.scripts) {
+ if (s.name.startsWith(script)) {
+ ourId = s.id;
+ running = s.running;
+ logger.debug("{}: Script {} is already installed, id={}", thingName, script, ourId);
+ }
+ }
+ }
+
+ // get script code from bundle resources
+ String file = BUNDLE_RESOURCE_SCRIPTS + "/" + script;
+ ClassLoader cl = Shelly2ApiRpc.class.getClassLoader();
+ if (cl != null) {
+ try (InputStream inputStream = cl.getResourceAsStream(file)) {
+ if (inputStream != null) {
+ code = new BufferedReader(new InputStreamReader(inputStream)).lines()
+ .collect(Collectors.joining("\n"));
+ }
+ } catch (IOException | UncheckedIOException e) {
+ logger.debug("{}: Installation of script {} failed: Unable to read {} from bundle resources!",
+ thingName, script, file, e);
+ }
+ }
+
+ boolean restart = false;
+ if (ourId == -1) {
+ // script not installed -> install it
+ upload = true;
+ } else {
+ try {
+ // verify that the same code version is active (avoid unnesesary flash updates)
+ json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_GETCODE).withId(ourId));
+ ShellyScriptResponse rsp = gson.fromJson(json, ShellyScriptResponse.class);
+ if (!rsp.data.trim().equals(code.trim())) {
+ logger.debug("{}: A script version was found, update to newest one", thingName);
+ upload = true;
+ } else {
+ logger.debug("{}: Same script version was found, restart", thingName);
+ restart = true;
+ }
+ } catch (ShellyApiException e) {
+ logger.debug("{}: Unable to read current script code -> force update (deviced returned: {})", thingName,
+ e.getMessage());
+ upload = true;
+ }
+ }
+
+ if (restart || (running && upload)) {
+ json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_STOP).withId(ourId));
+ // first stop running script
+ running = false;
+ }
+ if (upload && ourId != -1) {
+ // Delete existing script
+ logger.debug("{}: Delete existing script", thingName);
+ json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_DELETE).withId(ourId));
+ }
+
+ if (upload) {
+ logger.debug("{}: Script will be installed...", thingName);
+
+ // Create new script, get id
+ json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_CREATE).withName(script));
+ ShellyScriptResponse rsp = gson.fromJson(json, ShellyScriptResponse.class);
+ if (rsp != null) {
+ ourId = rsp.id;
+ logger.debug("{}: Script has been created, id={}", thingName, ourId);
+ upload = true;
+ }
+ }
+
+ if (upload) {
+ // Put script code for generated id
+ ShellyScriptPutCodeParams parms = new ShellyScriptPutCodeParams();
+ parms.id = ourId;
+ parms.append = false;
+ int length = code.length(), processed = 0, chunk = 1;
+ do {
+ int nextlen = Math.min(1024, length - processed);
+ parms.code = code.substring(processed, processed + nextlen);
+ logger.debug("{}: Uploading chunk {} of script (total {} chars, {} processed)", thingName, chunk,
+ length, processed);
+ apiRequest(SHELLYRPC_METHOD_SCRIPT_PUTCODE, parms, String.class);
+ processed += nextlen;
+ chunk++;
+ parms.append = true;
+ } while (processed < length);
+ running = false;
+
+ Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
+ params.config.enable = true;
+ apiRequest(SHELLYRPC_METHOD_SCRIPT_SETCONFIG, params, String.class);
+ }
+
+ if (!running) {
+ // Script was created or is there and stopped -> start it
+ json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_START).withId(ourId));
+ logger.debug("{}: Script {} was {} successful", thingName, script,
+ restart ? "restarted" : "installed and started");
+ }
+ }
+
@Override
public void onConnect(String deviceIp, boolean connected) {
if (thing == null && thingTable != null) {
* categories (e.g. bulbs)
*/
@Override
- public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
+ public void setLedStatus(String ledName, boolean value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
rpcSocket.sendMessage(gson.toJson(request)); // submit, result wull be async
}
+ @SuppressWarnings("null")
public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
String json = "";
Shelly2RpcBaseMessage req = buildRequest(method, params);
throw e;
}
}
- json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
- return fromJson(gson, json, classOfT);
+ Shelly2RpcBaseMessage response = gson.fromJson(json, Shelly2RpcBaseMessage.class);
+ if (response == null) {
+ throw new IllegalArgumentException("Unable to cover API result to obhect");
+ }
+ if (response.result != null) {
+ // return sub element result as requested class type
+ json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
+ return fromJson(gson, json, classOfT);
+ } else {
+ // return direct format
+ return gson.fromJson(json, classOfT);
+ }
}
public <T> T apiRequest(Shelly2RpcRequest request, Class<T> classOfT) throws ShellyApiException {
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
+import org.openhab.binding.shelly.internal.handler.ShellyBluSensorHandler;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.slf4j.Logger;
handler.onNotifyStatus(status);
return;
case SHELLYRPC_METHOD_NOTIFYEVENT:
- handler.onNotifyEvent(fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
- return;
+ Shelly2RpcNotifyEvent events = fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class);
+ events.src = message.src;
+ if (events.params == null || events.params.events == null) {
+ logger.debug("{}: Malformed event data: {}", thingName, receivedMessage);
+ } else {
+ for (Shelly2NotifyEvent e : events.params.events) {
+ if (getString(e.event).startsWith(SHELLY2_EVENT_BLUPREFIX)) {
+ String address = getString(e.data.addr).replaceAll(":", "");
+ if (thingTable != null && thingTable.findThing(address) != null) {
+ if (thingTable != null) { // known device
+ ShellyThingInterface thing = thingTable.getThing(address);
+ Shelly2ApiRpc api = (Shelly2ApiRpc) thing.getApi();
+ handler = api.getRpcHandler();
+ handler.onNotifyEvent(
+ fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
+ }
+ } else { // new device
+ if (e.event.equals(SHELLY2_EVENT_BLUSCAN)) {
+ ShellyBluSensorHandler.addBluThing(message.src, e, thingTable);
+ } else {
+ logger.debug("{}: NotifyEvent {} for unknown device {}", message.src,
+ e.event, e.data.name);
+ }
+ }
+ }
+ }
+ }
+ break;
default:
handler.onMessage(receivedMessage);
}
logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName,
getString(message.src), receivedMessage);
}
- } catch (ShellyApiException | IllegalArgumentException | NullPointerException e) {
+ } catch (ShellyApiException | IllegalArgumentException e) {
+ logger.debug("{}: Unable to process Rpc message ({}): {}", thingName, e.getMessage(), receivedMessage);
+ } catch (NullPointerException e) {
logger.debug("{}: Unable to process Rpc message: {}", thingName, receivedMessage, e);
}
}
@NonNullByDefault
public interface Shelly2RpctInterface {
- void onConnect(String deviceIp, boolean connected);
+ public void onConnect(String deviceIp, boolean connected);
- void onMessage(String decodedmessage);
+ public void onMessage(String decodedmessage);
- void onNotifyStatus(Shelly2RpcNotifyStatus message);
+ public void onNotifyStatus(Shelly2RpcNotifyStatus message);
- void onNotifyEvent(Shelly2RpcNotifyEvent message);
+ public void onNotifyEvent(Shelly2RpcNotifyEvent message);
- void onClose(int statusCode, String reason);
+ public void onClose(int statusCode, String reason);
- void onError(Throwable cause);
+ public void onError(Throwable cause);
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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.api2;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.openhab.binding.shelly.internal.api.ShellyApiException;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorSleepMode;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsInput;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorAccel;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorBat;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorLux;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorState;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
+import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
+import org.openhab.binding.shelly.internal.handler.ShellyBluSensorHandler;
+import org.openhab.binding.shelly.internal.handler.ShellyComponents;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link ShellyBluApi} implementsBLU interface
+ *
+ * @author Markus Michels - Initial contribution
+ */
+public class ShellyBluApi extends Shelly2ApiRpc {
+ private static final Logger logger = LoggerFactory.getLogger(ShellyBluApi.class);
+ private boolean connected = false; // true = BLU devices has connected
+ private ShellySettingsStatus deviceStatus = new ShellySettingsStatus();
+ private int lastPid = -1;
+
+ private static final Map<String, String> MAP_INPUT_EVENT_TYPE = new HashMap<>();
+ static {
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_1PUSH, SHELLY_BTNEVENT_1SHORTPUSH);
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_2PUSH, SHELLY_BTNEVENT_2SHORTPUSH);
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_3PUSH, SHELLY_BTNEVENT_3SHORTPUSH);
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_LPUSH, SHELLY_BTNEVENT_LONGPUSH);
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_LSPUSH, SHELLY_BTNEVENT_LONGSHORTPUSH);
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_SLPUSH, SHELLY_BTNEVENT_SHORTLONGPUSH);
+ MAP_INPUT_EVENT_TYPE.put("1", SHELLY_BTNEVENT_1SHORTPUSH);
+ MAP_INPUT_EVENT_TYPE.put("2", SHELLY_BTNEVENT_2SHORTPUSH);
+ MAP_INPUT_EVENT_TYPE.put("3", SHELLY_BTNEVENT_3SHORTPUSH);
+ MAP_INPUT_EVENT_TYPE.put("4", SHELLY_BTNEVENT_LONGPUSH);
+ }
+
+ /**
+ * Regular constructor - called by Thing handler
+ *
+ * @param thingName Symbolic thing name
+ * @param thing Thing Handler (ThingHandlerInterface)
+ */
+ public ShellyBluApi(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
+ super(thingName, thingTable, thing);
+
+ ShellyInputState input = new ShellyInputState();
+ deviceStatus.inputs = new ArrayList<>();
+ input.input = 0;
+ input.event = "";
+ input.eventCount = 0;
+ deviceStatus.inputs.add(input);
+ }
+
+ @Override
+ public void initialize() throws ShellyApiException {
+ if (!initialized) {
+ initialized = true;
+ connected = false;
+ } else {
+ }
+ }
+
+ @Override
+ public boolean isInitialized() {
+ return initialized;
+ }
+
+ @Override
+ public void setConfig(String thingName, ShellyThingConfiguration config) {
+ this.thingName = thingName;
+ this.config = config;
+ }
+
+ @Override
+ public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
+ ShellySettingsDevice info = new ShellySettingsDevice();
+ info.hostname = !config.serviceName.isEmpty() ? config.serviceName : "";
+ info.fw = "1234";
+ info.type = "SBBT";
+ info.mac = config.deviceAddress;
+ info.auth = false;
+ info.gen = 99;
+ return info;
+ }
+
+ @Override
+ public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
+ ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
+
+ profile.isBlu = true;
+ profile.settingsJson = "{}";
+ profile.thingName = thingName;
+ profile.name = getString(profile.settings.name);
+ if (profile.gateway.isEmpty()) {
+ profile.gateway = getThing().getProperty(PROPERTY_GW_DEVICE);
+ }
+
+ ShellySettingsDevice device = getDeviceInfo();
+ profile.settings.device = device;
+ profile.hostname = device.hostname;
+ profile.deviceType = device.type;
+ profile.mac = device.mac;
+ profile.auth = device.auth;
+ if (config.serviceName.isEmpty()) {
+ config.serviceName = getString(profile.hostname);
+ }
+ profile.fwDate = substringBefore(device.fw, "/");
+ profile.fwVersion = substringBefore(ShellyDeviceProfile.extractFwVersion(device.fw.replace("/", "/v")), "-");
+ profile.status.update.oldVersion = profile.fwVersion;
+ profile.status.hasUpdate = profile.status.update.hasUpdate = false;
+
+ if (profile.hasBattery) {
+ profile.settings.sleepMode = new ShellySensorSleepMode();
+ profile.settings.sleepMode.unit = "m";
+ profile.settings.sleepMode.period = 720;
+ }
+
+ if (profile.isButton) {
+ ShellySettingsInput settings = new ShellySettingsInput();
+ profile.numInputs = 1;
+ settings.btnType = SHELLY_BTNT_MOMENTARY;
+
+ if (profile.settings.inputs != null) {
+ profile.settings.inputs.set(0, settings);
+ } else {
+ profile.settings.inputs = new ArrayList<>();
+ profile.settings.inputs.add(settings);
+ }
+ profile.status = deviceStatus;
+ }
+
+ if (!connected) {
+ throw new ShellyApiException("BLU Device not yet connected");
+ }
+
+ profile.initialized = true;
+ return profile;
+ }
+
+ @Override
+ public ShellySettingsStatus getStatus() throws ShellyApiException {
+ if (!connected) {
+ throw new ShellyApiException("Thing is not yet initialized -> status not available");
+ }
+ return deviceStatus;
+ }
+
+ @Override
+ public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
+ if (!connected) {
+ throw new ShellyApiException("Thing is not yet initialized -> sensor data not available");
+ }
+
+ return sensorData;
+ }
+
+ @Override
+ public void onNotifyEvent(Shelly2RpcNotifyEvent message) {
+ logger.trace("{}: ShellyEvent received: {}", thingName, gson.toJson(message));
+
+ boolean updated = false;
+ ShellyBluSensorHandler t = (ShellyBluSensorHandler) thing;
+ if (t == null) {
+ logger.debug("{}: Thing is not initialized -> ignore event", thingName);
+ return;
+ }
+
+ try {
+ ShellyDeviceProfile profile = getProfile();
+
+ t.incProtMessages();
+
+ if (!connected) {
+ connected = true;
+ t.setThingOnline();
+ } else {
+ t.restartWatchdog();
+ }
+
+ for (Shelly2NotifyEvent e : message.params.events) {
+ logger.debug("{}: BluEvent received: {}", thingName, gson.toJson(message));
+ String event = getString(e.event);
+ if (event.startsWith(SHELLY2_EVENT_BLUPREFIX)) {
+ logger.debug("{}: BLU event {} received from address {}, pid={}", thingName, event,
+ getString(e.data.addr), getInteger(e.data.pid));
+ if (e.data.pid != null) {
+ int pid = e.data.pid;
+ if (pid == lastPid) {
+ logger.debug("{}: Duplicate packet for PID={} received, ignore", thingName, pid);
+ break;
+ }
+ lastPid = pid;
+ }
+ getThing().getProfile().gateway = message.src;
+ }
+
+ switch (event) {
+ case SHELLY2_EVENT_BLUSCAN:
+ if (e.data == null || e.data.addr == null) {
+ logger.debug("{}: Inconsistent BLU scan result ignored: {}", thingName,
+ gson.toJson(message));
+ break;
+ }
+ logger.debug("{}: BLU Device discovered", thingName);
+ if (e.data.name != null) {
+ profile.settings.name = buildBluServiceName(e.data.name, e.data.addr);
+ }
+ break;
+ case SHELLY2_EVENT_BLUDATA:
+ if (e.data == null || e.data.addr == null || e.data.pid == null) {
+ logger.debug("{}: Inconsistent BLU packet ignored: {}", thingName, gson.toJson(message));
+ break;
+ }
+
+ if (e.data.battery != null) {
+ if (sensorData.bat == null) {
+ sensorData.bat = new ShellySensorBat();
+ }
+ sensorData.bat.value = (double) e.data.battery;
+ }
+ if (e.data.rssi != null) {
+ deviceStatus.wifiSta.rssi = e.data.rssi;
+ }
+ if (e.data.windowState != null) {
+ if (sensorData.sensor == null) {
+ sensorData.sensor = new ShellySensorState();
+ }
+ sensorData.sensor.isValid = true;
+ sensorData.sensor.state = e.data.windowState == 1 ? SHELLY_API_DWSTATE_OPEN
+ : SHELLY_API_DWSTATE_CLOSE;
+ }
+ if (e.data.illuminance != null) {
+ if (sensorData.lux == null) {
+ sensorData.lux = new ShellySensorLux();
+ }
+ sensorData.lux.isValid = true;
+ sensorData.lux.value = (double) e.data.illuminance;
+ }
+ if (e.data.rotation != null) {
+ if (sensorData.accel == null) {
+ sensorData.accel = new ShellySensorAccel();
+ }
+ sensorData.accel.tilt = e.data.rotation.intValue();
+ }
+
+ if (e.data.buttonEvent != null) {
+ ShellyInputState input = deviceStatus.inputs != null ? deviceStatus.inputs.get(0)
+ : new ShellyInputState();
+ input.event = mapValue(MAP_INPUT_EVENT_TYPE, e.data.buttonEvent + "");
+ input.eventCount++;
+ deviceStatus.inputs.set(0, input);
+ // sensorData.inputs.set(0, input);
+
+ String group = getProfile().getInputGroup(0);
+ String suffix = profile.getInputSuffix(0);
+ t.updateChannel(group, CHANNEL_STATUS_EVENTTYPE + suffix, getStringType(input.event));
+ t.updateChannel(group, CHANNEL_STATUS_EVENTCOUNT + suffix, getDecimal(input.eventCount));
+ t.triggerButton(profile.getInputGroup(0), 0, input.event);
+ }
+ updated |= ShellyComponents.updateDeviceStatus(t, deviceStatus);
+ updated |= ShellyComponents.updateSensors(getThing(), deviceStatus);
+ break;
+ default:
+ super.onNotifyEvent(message);
+ }
+ }
+ } catch (ShellyApiException e) {
+ logger.debug("{}: Unable to process event", thingName, e);
+ t.incProtErrors();
+ }
+
+ if (updated) {
+
+ }
+ }
+
+ public static String buildBluServiceName(String name, String mac) throws IllegalArgumentException {
+ String model = name.contains("-") ? substringBefore(name, "-") : name; // e.g. SBBT-02C or just SBDW
+ switch (model) {
+ case SHELLYDT_BLUBUTTON:
+ return (THING_TYPE_SHELLYBLUBUTTON_STR + "-" + mac).toLowerCase();
+ case SHELLYDT_BLUDW:
+ return (THING_TYPE_SHELLYBLUDW_STR + "-" + mac).toLowerCase();
+ default:
+ throw new IllegalArgumentException("Unsupported BLU device model " + model);
+ }
+ }
+}
@NonNullByDefault
public class ShellyThingConfiguration {
public String deviceIp = ""; // ip address of thedevice
+ public String deviceAddress = ""; // IP address or MAC address for BLU devices
public String userId = ""; // userid for http basic auth
public String password = ""; // password for http basic auth
public String localIp = ""; // local ip addresses used to create callback url
public String localPort = "8080";
public String serviceName = "";
+
+ public boolean enableBluGateway = false;
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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.core.thing.Thing.PROPERTY_MAC_ADDRESS;
+
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Device discovery creates a thing in the inbox for each vehicle
+ * found in the data received from {@link ShellyBluDiscoveryService}.
+ *
+ * @author Markus Michels - Initial Contribution
+ *
+ */
+@NonNullByDefault
+public class ShellyBluDiscoveryService extends AbstractDiscoveryService {
+ private final Logger logger = LoggerFactory.getLogger(ShellyBluDiscoveryService.class);
+
+ private final BundleContext bundleContext;
+ private final ShellyThingTable thingTable;
+ private static final int TIMEOUT = 10;
+ private @Nullable ServiceRegistration<?> discoveryService;
+
+ public ShellyBluDiscoveryService(BundleContext bundleContext, ShellyThingTable thingTable) {
+ super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT);
+ this.bundleContext = bundleContext;
+ this.thingTable = thingTable;
+ }
+
+ @SuppressWarnings("null")
+ public void registerDeviceDiscoveryService() {
+ if (discoveryService == null) {
+ discoveryService = bundleContext.registerService(DiscoveryService.class.getName(), this,
+ new Hashtable<String, Object>());
+ }
+ }
+
+ @Override
+ protected void startScan() {
+ logger.debug("Starting BLU Discovery");
+ thingTable.startScan();
+ }
+
+ public void discoveredResult(ThingTypeUID tuid, String model, String serviceName, String address,
+ Map<String, Object> properties) {
+ ThingUID uid = ShellyThingCreator.getThingUID(serviceName, model, "", true);
+ logger.debug("Adding discovered thing with id {}", uid.toString());
+ properties.put(PROPERTY_MAC_ADDRESS, address);
+ String thingLabel = "Shelly BLU " + model + " (" + serviceName + ")";
+ DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
+ .withRepresentationProperty(PROPERTY_DEV_NAME).withLabel(thingLabel).build();
+ thingDiscovered(result);
+ }
+
+ public void unregisterDeviceDiscoveryService() {
+ if (discoveryService != null) {
+ discoveryService.unregister();
+ }
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ unregisterDeviceDiscoveryService();
+ }
+}
public static final String SHELLYDT_PRO4PM = "SPSW-004PE16EU";
public static final String SHELLYDT_PRO4PM_2 = "SPSW-104PE16EU";
+ // Shelly BLU Series
+ public static final String SHELLYDT_BLUBUTTON = "SBBT";
+ public static final String SHELLYDT_BLUDW = "SBDW";
+
// Thing names
public static final String THING_TYPE_SHELLY1_STR = "shelly1";
public static final String THING_TYPE_SHELLY1L_STR = "shelly1l";
public static final String THING_TYPE_SHELLYPRO3EM_STR = "shellypro3em";
public static final String THING_TYPE_SHELLYPRO4PM_STR = "shellypro4pm";
+ // Shelly BLU Series
+ public static final String THING_TYPE_SHELLYBLU_PREFIX = "shellyblu";
+ public static final String THING_TYPE_SHELLYBLUBUTTON_STR = THING_TYPE_SHELLYBLU_PREFIX + "button";
+ public static final String THING_TYPE_SHELLYBLUDW_STR = THING_TYPE_SHELLYBLU_PREFIX + "dw";
+
+ // Password protected or unknown device
public static final String THING_TYPE_SHELLYPROTECTED_STR = "shellydevice";
public static final String THING_TYPE_SHELLYUNKNOWN_STR = "shellyunknown";
public static final ThingTypeUID THING_TYPE_SHELLYPRO4PM = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYPRO4PM_STR);
+ // Shelly Blu series
+ public static final ThingTypeUID THING_TYPE_SHELLYBLUBUTTON = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYBLUBUTTON_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYBLUDW = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYBLUDW_STR);
+
private static final Map<String, String> THING_TYPE_MAPPING = new LinkedHashMap<>();
static {
// mapping by device type id
THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM, THING_TYPE_SHELLYPRO4PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM_2, THING_TYPE_SHELLYPRO4PM_STR);
+ // Blu Series
+ THING_TYPE_MAPPING.put(SHELLYDT_BLUBUTTON, THING_TYPE_SHELLYBLUBUTTON_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_BLUDW, THING_TYPE_SHELLYBLUDW_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);
import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
+import org.openhab.binding.shelly.internal.api2.ShellyBluApi;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.discovery.ShellyThingCreator;
private final int cacheCount = UPDATE_SETTINGS_INTERVAL_SECONDS / UPDATE_STATUS_INTERVAL_SECONDS;
private final boolean gen2;
+ private final boolean blu;
protected boolean autoCoIoT = false;
// Thing status
gen = "2";
}
gen2 = "2".equals(gen);
- this.api = !gen2 ? new Shelly1HttpApi(thingName, this) : new Shelly2ApiRpc(thingName, thingTable, this);
+ blu = thingType.startsWith("shellyblu");
+ this.api = !blu ? !gen2 ? new Shelly1HttpApi(thingName, this) : new Shelly2ApiRpc(thingName, thingTable, this)
+ : new ShellyBluApi(thingName, thingTable, this);
if (gen2) {
config.eventsCoIoT = false;
}
@Override
public boolean checkRepresentation(String key) {
- return key.equalsIgnoreCase(getUID()) || key.equalsIgnoreCase(config.deviceIp)
+ return key.equalsIgnoreCase(getUID()) || key.equalsIgnoreCase(config.deviceAddress)
|| key.equalsIgnoreCase(config.serviceName) || key.equalsIgnoreCase(getThingName());
}
boolean start = true;
try {
initializeThingConfig();
- logger.debug("{}: Device config: IP address={}, HTTP user/password={}/{}, update interval={}",
- thingName, config.deviceIp, config.userId.isEmpty() ? "<non>" : config.userId,
+ logger.debug("{}: Device config: Device address={}, HTTP user/password={}/{}, update interval={}",
+ thingName, config.deviceAddress, config.userId.isEmpty() ? "<non>" : config.userId,
config.password.isEmpty() ? "<none>" : "***", config.updateInterval);
logger.debug(
"{}: Configured Events: Button: {}, Switch (on/off): {}, Push: {}, Roller: {}, Sensor: {}, CoIoT: {}, Enable AutoCoIoT: {}",
thingName, config.eventsButton, config.eventsSwitch, config.eventsPush, config.eventsRoller,
config.eventsSensorReport, config.eventsCoIoT, bindingConfig.autoCoIoT);
- updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING,
- messages.get("status.unknown.initializing"));
start = initializeThing();
} catch (ShellyApiException e) {
ShellyApiResult res = e.getApiResult();
return httpClient;
}
+ @Override
+ public void startScan() {
+ if (api.isInitialized()) {
+ api.startScan();
+ }
+ }
+
/**
* This routine is called every time the Thing configuration has been changed
*/
resetStats();
logger.debug("{}: Start initializing for thing {}, type {}, IP address {}, Gen2: {}, CoIoT: {}", thingName,
- getThing().getLabel(), thingType, config.deviceIp, gen2, config.eventsCoIoT);
- if (config.deviceIp.isEmpty()) {
- setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "config-status.error.missing-device-ip");
+ getThing().getLabel(), thingType, config.deviceAddress, gen2, config.eventsCoIoT);
+ if (config.deviceAddress.isEmpty()) {
+ setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "config-status.error.missing-device-address");
return false;
}
+ updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING,
+ messages.get("status.unknown.initializing"));
+
profile.initFromThingType(thingType); // do some basic initialization
// Gen 1 only: Setup CoAP listener to we get the CoAP message, which triggers initialization even the thing
checkVersion(tmpPrf, tmpPrf.status);
startCoap(config, tmpPrf);
- if (!gen2) {
+ if (!gen2 && !blu) {
api.setActionURLs(); // register event urls
}
return;
}
- if (!profile.isInitialized() || (isThingOffline() && profile.alwaysOn)) {
+ if (!profile.isInitialized()) {
logger.debug("{}: {}", thingName, messages.get("command.init", command));
initializeThing();
} else {
logger.warn("{}: {} - {}", thingName, messages.get("command.failed", command, channelUID),
e.toString());
}
+
+ String group = getString(channelUID.getGroupId());
+ String channel = getString(channelUID.getIdWithoutGroup());
+ State oldValue = getChannelValue(group, channel);
+ if (oldValue != UnDefType.NULL) {
+ logger.info("{}: Restore channel value to {}", thingName, oldValue);
+ updateChannel(group, channel, oldValue);
+ }
+
} catch (IllegalArgumentException e) {
logger.debug("{}: {}", thingName, messages.get("command.failed", command, channelUID));
}
postEvent(ALARM_TYPE_RESTARTED, true);
}
- // If status update was successful the thing must be online
- setThingOnline();
+ // If status update was successful the thing must be online,
+ // but not while firmware update is in progress
+ if (getThingStatusDetail() != ThingStatusDetail.FIRMWARE_UPDATING) {
+ setThingOnline();
+ }
// map status to channels
updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_NAME, getStringType(profile.settings.name));
logger.debug("{}: Shelly settings info for {}: {}", thingName, profile.hostname, profile.settingsJson);
logger.debug("{}: Device "
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{}), ext. Switch Add-On: {}"
- + ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}"
+ + ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}, BLU Gateway support: {}"
+ ",alwaysOn:{}, updatePeriod:{}sec", thingName, profile.hasRelays, profile.numRelays, profile.isRoller,
profile.numRollers, profile.isDimmer, profile.numMeters, profile.isEMeter,
profile.settings.extSwitch != null ? "installed" : "n/a", profile.isSensor, profile.isDW,
profile.hasBattery, profile.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "",
profile.isSense, profile.isMotion, profile.isLight, profile.isBulb, profile.isDuo, profile.isRGBW2,
- profile.inColor, profile.alwaysOn, profile.updatePeriod);
+ profile.inColor, profile.alwaysOn, profile.updatePeriod, config.enableBluGateway);
if (profile.status.extTemperature != null || profile.status.extHumidity != null
|| profile.status.extVoltage != null || profile.status.extAnalogInput != null) {
logger.debug("{}: Shelly Add-On detected with at least 1 external sensor", thingName);
// Update uptime and WiFi, internal temp
ShellyComponents.updateDeviceStatus(this, status);
- stats.wifiRssi = status.wifiSta.rssi;
+ stats.wifiRssi = getInteger(status.wifiSta.rssi);
if (api.isInitialized()) {
stats.timeoutErrors = api.getTimeoutErrors();
* @return true if event was processed
*/
@Override
- public boolean onEvent(String ipAddress, String deviceName, String deviceIndex, String type,
+ public boolean onEvent(String address, String deviceName, String deviceIndex, String type,
Map<String, String> parameters) {
- if (thingName.equalsIgnoreCase(deviceName) || config.deviceIp.equals(ipAddress)) {
+ if (thingName.equalsIgnoreCase(deviceName) || config.deviceAddress.equals(address)
+ || config.serviceName.equals(deviceName)) {
logger.debug("{}: Event received: class={}, index={}, parameters={}", deviceName, type, deviceIndex,
parameters);
int idx = !deviceIndex.isEmpty() ? Integer.parseInt(deviceIndex) : 1;
String payload = "";
String parmType = getString(parameters.get("type"));
String event = !parmType.isEmpty() ? parmType : type;
- boolean isButton = profile.inButtonMode(idx - 1);
+ boolean isButton = profile.inButtonMode(idx - 1) || type.equals("button");
switch (event) {
case SHELLY_EVENT_SHORTPUSH:
case SHELLY_EVENT_DOUBLE_SHORTPUSH:
thingName = getString(properties.get(PROPERTY_SERVICE_NAME));
if (thingName.isEmpty()) {
thingName = getString(thingType + "-" + getString(getThing().getUID().getId())).toLowerCase();
- logger.debug("{}: Thing name derived from UID {}", thingName, getString(getThing().getUID().toString()));
}
config = getConfigAs(ShellyThingConfiguration.class);
- if (config.deviceIp.isEmpty()) {
- logger.debug("{}: IP address for the device must not be empty", thingName); // may not set in .things file
+ if (config.deviceAddress.isEmpty()) {
+ config.deviceAddress = config.deviceIp;
+ }
+ if (config.deviceAddress.isEmpty()) {
+ logger.debug("{}: IP/MAC address for the device must not be empty", thingName); // may not set in .things
+ // file
return;
}
- try {
- InetAddress addr = InetAddress.getByName(config.deviceIp);
- String saddr = addr.getHostAddress();
- if (!config.deviceIp.equals(saddr)) {
- logger.debug("{}: hostname {} resolved to IP address {}", thingName, config.deviceIp, saddr);
- config.deviceIp = saddr;
+
+ config.deviceAddress = config.deviceAddress.toLowerCase().replaceAll(":", ""); // remove : from MAC address and
+ // convert to lower case
+ if (!config.deviceIp.isEmpty()) {
+ try {
+ InetAddress addr = InetAddress.getByName(config.deviceIp);
+ String saddr = addr.getHostAddress();
+ if (!config.deviceIp.equals(saddr)) {
+ logger.debug("{}: hostname {} resolved to IP address {}", thingName, config.deviceIp, saddr);
+ config.deviceIp = saddr;
+ }
+ } catch (UnknownHostException e) {
+ logger.debug("{}: Unable to resolve hostname {}", thingName, config.deviceIp);
}
- } catch (UnknownHostException e) {
- logger.debug("{}: Unable to resolve hostname {}", thingName, config.deviceIp);
}
config.serviceName = getString(properties.get(PROPERTY_SERVICE_NAME));
properties.put(PROPERTY_MAC_ADDRESS, profile.mac);
properties.put(PROPERTY_FIRMWARE_VERSION, profile.fwVersion + "/" + profile.fwDate);
properties.put(PROPERTY_DEV_MODE, profile.mode);
- properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays));
- properties.put(PROPERTY_NUM_ROLLERS, String.valueOf(profile.numRollers));
- properties.put(PROPERTY_NUM_METER, String.valueOf(profile.numMeters));
+ if (profile.hasRelays) {
+ properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays));
+ properties.put(PROPERTY_NUM_ROLLERS, String.valueOf(profile.numRollers));
+ properties.put(PROPERTY_NUM_METER, String.valueOf(profile.numMeters));
+ }
properties.put(PROPERTY_UPDATE_PERIOD, String.valueOf(profile.updatePeriod));
if (!profile.hwRev.isEmpty()) {
properties.put(PROPERTY_HWREV, profile.hwRev);
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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.api2.ShellyBluApi.buildBluServiceName;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+import static org.openhab.core.thing.Thing.PROPERTY_MODEL_ID;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
+import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
+import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link ShellyBluSensorHandler} implements the thing handler for the BLU devices
+ *
+ * @author Markus Michels - Initial contribution
+ */
+public class ShellyBluSensorHandler extends ShellyBaseHandler {
+ private final static Logger logger = LoggerFactory.getLogger(ShellyBluSensorHandler.class);
+
+ public ShellyBluSensorHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
+ final ShellyBindingConfiguration bindingConfig, final ShellyThingTable thingTable,
+ final Shelly1CoapServer coapServer, final HttpClient httpClient) {
+ super(thing, translationProvider, bindingConfig, thingTable, coapServer, httpClient);
+ }
+
+ @Override
+ public void initialize() {
+ logger.debug("Thing is using {}", this.getClass());
+ super.initialize();
+ }
+
+ public static void addBluThing(String gateway, Shelly2NotifyEvent e, ShellyThingTable thingTable) {
+ String model = substringBefore(getString(e.data.name), "-").toUpperCase();
+ String mac = e.data.addr.replaceAll(":", "");
+ String ttype = "";
+ logger.debug("{}: Create thing for new BLU device {}: {} / {}", gateway, e.data.name, model, mac);
+ ThingTypeUID tuid;
+ switch (model) {
+ case SHELLYDT_BLUBUTTON:
+ ttype = THING_TYPE_SHELLYBLUBUTTON_STR;
+ tuid = THING_TYPE_SHELLYBLUBUTTON;
+ break;
+ case SHELLYDT_BLUDW:
+ ttype = THING_TYPE_SHELLYBLUDW_STR;
+ tuid = THING_TYPE_SHELLYBLUDW;
+ break;
+ default:
+ logger.debug("{}: Unsupported BLU device model {}, MAC={}", gateway, model, mac);
+ return;
+ }
+ String serviceName = buildBluServiceName(model, mac);
+
+ Map<String, Object> properties = new TreeMap<>();
+ addProperty(properties, PROPERTY_MODEL_ID, model);
+ addProperty(properties, PROPERTY_SERVICE_NAME, serviceName);
+ addProperty(properties, PROPERTY_DEV_NAME, e.data.name);
+ addProperty(properties, PROPERTY_DEV_TYPE, ttype);
+ addProperty(properties, PROPERTY_DEV_GEN, "BLU");
+ addProperty(properties, PROPERTY_GW_DEVICE, gateway);
+ addProperty(properties, CONFIG_DEVICEADDRESS, mac);
+
+ if (thingTable != null) {
+ thingTable.discoveredResult(tuid, model, serviceName, mac, properties);
+ }
+ }
+
+ private static void addProperty(Map<String, Object> properties, String key, @Nullable String value) {
+ properties.put(key, value != null ? value : "");
+ }
+}
public static boolean updateDeviceStatus(ShellyThingInterface thingHandler, ShellySettingsStatus status) {
ShellyDeviceProfile profile = thingHandler.getProfile();
+ if (!profile.gateway.isEmpty()) {
+ thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_GATEWAY, getStringType(profile.gateway));
+ }
if (!thingHandler.areChannelsCreated()) {
thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createDeviceChannels(thingHandler.getThing(),
thingHandler.getProfile(), status));
break;
}
if (pos != -1) {
+ thingHandler.logger.debug("{}: Update roller position to {}/{}, state={}", thingHandler.thingName, pos,
+ SHELLY_MAX_ROLLER_POS - pos, state);
updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
// convert Watt/Min to kw/h
if (meter.total != null) {
- double kwh = getDouble(meter.total) / 60 / 1000;
+ double kwh = getDouble(meter.total) / 1000 / 60;
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
toQuantityType(kwh, DIGITS_KWH, Units.KILOWATT_HOUR));
accumulatedTotal += kwh;
.createEMeterChannels(thingHandler.getThing(), profile, emeter, groupName));
}
- // convert Watt/Hour tok w/h
+ // convert Watt/Hour to kw/h
+ double total = getDouble(emeter.total) / 1000 / 60;
+ double totalReturned = getDouble(emeter.totalReturned) / 1000;
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
toQuantityType(getDouble(emeter.power), DIGITS_WATT, Units.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
- toQuantityType(getDouble(emeter.total) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
- updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET, toQuantityType(
- getDouble(emeter.totalReturned) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
+ toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
+ updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET,
+ toQuantityType(totalReturned, DIGITS_KWH, Units.KILOWATT_HOUR));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_REACTWATTS,
toQuantityType(getDouble(emeter.reactive), DIGITS_WATT, Units.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_VOLTAGE,
toQuantityType(computePF(emeter), Units.PERCENT));
accumulatedWatts += getDouble(emeter.power);
- accumulatedTotal += getDouble(emeter.total) / 1000;
- accumulatedReturned += getDouble(emeter.totalReturned) / 1000;
+ accumulatedTotal += total;
+ accumulatedReturned += totalReturned;
if (updated) {
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
}
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
toQuantityType(getDouble(currentWatts), DIGITS_WATT, Units.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
- toQuantityType(getDouble(totalWatts), DIGITS_KWH, Units.KILOWATT_HOUR));
+ toQuantityType(totalWatts, DIGITS_KWH, Units.KILOWATT_HOUR));
if (updated && timestamp > 0) {
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
}
if (!profile.isRoller && !profile.isRGBW2) {
- thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS,
- toQuantityType(accumulatedWatts, DIGITS_WATT, Units.WATT));
+ thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS, toQuantityType(
+ status.totalPower != null ? status.totalPower : accumulatedWatts, DIGITS_WATT, Units.WATT));
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL,
- toQuantityType(accumulatedTotal, DIGITS_KWH, Units.KILOWATT_HOUR));
+ toQuantityType(status.totalCurrent != null ? status.totalCurrent / 1000 : accumulatedTotal,
+ DIGITS_KWH, Units.KILOWATT_HOUR));
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCURETURNED,
- toQuantityType(accumulatedReturned, DIGITS_KWH, Units.KILOWATT_HOUR));
+ toQuantityType(status.totalReturned != null ? status.totalReturned / 1000 : accumulatedReturned,
+ DIGITS_KWH, Units.KILOWATT_HOUR));
}
}
/**
* This method is called when new device information is received.
*/
- boolean onEvent(String ipAddress, String deviceName, String deviceIndex, String eventType,
+ public boolean onEvent(String ipAddress, String deviceName, String deviceIndex, String eventType,
Map<String, String> parameters);
}
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 ShellyThingTable thingTable,
final Shelly1CoapServer coapServer, final HttpClient httpClient) {
}
}
+ @SuppressWarnings("deprecation")
private boolean handleColorPicker(ShellyDeviceProfile profile, Integer lightId, ShellyColorUtils col,
Command command) throws ShellyApiException {
boolean updated = false;
@NonNullByDefault
public interface ShellyManagerInterface {
- Thing getThing();
+ public Thing getThing();
- String getThingName();
+ public String getThingName();
- ShellyDeviceProfile getProfile();
+ public ShellyDeviceProfile getProfile();
- ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
+ public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
- ShellyApiInterface getApi();
+ public ShellyApiInterface getApi();
- ShellyDeviceStats getStats();
+ public ShellyDeviceStats getStats();
- void resetStats();
+ public void resetStats();
- State getChannelValue(String group, String channel);
+ public State getChannelValue(String group, String channel);
- void setThingOnline();
+ public void setThingOnline();
- void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
+ public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
- boolean requestUpdates(int requestCount, boolean refreshSettings);
+ public boolean requestUpdates(int requestCount, boolean refreshSettings);
- void incProtMessages();
+ public void incProtMessages();
- void incProtErrors();
+ public void incProtErrors();
}
@NonNullByDefault
public interface ShellyThingInterface {
- ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
+ public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
- @Nullable
- List<StateOption> getStateOptions(ChannelTypeUID uid);
+ public @Nullable List<StateOption> getStateOptions(ChannelTypeUID uid);
- double getChannelDouble(String group, String channel);
+ public double getChannelDouble(String group, String channel);
- boolean updateChannel(String group, String channel, State value);
+ public boolean updateChannel(String group, String channel, State value);
- boolean updateChannel(String channelId, State value, boolean force);
+ public boolean updateChannel(String channelId, State value, boolean force);
- void setThingOnline();
+ public void setThingOnline();
- void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
+ public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
- boolean isStopping();
+ public boolean isStopping();
- String getThingType();
+ public String getThingType();
- ThingStatus getThingStatus();
+ public ThingStatus getThingStatus();
- ThingStatusDetail getThingStatusDetail();
+ public ThingStatusDetail getThingStatusDetail();
- boolean isThingOnline();
+ public boolean isThingOnline();
- boolean requestUpdates(int requestCount, boolean refreshSettings);
+ public boolean requestUpdates(int requestCount, boolean refreshSettings);
- void triggerUpdateFromCoap();
+ public void triggerUpdateFromCoap();
- void reinitializeThing();
+ public void reinitializeThing();
- void restartWatchdog();
+ public void restartWatchdog();
- void publishState(String channelId, State value);
+ public void publishState(String channelId, State value);
- boolean areChannelsCreated();
+ public boolean areChannelsCreated();
- State getChannelValue(String group, String channel);
+ public State getChannelValue(String group, String channel);
- boolean updateInputs(ShellySettingsStatus status);
+ public boolean updateInputs(ShellySettingsStatus status);
- void updateChannelDefinitions(Map<String, Channel> dynChannels);
+ public void updateChannelDefinitions(Map<String, Channel> dynChannels);
- void postEvent(String event, boolean force);
+ public void postEvent(String event, boolean force);
- void triggerChannel(String group, String channelID, String event);
+ public void triggerChannel(String group, String channelID, String event);
- void triggerButton(String group, int idx, String value);
+ public void triggerButton(String group, int idx, String value);
- ShellyDeviceStats getStats();
+ public ShellyDeviceStats getStats();
- void resetStats();
+ public void resetStats();
- Thing getThing();
+ public Thing getThing();
- String getThingName();
+ public String getThingName();
- ShellyThingConfiguration getThingConfig();
+ public ShellyThingConfiguration getThingConfig();
- HttpClient getHttpClient();
+ public HttpClient getHttpClient();
- String getProperty(String key);
+ public String getProperty(String key);
- void updateProperties(String key, String value);
+ public void updateProperties(String key, String value);
- boolean updateWakeupReason(@Nullable List<Object> valueArray);
+ public boolean updateWakeupReason(@Nullable List<Object> valueArray);
- ShellyApiInterface getApi();
+ public ShellyApiInterface getApi();
- ShellyDeviceProfile getProfile();
+ public ShellyDeviceProfile getProfile();
- long getScheduledUpdates();
+ public long getScheduledUpdates();
- void fillDeviceStatus(ShellySettingsStatus status, boolean updated);
+ public void fillDeviceStatus(ShellySettingsStatus status, boolean updated);
- boolean checkRepresentation(String key);
+ public boolean checkRepresentation(String key);
- void incProtMessages();
+ public void incProtMessages();
- void incProtErrors();
+ public void incProtErrors();
+
+ public void startScan();
}
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.discovery.ShellyBluDiscoveryService;
+import org.openhab.core.thing.ThingTypeUID;
+import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Deactivate;
/***
* The{@link ShellyThingTable} implements a simple table to allow dispatching incoming events to the proper thing
@Component(service = ShellyThingTable.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
public class ShellyThingTable {
private Map<String, ShellyThingInterface> thingTable = new ConcurrentHashMap<>();
+ private @Nullable ShellyBluDiscoveryService bluDiscoveryService;
public void addThing(String key, ShellyThingInterface thing) {
if (thingTable.containsKey(key)) {
thingTable.put(key, thing);
}
- public ShellyThingInterface getThing(String key) {
+ public @Nullable ShellyThingInterface findThing(String key) {
ShellyThingInterface t = thingTable.get(key);
if (t != null) {
return t;
return t;
}
}
- throw new IllegalArgumentException();
+ return null;
+ }
+
+ public ShellyThingInterface getThing(String key) {
+ ShellyThingInterface t = findThing(key);
+ if (t == null) {
+ throw new IllegalArgumentException();
+ }
+ return t;
}
public void removeThing(String key) {
public int size() {
return thingTable.size();
}
+
+ public void startDiscoveryService(BundleContext bundleContext) {
+ if (bluDiscoveryService == null) {
+ bluDiscoveryService = new ShellyBluDiscoveryService(bundleContext, this);
+ bluDiscoveryService.registerDeviceDiscoveryService();
+ }
+ }
+
+ public void startScan() {
+ for (Map.Entry<String, ShellyThingInterface> thing : thingTable.entrySet()) {
+ (thing.getValue()).startScan();
+ }
+ }
+
+ public void stopDiscoveryService() {
+ if (bluDiscoveryService != null) {
+ bluDiscoveryService.unregisterDeviceDiscoveryService();
+ bluDiscoveryService = null;
+ }
+ }
+
+ public void discoveredResult(ThingTypeUID uid, String model, String serviceName, String address,
+ Map<String, Object> properties) {
+ if (bluDiscoveryService != null) {
+ bluDiscoveryService.discoveredResult(uid, model, serviceName, address, properties);
+ }
+ }
+
+ @Deactivate
+ public void deactivate() {
+ stopDiscoveryService();
+ }
}
}
}
+ @SuppressWarnings("null")
private void cleanMap() {
long currentTime = new Date().getTime();
for (K key : timeMap.keySet()) {
public static final String ACTION_GETDEB1 = "getdebug1";
public static final String ACTION_NONE = "-";
- public static final String TEMPLATE_PATH = "sniplets/";
public static final String HEADER_HTML = "header.html";
public static final String OVERVIEW_HTML = "overview.html";
public static final String OVERVIEW_HEADER = "ov_header.html";
}
String html = "";
- String file = TEMPLATE_PATH + template;
+ String file = BUNDLE_RESOURCE_SNIPLETS + "/" + template;
logger.debug("Read HTML from {}", file);
ClassLoader cl = ShellyManagerInterface.class.getClassLoader();
if (cl != null) {
logger.debug("{} stopped", className);
}
+ @SuppressWarnings("resource")
@Override
protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
throws ServletException, IOException, IllegalArgumentException {
CHANNEL_DEFINITIONS
// Device
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_NAME, "deviceName", ITEMT_STRING))
+ .add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_GATEWAY, "gatewayDevice", ITEMT_STRING))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ITEMP, "deviceTemp", ITEMT_TEMP))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_WAKEUP, "sensorWakeup", ITEMT_STRING))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ACCUWATTS, "meterAccuWatts", ITEMT_POWER))
Map<String, Channel> add = new LinkedHashMap<>();
addChannel(thing, add, profile.settings.name != null, CHGR_DEVST, CHANNEL_DEVST_NAME);
+ addChannel(thing, add, !profile.gateway.isEmpty() || profile.isBlu, CHGR_DEVST, CHANNEL_DEVST_GATEWAY);
- if (!profile.isSensor && !profile.isIX && status.temperature != null
- && status.temperature != SHELLY_API_INVTEMP) {
+ if (!profile.isSensor && !profile.isIX
+ && ((status.temperature != null && getDouble(status.temperature) != SHELLY_API_INVTEMP)
+ || (status.tmp != null && getDouble(status.tmp.tC) != SHELLY_API_INVTEMP))) {
// Only some devices report the internal device temp
addChannel(thing, add, status.tmp != null || status.temperature != null, CHGR_DEVST, CHANNEL_DEVST_ITEMP);
}
addChannel(thing, add, profile.status.extTemperature.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP1);
addChannel(thing, add, profile.status.extTemperature.sensor2 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP2);
addChannel(thing, add, profile.status.extTemperature.sensor3 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP3);
+ addChannel(thing, add, profile.status.extTemperature.sensor4 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP4);
+ addChannel(thing, add, profile.status.extTemperature.sensor5 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP5);
}
addChannel(thing, add, profile.status.extHumidity != null, CHGR_SENSOR, CHANNEL_ESENSOR_HUMIDITY);
addChannel(thing, add, profile.status.extVoltage != null, CHGR_SENSOR, CHANNEL_ESENSOR_VOLTAGE);
for (int i = 0; i < profile.numInputs; i++) {
String group = profile.getInputGroup(i);
String suffix = profile.getInputSuffix(i); // multi ? String.valueOf(i + 1) : "";
- addChannel(thing, add, true, group, CHANNEL_INPUT + suffix);
+ addChannel(thing, add, !profile.isButton, group, CHANNEL_INPUT + suffix);
addChannel(thing, add, true, group,
(!profile.isRoller ? CHANNEL_BUTTON_TRIGGER + suffix : CHANNEL_EVENT_TRIGGER));
if (profile.inButtonMode(i)) {
<type>binding</type>
<name>@text/addon.shelly.name</name>
<description>@text/addon.shelly.description</description>
- <connection>local</connection>
<config-description>
<parameter name="defaultUserId" type="text">
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
+ <parameter name="enableBluGateway" type="boolean" required="false">
+ <label>@text/thing-type.config.shelly.enableBluGateway.label</label>
+ <description>@text/thing-type.config.shelly.enableBluGateway.description</description>
+ <default>false</default>
+ </parameter>
</config-description>
<config-description uri="thing-type:shelly:roller-gen2">
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
+ <parameter name="enableBluGateway" type="boolean" required="false">
+ <label>@text/thing-type.config.shelly.enableBluGateway.label</label>
+ <description>@text/thing-type.config.shelly.enableBluGateway.description</description>
+ <default>false</default>
+ </parameter>
</config-description>
<config-description uri="thing-type:shelly:battery-gen2">
--- /dev/null
+<?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:blubattery">
+ <parameter name="deviceAddress" type="text" required="true">
+ <label>@text/thing-type.config.shelly.deviceAddress.label</label>
+ <description>@text/thing-type.config.shelly.@text/thing-type.config.shelly.deviceAddress.label.description</description>
+ </parameter>
+ <parameter name="lowBattery" type="integer" required="false">
+ <label>@text/thing-type.config.shelly.battery.lowBattery.label</label>
+ <description>@text/thing-type.config.shelly.battery.lowBattery.description</description>
+ <default>20</default>
+ <unitLabel>%</unitLabel>
+ </parameter>
+ </config-description>
+
+</config-description:config-descriptions>
addon.shelly.config.autoCoIoT.description = If enabled CoIoT will be automatically used when the devices runs a firmware version 1.6 or newer; false: Use thing configuration to enabled/disable CoIoT events.
# Config status messages
-message.config-status.error.missing-device-ip = IP address of the Shelly device is missing.
+message.config-status.error.missing-device-address = IP/MAC Address of the Shelly device is missing.
message.config-status.error.missing-userid = No user ID in the Thing configuration
# Thing status descriptions
message.coap.init.failed = Unable to start CoIoT: {0}
message.discovery.disabled = Device is marked as non-discoverable, will be skipped
message.discovery.protected = Device {0} reported 'Access defined' (missing userid/password or incorrect).
-message.discovery.failed = Device discovery of device with IP address {0} failed: {1}
+message.discovery.failed = Device discovery of device with address {0} failed: {1}
message.roller.calibrating = Device is not calibrated, use Shelly App to perform initial roller calibration.
message.roller.favmissing = Roller position favorites are not supported by installed firmware or not configured in the Shelly App
thing-type.shelly.shellyix3.description = Shelly ix3 (Activation Device with 3 inputs)
thing-type.shelly.shellypludht.description = Shelly Plus HT - Temperature and Humidity Sensor
-# Plus Devices
-thing-type.shelly.shellyplus1.description = Shelly Plus 1 (Single Relay Switch)
-thing-type.shelly.shellyplus1pm.description = Shelly Plus 1PM - Single Relay Switch with Power Meter
-thing-type.shelly.shellyplus2-relay.description = Shelly Plus 2PM - Dual Relay Switch with Power Meter
-thing-type.shelly.shellyplus2pm-roller.description = Shelly Plus 2PM - Roller Control with Power Meter
-thing-type.shelly.shellyplusplug.description = Shelly Plus Plug S/IT/UK/US . Outlet with Power Meter
-thing-type.shelly.shellyplusht.description = Shelly Plus HT - Humidity and Temperature sensor with display
-thing-type.shelly.shellyplusi4.description = Shelly Plus i4 - 4xInput Device
-thing-type.shelly.shellyplusi4dc.description = Shelly Plus i4DC - 4xDC Input Device
-
# Pro Devices
thing-type.shelly.shellypro1.description = Shelly Pro 1 - Single Relay Switch
thing-type.shelly.shellypro1pm.description = Shelly Pro 1PM - Single Relay Switch with Power Meter
thing-type.shelly.shellypro3em.description = Shelly Pro 3EM - 3xPower Meter
thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter
-
-# Plus/Pro devices
+# Plus devices
thing-type.shelly.shellyplus1.description = Shelly Plus 1 (Single Relay Switch)
thing-type.shelly.shellyplus1pm.description = Shelly Plus 1PM - Single Relay Switch with Power Meter
thing-type.shelly.shellyplus2-relay.description = Shelly Plus 2PM - Dual Relay Switch with Power Meter
thing-type.shelly.shellyplus2pm-roller.description = Shelly Plus 2PM - Roller Control with Power Meter
+ thing-type.shelly.shellyplusplug.description = Shelly Plus Plug S/IT/UK/US . Outlet with Power Meter
thing-type.shelly.shellyplusht.description = Shelly Plus HT - Humidity and Temperature sensor with display
thing-type.shelly.shellyplussmoke.description = Shelly Plus Smoke - Smoke Detector with Alarm
thing-type.shelly.shellyplusi4.description = Shelly Plus i4 - 4xInput Device
thing-type.shelly.shellyplusi4dc.description = Shelly Plus i4DC - 4xDC Input Device
+
+ # Pro devices
thing-type.shelly.shellypro1.description = Shelly Pro 1 - Single Relay Switch
thing-type.shelly.shellypro1pm.description = Shelly Pro 1PM - Single Relay Switch with Power Meter
thing-type.shelly.shellypro2-relay.description = Shelly Pro 2 - Dual Relay Switch
thing-type.shelly.shellypro3.description = Shelly Pro 3 - 3xRelay Switch
thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter
+ # BLU devices
+ thing-type.shelly.shellypblubutton.description = Shelly BLU Button
+ thing-type.shelly.shellybludw.description = Shelly BLU Door/Window Sensor
+
# thing config - shellydevice
thing-type.config.shelly.deviceIp.label = IP Address
thing-type.config.shelly.deviceIp.description = IP Address of the Shelly device
+thing-type.config.shelly.deviceAddress.label = MAC Address
+thing-type.config.shelly.deviceAddress.description = MAC Address of the Shelly device
thing-type.config.shelly.userId.label = User ID
thing-type.config.shelly.userId.description = User ID for API access
thing-type.config.shelly.password.label = Password
thing-type.config.shelly.password.description = Password for API access
thing-type.config.shelly.updateInterval.label = Status Interval
thing-type.config.shelly.updateInterval.description = Interval for the device status update
+thing-type.config.shelly.enableBluGateway.label = Enable BLU Gateway Support
+thing-type.config.shelly.enableBluGateway.description = Enables BLU Gateway support incl- auto-upload of the required script
thing-type.config.shelly.eventsButton.label = Button Events
thing-type.config.shelly.eventsButton.description = Activates the Button Action URLS
thing-type.config.shelly.eventsPush.label = Push Events
channel-type.shelly.meterAccuWatts.description = Accumulated Power Consumption in Watts of the device (including all meters)
channel-type.shelly.meterAccuTotal.label = Accumulated Total Power
channel-type.shelly.meterAccuTotal.description = Accumulated Total Power in kW/h of the device (including all meters)
-channel-type.shelly.meterAccuReturned.label = Accumulated Returned Power
-channel-type.shelly.meterAccuReturned.description = Accumulated Returned Power in kW/h of the device (including all meters)
+channel-type.shelly.meterAccuReturned.label = Accumulated Apparent Power
+channel-type.shelly.meterAccuReturned.description = Accumulated Apparent Power in kW/h of the device (including all meters)
channel-type.shelly.meterReactive.label = Reactive Energy
channel-type.shelly.meterReactive.description = Instantaneous reactive power in Watts (W)
channel-type.shelly.lastPower1.label = Last Power
channel-type.shelly.senseKey.description = Send a defined key code
channel-type.shelly.deviceName.label = Device Name
channel-type.shelly.deviceName.description = Symbolic Device Name as configured in the Shelly App
+channel-type.shelly.gatewayDevice.label = Gateway Device
+channel-type.shelly.gatewayDevice.description = Last Shelly Device, which forwarded the event
channel-type.shelly.uptime.label = Uptime
channel-type.shelly.uptime.description = Number of seconds since the device was powered up
channel-type.shelly.heartBeat.label = Last Heartbeat
<state readOnly="true">
</state>
</channel-type>
+ <channel-type id="gatewayDevice" advanced="true">
+ <item-type>String</item-type>
+ <label>@text/channel-type.shelly.gatewayDevice.label</label>
+ <description>@text/channel-type.shelly.gatewayDevice.description</description>
+ <state readOnly="true">
+ </state>
+ </channel-type>
<channel-type id="calibrated" advanced="true">
<item-type>Switch</item-type>
<label>@text/channel-type.shelly.calibrated.label</label>
--- /dev/null
+<?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="shellyblubutton">
+ <label>Shelly BLU Button</label>
+ <description>@text/thing-type.shelly.shellyblubutton.description</description>
+ <category>WallSwitch</category>
+ <channel-groups>
+ <channel-group id="status" typeId="buttonState"/>
+ <channel-group id="battery" typeId="batteryStatus"/>
+ <channel-group id="device" typeId="deviceStatus"/>
+ </channel-groups>
+
+ <representation-property>serviceName</representation-property>
+ <config-description-ref uri="thing-type:shelly:blubattery"/>
+ </thing-type>
+
+ <thing-type id="shellybludw">
+ <label>Shelly BLU Door/Window</label>
+ <description>@text/thing-type.shelly.shellybludw.description</description>
+ <category>Sensor</category>
+ <channel-groups>
+ <channel-group id="sensors" typeId="sensorData"/>
+ <channel-group id="battery" typeId="batteryStatus"/>
+ <channel-group id="device" typeId="deviceStatus"/>
+ </channel-groups>
+
+ <representation-property>serviceName</representation-property>
+ <config-description-ref uri="thing-type:shelly:blubattery"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+/*
+ * This script uses the BLE scan functionality in scripting to pass scan reults to openHAB
+ * Supported BLU Devices: SBBT , SBDW
+ */
+
+let ALLTERCO_DEVICE_NAME_PREFIX = ["SBBT", "SBDW"];
+let ALLTERCO_MFD_ID_STR = "0ba9";
+let BTHOME_SVC_ID_STR = "fcd2";
+
+let ALLTERCO_MFD_ID = JSON.parse("0x" + ALLTERCO_MFD_ID_STR);
+let BTHOME_SVC_ID = JSON.parse("0x" + BTHOME_SVC_ID_STR);
+let SCAN_DURATION = BLE.Scanner.INFINITE_SCAN;
+
+let SHELLY_BLU_CACHE = {};
+let LAST_PID = {};
+
+let uint8 = 0;
+let int8 = 1;
+let uint16 = 2;
+let int16 = 3;
+let uint24 = 4;
+let int24 = 5;
+
+let BTH = [];
+BTH[0x00] = { n: "pid", t: uint8 };
+BTH[0x01] = { n: "Battery", t: uint8, u: "%" };
+BTH[0x05] = { n: "Illuminance", t: uint24, f: 0.01 };
+BTH[0x1a] = { n: "Door", t: uint8 };
+BTH[0x20] = { n: "Moisture", t: uint8 };
+BTH[0x2d] = { n: "Window", t: uint8 };
+BTH[0x3a] = { n: "Button", t: uint8 };
+BTH[0x3f] = { n: "Rotation", t: int16, f: 0.1 };
+
+function getByteSize(type) {
+ if (type === uint8 || type === int8) return 1;
+ if (type === uint16 || type === int16) return 2;
+ if (type === uint24 || type === int24) return 3;
+ //impossible as advertisements are much smaller;
+ return 255;
+}
+
+let BTHomeDecoder = {
+ utoi: function (num, bitsz) {
+ let mask = 1 << (bitsz - 1);
+ return num & mask ? num - (1 << bitsz) : num;
+ },
+ getUInt8: function (buffer) {
+ return buffer.at(0);
+ },
+ getInt8: function (buffer) {
+ return this.utoi(this.getUInt8(buffer), 8);
+ },
+ getUInt16LE: function (buffer) {
+ return 0xffff & ((buffer.at(1) << 8) | buffer.at(0));
+ },
+ getInt16LE: function (buffer) {
+ return this.utoi(this.getUInt16LE(buffer), 16);
+ },
+ getUInt24LE: function (buffer) {
+ return (
+ 0x00ffffff & ((buffer.at(2) << 16) | (buffer.at(1) << 8) | buffer.at(0))
+ );
+ },
+ getInt24LE: function (buffer) {
+ return this.utoi(this.getUInt24LE(buffer), 24);
+ },
+ getBufValue: function (type, buffer) {
+ if (buffer.length < getByteSize(type)) return null;
+ let res = null;
+ if (type === uint8) res = this.getUInt8(buffer);
+ if (type === int8) res = this.getInt8(buffer);
+ if (type === uint16) res = this.getUInt16LE(buffer);
+ if (type === int16) res = this.getInt16LE(buffer);
+ if (type === uint24) res = this.getUInt24LE(buffer);
+ if (type === int24) res = this.getInt24LE(buffer);
+ return res;
+ },
+ unpack: function (buffer) {
+ // beacons might not provide BTH service data
+ if (typeof buffer !== "string" || buffer.length === 0) return null;
+ let result = {};
+ let _dib = buffer.at(0);
+ result["encryption"] = _dib & 0x1 ? true : false;
+ result["BTHome_version"] = _dib >> 5;
+ if (result["BTHome_version"] !== 2) return null;
+ //Can not handle encrypted data
+ if (result["encryption"]) return result;
+ buffer = buffer.slice(1);
+
+ let _bth;
+ let _value;
+ while (buffer.length > 0) {
+ _bth = BTH[buffer.at(0)];
+ if (_bth === "undefined") {
+ console.log("BTH: unknown type");
+ break;
+ }
+ buffer = buffer.slice(1);
+ _value = this.getBufValue(_bth.t, buffer);
+ if (_value === null) break;
+ if (typeof _bth.f !== "undefined") _value = _value * _bth.f;
+ result[_bth.n] = _value;
+ buffer = buffer.slice(getByteSize(_bth.t));
+ }
+ return result;
+ },
+};
+
+let ShellyBLUParser = {
+ getData: function (res) {
+ let result = BTHomeDecoder.unpack(res.service_data[BTHOME_SVC_ID_STR]);
+ result.addr = res.addr;
+ result.rssi = res.rssi;
+ return result;
+ },
+};
+
+function scanCB(ev, res) {
+ if (ev !== BLE.Scanner.SCAN_RESULT) return;
+ // skip if there is no service_data member
+ if (typeof res.service_data === 'undefined' || typeof res.service_data[BTHOME_SVC_ID_STR] === 'undefined') return;
+ // skip if we have already found this device
+ if (typeof SHELLY_BLU_CACHE[res.addr] === 'undefined') {
+ if (typeof res.local_name === "undefined") console.log("res.local_name undefined")
+ if (typeof res.local_name !== 'string') return;
+ let shellyBluNameIdx = 0;
+ for (shellyBluNameIdx in ALLTERCO_DEVICE_NAME_PREFIX) {
+ if (res.local_name.indexOf(ALLTERCO_DEVICE_NAME_PREFIX[shellyBluNameIdx]) === 0) {
+ console.log('New device found: address=', res.addr, ', name=', res.local_name);
+ Shelly.emitEvent("oh-blu.scan_result", {"addr":res.addr, "name":res.local_name, "rssi":res.rssi, "tx_power":res.tx_power_level});
+ SHELLY_BLU_CACHE[res.addr] = res.local_name;
+ }
+ }
+ }
+
+ let BTHparsed = ShellyBLUParser.getData(res); // skip if parsing failed
+ if (BTHparsed === null) {
+ console.log("Failed to parse BTH data");
+ return;
+ }
+
+ // skip, we are deduping results
+ if (typeof LAST_PID[res.addr] === 'undefined' ||
+ BTHparsed.pid !== LAST_PID[res.addr]) {
+ Shelly.emitEvent("oh-blu.data", BTHparsed);
+ LAST_PID[res.addr] = BTHparsed.pid;
+ }
+}
+
+// retry several times to start the scanner if script was started before
+// BLE infrastructure was up in the Shelly
+function startBLEScan() {
+ let bleScanSuccess = BLE.Scanner.Start({ duration_ms: SCAN_DURATION, active: true }, scanCB);
+ if( bleScanSuccess === false ) {
+ Timer.set(1000, false, startBLEScan);
+ } else {
+ console.log('Success: OH-BLU Event Gateway running');
+ }
+}
+
+let BLEConfig = Shelly.getComponentConfig('ble');
+if(BLEConfig.enable === false) {
+ console.log('Error: BLE not enabled');
+} else {
+ Timer.set(1000, false, startBLEScan);
+}
+
\ No newline at end of file