[shelly] Add support for Shelly BLU series of devices (#15031)

* support for Pro 3EM (WIP)
* Support for Plug-S and Smoke added
* new channel resetTotals for emeters, new channel sensor#mute for Smoke
* Validate Temp reported by CoAP before updating channel, ignore 999
* Add support for Shelly BLU Button and Door/Window

Signed-off-by: Markus Michels <markus7017@gmail.com>
This commit is contained in:
Markus Michels 2023-06-26 16:37:50 +02:00 committed by GitHub
parent 46039efd0a
commit 631148320f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1459 additions and 196 deletions

View File

@ -77,7 +77,7 @@ The binding provides the same feature set across all devices as good as possible
### 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 |
@ -104,6 +104,14 @@ The binding provides the same feature set across all devices as good as possible
| 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:
@ -160,6 +168,29 @@ This allows routing the CoIoT/CoAP messages across multiple IP subnets without s
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.
@ -251,6 +282,7 @@ You could also create a rule to catch those status changes or device alarms (see
| 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
@ -380,7 +412,7 @@ A new alarm will be triggered on a new condition or every 5 minutes if the condi
| 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
@ -1341,6 +1373,38 @@ Channels lastEvent and eventCount are only available if input type is set to mom
| | 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
@ -1582,4 +1646,3 @@ sitemap demo label="Home"
Number item=Shelly_Power
}
}
```

View File

@ -85,11 +85,14 @@ public class ShellyBindingConstants {
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";
@ -99,6 +102,7 @@ public class ShellyBindingConstants {
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";
@ -230,6 +234,7 @@ public class ShellyBindingConstants {
// 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";
@ -311,4 +316,7 @@ public class ShellyBindingConstants {
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
}

View File

@ -24,6 +24,7 @@ import org.eclipse.jetty.client.HttpClient;
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;
@ -103,6 +104,11 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
this.coapServer = new Shelly1CoapServer();
}
@Activate
void activate() {
thingTable.startDiscoveryService(bundleContext);
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
@ -123,11 +129,15 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
|| 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);
}
@ -143,20 +153,13 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
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);
}
@ -185,4 +188,12 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
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;
}
}

View File

@ -83,6 +83,8 @@ public class ShellyApiException extends Exception {
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()) {

View File

@ -46,7 +46,7 @@ public interface ShellyApiInterface {
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;
@ -54,9 +54,9 @@ public interface ShellyApiInterface {
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;
@ -80,7 +80,6 @@ public interface ShellyApiInterface {
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;
@ -137,5 +136,9 @@ public interface ShellyApiInterface {
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();
}

View File

@ -67,6 +67,8 @@ public class ShellyDeviceProfile {
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 = "";
@ -125,6 +127,10 @@ public class ShellyDeviceProfile {
// 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);
@ -185,6 +191,8 @@ public class ShellyDeviceProfile {
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)
@ -201,12 +209,14 @@ public class ShellyDeviceProfile {
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);
@ -241,7 +251,8 @@ public class ShellyDeviceProfile {
} 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;

View File

@ -146,14 +146,15 @@ public class ShellyHttpClient {
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);
@ -204,7 +205,7 @@ public class ShellyHttpClient {
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()));
}
}
@ -253,4 +254,8 @@ public class ShellyHttpClient {
public int getTimeoutsRecovered() {
return timeoutsRecovered;
}
public void postEvent(String device, String index, String event, Map<String, String> parms)
throws ShellyApiException {
}
}

View File

@ -739,7 +739,12 @@ public class Shelly1ApiJsonDTO {
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")

View File

@ -30,14 +30,14 @@ import org.openhab.core.types.State;
*/
@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();
}

View File

@ -210,8 +210,8 @@ public class Shelly1CoIoTProtocol {
"{}: 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]);

View File

@ -176,7 +176,7 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
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) {

View File

@ -23,5 +23,5 @@ import org.eclipse.jdt.annotation.Nullable;
*/
@NonNullByDefault
public interface Shelly1CoapListener {
void processResponse(@Nullable Response response);
public void processResponse(@Nullable Response response);
}

View File

@ -177,7 +177,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
@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
@ -245,7 +245,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
} 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);
}
@ -294,7 +294,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
}
@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));
}
@ -752,4 +752,8 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
@Override
public void close() {
}
@Override
public void startScan() {
}
}

View File

@ -289,6 +289,16 @@ public class Shelly2ApiClient extends ShellyHttpClient {
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;
@ -683,7 +693,7 @@ public class Shelly2ApiClient extends ShellyHttpClient {
throw new ShellyApiException("Thing/profile not initialized!");
}
ShellyDeviceProfile getProfile() throws ShellyApiException {
protected ShellyDeviceProfile getProfile() throws ShellyApiException {
if (thing != null) {
return thing.getProfile();
}

View File

@ -58,6 +58,15 @@ public class Shelly2ApiJsonDTO {
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
@ -118,6 +127,12 @@ public class Shelly2ApiJsonDTO {
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";
@ -549,6 +564,13 @@ public class Shelly2ApiJsonDTO {
@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 {
@ -754,6 +776,9 @@ public class Shelly2ApiJsonDTO {
// Cloud.SetConfig
public Shelly2ConfigParms config;
// Script
public String name;
public Shelly2RpcRequestParams withConfig() {
config = new Shelly2ConfigParms();
return this;
@ -779,6 +804,11 @@ public class Shelly2ApiJsonDTO {
params.pos = pos;
return this;
}
public Shelly2RpcRequest withName(String name) {
params.name = name;
return this;
}
}
public static class Shelly2WsConfigResponse {
@ -793,6 +823,30 @@ public class Shelly2ApiJsonDTO {
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"}
@ -804,8 +858,10 @@ public class Shelly2ApiJsonDTO {
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;
@ -852,11 +908,49 @@ public class Shelly2ApiJsonDTO {
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")
@ -869,7 +963,8 @@ public class Shelly2ApiJsonDTO {
}
public static class Shelly2RpcNotifyEvent {
public String src;
public Double ts;
Shelly2NotifyEventData params;
public Shelly2NotifyEventData params;
}
}

View File

@ -17,9 +17,15 @@ 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.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;
@ -65,6 +71,10 @@ import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequ
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;
@ -83,7 +93,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
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();
@ -139,6 +149,17 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
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();
@ -269,6 +290,19 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
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;
@ -306,6 +340,117 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
}
}
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) {
@ -755,7 +900,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
* 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");
}
@ -866,6 +1011,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
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);
@ -905,8 +1051,18 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
throw e;
}
}
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 {

View File

@ -34,9 +34,11 @@ import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
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;
@ -250,8 +252,34 @@ public class Shelly2RpcSocket {
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);
}
@ -259,7 +287,9 @@ public class Shelly2RpcSocket {
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);
}
}

View File

@ -24,15 +24,15 @@ import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNoti
@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);
}

View File

@ -0,0 +1,323 @@
/**
* 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);
}
}
}

View File

@ -22,6 +22,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
@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
@ -42,4 +43,6 @@ public class ShellyThingConfiguration {
public String localIp = ""; // local ip addresses used to create callback url
public String localPort = "8080";
public String serviceName = "";
public boolean enableBluGateway = false;
}

View File

@ -0,0 +1,93 @@
/**
* 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();
}
}

View File

@ -101,6 +101,10 @@ public class ShellyThingCreator {
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";
@ -164,6 +168,12 @@ public class ShellyThingCreator {
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";
@ -258,6 +268,11 @@ public class ShellyThingCreator {
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
@ -324,6 +339,10 @@ public class ShellyThingCreator {
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);

View File

@ -46,6 +46,7 @@ import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO;
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;
@ -104,6 +105,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
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
@ -151,7 +153,9 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
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;
}
@ -162,7 +166,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
@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());
}
@ -176,15 +180,13 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
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();
@ -225,6 +227,13 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
return httpClient;
}
@Override
public void startScan() {
if (api.isInitialized()) {
api.startScan();
}
}
/**
* This routine is called every time the Thing configuration has been changed
*/
@ -257,12 +266,15 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
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
@ -326,7 +338,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
checkVersion(tmpPrf, tmpPrf.status);
startCoap(config, tmpPrf);
if (!gen2) {
if (!gen2 && !blu) {
api.setActionURLs(); // register event urls
}
@ -358,7 +370,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
return;
}
if (!profile.isInitialized() || (isThingOffline() && profile.alwaysOn)) {
if (!profile.isInitialized()) {
logger.debug("{}: {}", thingName, messages.get("command.init", command));
initializeThing();
} else {
@ -468,6 +480,15 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
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));
}
@ -518,8 +539,11 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
postEvent(ALARM_TYPE_RESTARTED, true);
}
// If status update was successful the thing must be online
// 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));
@ -587,13 +611,13 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
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);
@ -715,7 +739,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
// 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();
@ -829,9 +853,10 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
* @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;
@ -851,7 +876,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
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:
@ -959,14 +984,21 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
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;
}
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();
@ -977,6 +1009,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
} catch (UnknownHostException e) {
logger.debug("{}: Unable to resolve hostname {}", thingName, config.deviceIp);
}
}
config.serviceName = getString(properties.get(PROPERTY_SERVICE_NAME));
config.localIp = bindingConfig.localIP;
@ -1406,9 +1439,11 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
properties.put(PROPERTY_MAC_ADDRESS, profile.mac);
properties.put(PROPERTY_FIRMWARE_VERSION, profile.fwVersion + "/" + profile.fwDate);
properties.put(PROPERTY_DEV_MODE, profile.mode);
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);

View File

@ -0,0 +1,93 @@
/**
* 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 : "");
}
}

View File

@ -54,6 +54,9 @@ public class ShellyComponents {
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));
@ -177,6 +180,8 @@ public class ShellyComponents {
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,
@ -238,7 +243,7 @@ public class ShellyComponents {
// 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;
@ -264,12 +269,14 @@ public class ShellyComponents {
}
// 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,
@ -280,8 +287,8 @@ public class ShellyComponents {
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());
}
@ -327,7 +334,7 @@ public class ShellyComponents {
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,
@ -336,12 +343,14 @@ public class ShellyComponents {
}
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));
}
}

View File

@ -28,6 +28,6 @@ public interface ShellyDeviceListener {
/**
* 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);
}

View File

@ -56,15 +56,6 @@ public class ShellyLightHandler extends ShellyBaseHandler {
private final Logger logger = LoggerFactory.getLogger(ShellyLightHandler.class);
private final Map<Integer, ShellyColorUtils> channelColors;
/**
* Constructor
*
* @param thing The thing passed by the HandlerFactory
* @param bindingConfig configuration of the binding
* @param coapServer coap server instance
* @param localIP local IP of the openHAB host
* @param httpPort port of the openHAB HTTP API
*/
public ShellyLightHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, final ShellyThingTable thingTable,
final Shelly1CoapServer coapServer, final HttpClient httpClient) {
@ -230,6 +221,7 @@ public class ShellyLightHandler extends ShellyBaseHandler {
}
}
@SuppressWarnings("deprecation")
private boolean handleColorPicker(ShellyDeviceProfile profile, Integer lightId, ShellyColorUtils col,
Command command) throws ShellyApiException {
boolean updated = false;

View File

@ -28,29 +28,29 @@ import org.openhab.core.types.State;
@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();
}

View File

@ -39,84 +39,85 @@ import org.openhab.core.types.StateOption;
@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();
}

View File

@ -16,8 +16,13 @@ import java.util.Map;
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
@ -29,6 +34,7 @@ import org.osgi.service.component.annotations.ConfigurationPolicy;
@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)) {
@ -37,7 +43,7 @@ public class ShellyThingTable {
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;
@ -48,8 +54,16 @@ public class ShellyThingTable {
return t;
}
}
return null;
}
public ShellyThingInterface getThing(String key) {
ShellyThingInterface t = findThing(key);
if (t == null) {
throw new IllegalArgumentException();
}
return t;
}
public void removeThing(String key) {
if (thingTable.containsKey(key)) {
@ -64,4 +78,36 @@ public class ShellyThingTable {
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();
}
}

View File

@ -87,6 +87,7 @@ public class ShellyManagerCache<K, V> extends ConcurrentHashMap<K, V> {
}
}
@SuppressWarnings("null")
private void cleanMap() {
long currentTime = new Date().getTime();
for (K key : timeMap.keySet()) {

View File

@ -61,7 +61,6 @@ public class ShellyManagerConstants {
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";

View File

@ -164,7 +164,7 @@ public class ShellyManagerPage {
}
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) {

View File

@ -84,6 +84,7 @@ public class ShellyManagerServlet extends HttpServlet {
logger.debug("{} stopped", className);
}
@SuppressWarnings("resource")
@Override
protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
throws ServletException, IOException, IllegalArgumentException {

View File

@ -125,6 +125,7 @@ public class ShellyChannelDefinitions {
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))
@ -297,9 +298,11 @@ public class ShellyChannelDefinitions {
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);
}
@ -355,6 +358,8 @@ public class ShellyChannelDefinitions {
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);
@ -411,7 +416,7 @@ public class ShellyChannelDefinitions {
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)) {

View File

@ -6,7 +6,6 @@
<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">

View File

@ -22,6 +22,11 @@
<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">
@ -52,6 +57,11 @@
<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">

View File

@ -0,0 +1,20 @@
<?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>

View File

@ -11,7 +11,7 @@ addon.shelly.config.autoCoIoT.label = Auto-CoIoT
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
@ -43,7 +43,7 @@ message.event.filtered = Event filtered: {0}
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
@ -88,16 +88,6 @@ thing-type.shelly.shellytrv.description = Shelly TRV (Radiator value, battery po
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
@ -108,16 +98,18 @@ thing-type.shelly.shellypro3.description = Shelly Pro 3 - 3xRelay Switch
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
@ -127,15 +119,23 @@ thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch wit
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
@ -287,8 +287,8 @@ channel-type.shelly.meterAccuWatts.label = Accumulated Power Consumption
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
@ -450,6 +450,8 @@ channel-type.shelly.senseKey.label = IR Key to Send
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

View File

@ -59,6 +59,13 @@
<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>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="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>

View File

@ -0,0 +1,167 @@
/*
* 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);
}