[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

@ -76,20 +76,20 @@ The binding provides the same feature set across all devices as good as possible
### Generation 2 Plus series ### Generation 2 Plus series
| thing-type | Model | Vendor ID | | thing-type | Model | Vendor ID |
| -------------------- | -------------------------------------------------------- | --------------------------------------------- | | -------------------- | -------------------------------------------------------- | ---------------------------- |
| shellyplus1 | Shelly Plus 1 with 1x relay | SNSW-001X16EU | | shellyplus1 | Shelly Plus 1 with 1x relay | SNSW-001X16EU |
| shellyplus1pm | Shelly Plus 1PM with 1x relay + power meter | SNSW-001P16EU | | shellyplus1pm | Shelly Plus 1PM with 1x relay + power meter | SNSW-001P16EU |
| shellyplus2pm-relay | Shelly Plus 2PM with 2x relay + power meter, relay mode | SNSW-002P16EU, SNSW-102P16EU | | shellyplus2pm-relay | Shelly Plus 2PM with 2x relay + power meter, relay mode | SNSW-002P16EU, SNSW-102P16EU |
| shellyplus2pm-roller | Shelly Plus 2PM with 2x relay + power meter, roller mode | SNSW-002P16EU, SNSW-102P16EU | | shellyplus2pm-roller | Shelly Plus 2PM with 2x relay + power meter, roller mode | SNSW-002P16EU, SNSW-102P16EU |
| shellyplusplug | Shelly Plug-S | SNPL-00112EU | | shellyplusplug | Shelly Plug-S | SNPL-00112EU |
| shellyplusplug | Shelly Plug-IT | SNPL-00110IT | | shellyplusplug | Shelly Plug-IT | SNPL-00110IT |
| shellyplusplug | Shelly Plug-UK | SNPL-00112UK | | shellyplusplug | Shelly Plug-UK | SNPL-00112UK |
| shellyplusplug | Shelly Plug-US | SNPL-00116US | | shellyplusplug | Shelly Plug-US | SNPL-00116US |
| shellyplusi4 | Shelly Plus i4 with 4x AC input | SNSN-0024X | | shellyplusi4 | Shelly Plus i4 with 4x AC input | SNSN-0024X |
| shellyplusi4dc | Shelly Plus i4 with 4x DC input | SNSN-0D24X | | shellyplusi4dc | Shelly Plus i4 with 4x DC input | SNSN-0D24X |
| shellyplusht | Shelly Plus HT with temperature + humidity sensor | SNSN-0013A | | shellyplusht | Shelly Plus HT with temperature + humidity sensor | SNSN-0013A |
| shellyplussmoke | Shelly Plus Smoke sensor | SNSN-0031Z | | shellyplussmoke | Shelly Plus Smoke sensor | SNSN-0031Z |
### Generation 2 Pro series ### Generation 2 Pro series
@ -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 | | 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 | | 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 ## Binding Configuration
The binding has the following configuration options: 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). 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. 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 ### Password Protected Devices
The Shelly devices can be configured to require authorization through a user id and password. 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 | | 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 | | 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 | | 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 ### 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 | | TEMP_OVER | Above "temperature over" threshold |
| VIBRATION | A vibration/tamper was detected (DW2 only) | | 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 ## 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 | | | timerActive | Switch | yes | Relay #1: ON: An auto-on/off timer is active |
| | button | Trigger | yes | Event trigger, see section Button Events | | | 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 ## Full Example
### shelly.things ### shelly.things
@ -1582,4 +1646,3 @@ sitemap demo label="Home"
Number item=Shelly_Power Number item=Shelly_Power
} }
} }
```

View File

@ -85,11 +85,14 @@ public class ShellyBindingConstants {
THING_TYPE_SHELLYPLUSSMOKE, // THING_TYPE_SHELLYPLUSSMOKE, //
THING_TYPE_SHELLYPLUSPLUGS, // THING_TYPE_SHELLYPLUSPLUGS, //
THING_TYPE_SHELLYPLUSPLUGUS, // THING_TYPE_SHELLYPLUSPLUGUS, //
THING_TYPE_SHELLYBLUBUTTON, //
THING_TYPE_SHELLYBLUDW, //
THING_TYPE_SHELLYPROTECTED, // THING_TYPE_SHELLYPROTECTED, //
THING_TYPE_SHELLYUNKNOWN); THING_TYPE_SHELLYUNKNOWN);
// Thing Configuration Properties // Thing Configuration Properties
public static final String CONFIG_DEVICEIP = "deviceIp"; 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_USERID = "userId";
public static final String CONFIG_HTTP_PASSWORD = "password"; public static final String CONFIG_HTTP_PASSWORD = "password";
public static final String CONFIG_UPDATE_INTERVAL = "updateInterval"; 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_TYPE = "deviceType";
public static final String PROPERTY_DEV_MODE = "deviceMode"; public static final String PROPERTY_DEV_MODE = "deviceMode";
public static final String PROPERTY_DEV_GEN = "deviceGeneration"; 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_HWREV = "deviceHwRev";
public static final String PROPERTY_HWBATCH = "deviceHwBatch"; public static final String PROPERTY_HWBATCH = "deviceHwBatch";
public static final String PROPERTY_UPDATE_PERIOD = "devUpdatePeriod"; public static final String PROPERTY_UPDATE_PERIOD = "devUpdatePeriod";
@ -230,6 +234,7 @@ public class ShellyBindingConstants {
// Device Status // Device Status
public static final String CHANNEL_GROUP_DEV_STATUS = "device"; public static final String CHANNEL_GROUP_DEV_STATUS = "device";
public static final String CHANNEL_DEVST_NAME = "deviceName"; 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_UPTIME = "uptime";
public static final String CHANNEL_DEVST_HEARTBEAT = "heartBeat"; public static final String CHANNEL_DEVST_HEARTBEAT = "heartBeat";
public static final String CHANNEL_DEVST_RSSI = "wifiSignal"; 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 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 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 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.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration; import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler; 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.ShellyLightHandler;
import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface; import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.handler.ShellyProtectedHandler; import org.openhab.binding.shelly.internal.handler.ShellyProtectedHandler;
@ -103,6 +104,11 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
this.coapServer = new Shelly1CoapServer(); this.coapServer = new Shelly1CoapServer();
} }
@Activate
void activate() {
thingTable.startDiscoveryService(bundleContext);
}
@Override @Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) { public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(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_SHELLYRGBW2_WHITE_STR) || thingType.equals(THING_TYPE_SHELLYDUORGBW_STR) || thingType.equals(THING_TYPE_SHELLYRGBW2_WHITE_STR) || thingType.equals(THING_TYPE_SHELLYDUORGBW_STR)
|| thingType.equals(THING_TYPE_SHELLYVINTAGE_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()); thingTypeUID.toString());
handler = new ShellyLightHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient); 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)) { } 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()); thingTypeUID.toString());
handler = new ShellyRelayHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient); handler = new ShellyRelayHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
} }
@ -143,20 +153,13 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
return null; 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. * Remove handler of things.
*/ */
@Override @Override
protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) { protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) {
if (thingHandler instanceof ShellyBaseHandler) { if (thingHandler instanceof ShellyBaseHandler) {
((ShellyBaseHandler) thingHandler).stop();
String uid = thingHandler.getThing().getUID().getAsString(); String uid = thingHandler.getThing().getUID().getAsString();
thingTable.removeThing(uid); thingTable.removeThing(uid);
} }
@ -185,4 +188,12 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
public ShellyBindingConfiguration getBindingConfig() { public ShellyBindingConfiguration getBindingConfig() {
return bindingConfig; 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]); string[1]);
} else if (isMalformedURL()) { } else if (isMalformedURL()) {
message = "Invalid URL: " + url; message = "Invalid URL: " + url;
} else if (isJsonError()) {
message = getString(getMessage());
} else if (isTimeout()) { } else if (isTimeout()) {
message = "API Timeout for " + url; message = "API Timeout for " + url;
} else if (!isConnectionError()) { } else if (!isConnectionError()) {

View File

@ -46,7 +46,7 @@ public interface ShellyApiInterface {
ShellySettingsStatus getStatus() throws ShellyApiException; ShellySettingsStatus getStatus() throws ShellyApiException;
void setLedStatus(String ledName, Boolean value) throws ShellyApiException; void setLedStatus(String ledName, boolean value) throws ShellyApiException;
void setSleepTime(int value) throws ShellyApiException; void setSleepTime(int value) throws ShellyApiException;
@ -54,9 +54,9 @@ public interface ShellyApiInterface {
void setRelayTurn(int id, String turnMode) throws ShellyApiException; void setRelayTurn(int id, String turnMode) throws ShellyApiException;
public void resetMeterTotal(int id) throws ShellyApiException; void resetMeterTotal(int id) throws ShellyApiException;
public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException; ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException;
void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException; void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException;
@ -80,7 +80,6 @@ public interface ShellyApiInterface {
void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException; void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException;
// Valve
void setValveMode(int id, boolean auto) throws ShellyApiException; void setValveMode(int id, boolean auto) throws ShellyApiException;
void setValveTemperature(int valveId, int value) 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 sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException;
void postEvent(String device, String index, String event, Map<String, String> parms) throws ShellyApiException;
void close(); void close();
void startScan();
} }

View File

@ -67,6 +67,8 @@ public class ShellyDeviceProfile {
public boolean auth = false; public boolean auth = false;
public boolean alwaysOn = true; public boolean alwaysOn = true;
public boolean isGen2 = false; public boolean isGen2 = false;
public boolean isBlu = false;
public String gateway = "";
public String hwRev = ""; public String hwRev = "";
public String hwBatchId = ""; public String hwBatchId = "";
@ -125,6 +127,10 @@ public class ShellyDeviceProfile {
// Shelly UNI uses ext_temperature array, reformat to avoid GSON exception // Shelly UNI uses ext_temperature array, reformat to avoid GSON exception
json = json.replace("ext_temperature", "ext_temperature_array"); 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; settingsJson = json;
settings = fromJson(gson, json, ShellySettingsGlobal.class); settings = fromJson(gson, json, ShellySettingsGlobal.class);
@ -185,6 +191,8 @@ public class ShellyDeviceProfile {
return; return;
} }
isBlu = thingType.startsWith("shellyblu"); // e.g. SBBT for BU Button
isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2); isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2);
isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR); isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR);
isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_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 isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR);
boolean isUNI = thingType.equals(THING_TYPE_SHELLYUNI_STR); boolean isUNI = thingType.equals(THING_TYPE_SHELLYUNI_STR);
isHT = thingType.equals(THING_TYPE_SHELLYHT_STR) || thingType.equals(THING_TYPE_SHELLYPLUSHT_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); isMotion = thingType.startsWith(THING_TYPE_SHELLYMOTION_STR);
isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR); isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR);
isIX = thingType.equals(THING_TYPE_SHELLYIX3_STR) || thingType.equals(THING_TYPE_SHELLYPLUSI4_STR) isIX = thingType.equals(THING_TYPE_SHELLYIX3_STR) || thingType.equals(THING_TYPE_SHELLYPLUSI4_STR)
|| thingType.equals(THING_TYPE_SHELLYPLUSI4DC_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; isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isMotion || isSense || isTRV;
hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion || isTRV; hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion || isTRV;
isTRV = thingType.equals(THING_TYPE_SHELLYTRV_STR); isTRV = thingType.equals(THING_TYPE_SHELLYTRV_STR);
@ -241,7 +251,8 @@ public class ShellyDeviceProfile {
} else if (hasRelays) { } else if (hasRelays) {
return numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx; return numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx;
} else if (isRGBW2) { } 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; : CHANNEL_GROUP_LIGHT_CHANNEL + idx;
} else if (isLight) { } else if (isLight) {
return CHANNEL_GROUP_LIGHT_CONTROL; return CHANNEL_GROUP_LIGHT_CONTROL;

View File

@ -146,14 +146,15 @@ public class ShellyHttpClient {
HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes())); HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
} }
fillPostData(request, data); 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 // Do request and get response
ContentResponse contentResponse = request.send(); ContentResponse contentResponse = request.send();
apiResult = new ShellyApiResult(contentResponse); apiResult = new ShellyApiResult(contentResponse);
apiResult.httpCode = contentResponse.getStatus(); apiResult.httpCode = contentResponse.getStatus();
String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim(); 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 if (response.contains("\"error\":{")) { // Gen2
Shelly2RpcBaseMessage message = gson.fromJson(response, Shelly2RpcBaseMessage.class); Shelly2RpcBaseMessage message = gson.fromJson(response, Shelly2RpcBaseMessage.class);
@ -204,7 +205,7 @@ public class ShellyHttpClient {
StringContentProvider postData; StringContentProvider postData;
postData = new StringContentProvider(type, data, StandardCharsets.UTF_8); postData = new StringContentProvider(type, data, StandardCharsets.UTF_8);
request.content(postData); 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() { public int getTimeoutsRecovered() {
return timeoutsRecovered; 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<ShellyRollerStatus> rollers;
public ArrayList<ShellySettingsLight> lights; public ArrayList<ShellySettingsLight> lights;
public ArrayList<ShellySettingsMeter> meters; public ArrayList<ShellySettingsMeter> meters;
public ArrayList<ShellySettingsEMeter> emeters; public ArrayList<ShellySettingsEMeter> emeters;
public Double totalCurrent;
public Double totalPower;
public Double totalReturned;
@SerializedName("ext_temperature") @SerializedName("ext_temperature")
public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
@SerializedName("ext_humidity") @SerializedName("ext_humidity")

View File

@ -30,14 +30,14 @@ import org.openhab.core.types.State;
*/ */
@NonNullByDefault @NonNullByDefault
public interface Shelly1CoIoTInterface { 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); 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[{}]={}", "{}: Check button[{}] for event trigger (inButtonMode={}, isButton={}, hasBattery={}, serial={}, count={}, lastEventCount[{}]={}",
thingName, idx, profile.inButtonMode(idx), profile.isButton, profile.hasBattery, serial, count, idx, thingName, idx, profile.inButtonMode(idx), profile.isButton, profile.hasBattery, serial, count, idx,
lastEventCount[idx]); lastEventCount[idx]);
if (profile.inButtonMode(idx) && ((profile.hasBattery && count == 1) if (profile.inButtonMode(idx) && ((profile.hasBattery && count == 1) || lastEventCount[idx] == -1
|| (lastEventCount[idx] != -1 && count != lastEventCount[idx]))) { || count != lastEventCount[idx])) {
if (!profile.isButton || (profile.isButton && (serial != 0x200))) { // skip duplicate on wake-up if (!profile.isButton || (profile.isButton && (serial != 0x200))) { // skip duplicate on wake-up
logger.debug("{}: Trigger event {}", thingName, inputEvent[idx]); logger.debug("{}: Trigger event {}", thingName, inputEvent[idx]);
thingHandler.triggerButton(group, idx, 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(); List<Option> options = response.getOptions().asSortedList();
String ip = response.getSourceContext().getPeerAddress().toString(); String ip = response.getSourceContext().getPeerAddress().toString();
boolean match = ip.contains(config.deviceIp); boolean match = ip.contains("/" + config.deviceIp + ":");
if (!match) { if (!match) {
// We can't identify device by IP, so we need to check the CoAP header's Global Device ID // We can't identify device by IP, so we need to check the CoAP header's Global Device ID
for (Option opt : options) { for (Option opt : options) {

View File

@ -23,5 +23,5 @@ import org.eclipse.jdt.annotation.Nullable;
*/ */
@NonNullByDefault @NonNullByDefault
public interface Shelly1CoapListener { 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 @Override
public void resetMeterTotal(int id) throws ShellyApiException { 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 @Override
@ -245,7 +245,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
} else if (profile.isLight) { } else if (profile.isLight) {
type = SHELLY_CLASS_LIGHT; 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); httpRequest(uri);
} }
@ -294,7 +294,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
} }
@Override @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)); httpRequest(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
} }
@ -752,4 +752,8 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
@Override @Override
public void close() { public void close() {
} }
@Override
public void startScan() {
}
} }

View File

@ -289,6 +289,16 @@ public class Shelly2ApiClient extends ShellyHttpClient {
return false; 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(); ShellySettingsMeter sm = new ShellySettingsMeter();
ShellySettingsEMeter emeter = status.emeters.get(0); ShellySettingsEMeter emeter = status.emeters.get(0);
sm.isValid = emeter.isValid = true; sm.isValid = emeter.isValid = true;
@ -683,7 +693,7 @@ public class Shelly2ApiClient extends ShellyHttpClient {
throw new ShellyApiException("Thing/profile not initialized!"); throw new ShellyApiException("Thing/profile not initialized!");
} }
ShellyDeviceProfile getProfile() throws ShellyApiException { protected ShellyDeviceProfile getProfile() throws ShellyApiException {
if (thing != null) { if (thing != null) {
return thing.getProfile(); 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_WSSETCONFIG = "WS.SetConfig";
public static final String SHELLYRPC_METHOD_SMOKE_SETCONFIG = "Smoke.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_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_NOTIFYSTATUS = "NotifyStatus"; // inbound status
public static final String SHELLYRPC_METHOD_NOTIFYFULLSTATUS = "NotifyFullStatus"; // inbound status from bat device 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_WIFICONNFAILED = "sta_connect_fail";
public static final String SHELLY2_EVENT_WIFIDISCONNECTED = "sta_disconnected"; 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 // Error Codes
public static final String SHELLY2_ERROR_OVERPOWER = "overpower"; public static final String SHELLY2_ERROR_OVERPOWER = "overpower";
public static final String SHELLY2_ERROR_OVERTEMP = "overtemp"; public static final String SHELLY2_ERROR_OVERTEMP = "overtemp";
@ -549,6 +564,13 @@ public class Shelly2ApiJsonDTO {
@SerializedName("n_current") @SerializedName("n_current")
public Double nCurrent; 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 { public static class Shelly2DeviceStatusEmData {
@ -754,6 +776,9 @@ public class Shelly2ApiJsonDTO {
// Cloud.SetConfig // Cloud.SetConfig
public Shelly2ConfigParms config; public Shelly2ConfigParms config;
// Script
public String name;
public Shelly2RpcRequestParams withConfig() { public Shelly2RpcRequestParams withConfig() {
config = new Shelly2ConfigParms(); config = new Shelly2ConfigParms();
return this; return this;
@ -779,6 +804,11 @@ public class Shelly2ApiJsonDTO {
params.pos = pos; params.pos = pos;
return this; return this;
} }
public Shelly2RpcRequest withName(String name) {
params.name = name;
return this;
}
} }
public static class Shelly2WsConfigResponse { public static class Shelly2WsConfigResponse {
@ -793,6 +823,30 @@ public class Shelly2ApiJsonDTO {
public Shelly2WsConfigResult result; 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 { public static class Shelly2RpcBaseMessage {
// Basic message format, e.g. // Basic message format, e.g.
// {"id":1,"src":"localweb528","method":"Shelly.GetConfig"} // {"id":1,"src":"localweb528","method":"Shelly.GetConfig"}
@ -804,8 +858,10 @@ public class Shelly2ApiJsonDTO {
public Integer id; public Integer id;
public String src; public String src;
public String dst; public String dst;
public String component;
public String method; public String method;
public Object params; public Object params;
public String event;
public Object result; public Object result;
public Shelly2AuthRequest auth; public Shelly2AuthRequest auth;
public Shelly2RpcMessageError error; public Shelly2RpcMessageError error;
@ -852,11 +908,49 @@ public class Shelly2ApiJsonDTO {
public String algorithm; 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 class Shelly2NotifyEvent {
public Integer id; public Integer id;
public Double ts; public Double ts;
public String component; public String component;
public String event; public String event;
public Shelly2NotifyEventMessage data;
public String msg; public String msg;
public Integer reason; public Integer reason;
@SerializedName("cfg_rev") @SerializedName("cfg_rev")
@ -869,7 +963,8 @@ public class Shelly2ApiJsonDTO {
} }
public static class Shelly2RpcNotifyEvent { public static class Shelly2RpcNotifyEvent {
public String src;
public Double ts; 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.api2.Shelly2ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*; 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.Shelly2RpcRequest.Shelly2RpcRequestParams;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse; 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.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.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface; import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable; 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 Logger logger = LoggerFactory.getLogger(Shelly2ApiRpc.class);
private final @Nullable ShellyThingTable thingTable; private final @Nullable ShellyThingTable thingTable;
private boolean initialized = false; protected boolean initialized = false;
private boolean discovery = false; private boolean discovery = false;
private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket(); private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
private Shelly2AuthResponse authInfo = new Shelly2AuthResponse(); private Shelly2AuthResponse authInfo = new Shelly2AuthResponse();
@ -139,6 +149,17 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
return initialized; return initialized;
} }
@Override
public void startScan() {
if (config.enableBluGateway) {
try {
installScript(SHELLY2_BLU_GWSCRIPT);
} catch (ShellyApiException e) {
}
}
}
@SuppressWarnings("null")
@Override @Override
public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException { public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile(); ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
@ -269,6 +290,19 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
if (!discovery) { if (!discovery) {
getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status) getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device 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; 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 @Override
public void onConnect(String deviceIp, boolean connected) { public void onConnect(String deviceIp, boolean connected) {
if (thing == null && thingTable != null) { if (thing == null && thingTable != null) {
@ -755,7 +900,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
* categories (e.g. bulbs) * categories (e.g. bulbs)
*/ */
@Override @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"); 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 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 { public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
String json = ""; String json = "";
Shelly2RpcBaseMessage req = buildRequest(method, params); Shelly2RpcBaseMessage req = buildRequest(method, params);
@ -905,8 +1051,18 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
throw e; throw e;
} }
} }
json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result); Shelly2RpcBaseMessage response = gson.fromJson(json, Shelly2RpcBaseMessage.class);
return fromJson(gson, json, classOfT); 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 { 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.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException; import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi; 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.Shelly2RpcBaseMessage;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent; import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus; 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.ShellyThingInterface;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable; import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -250,8 +252,34 @@ public class Shelly2RpcSocket {
handler.onNotifyStatus(status); handler.onNotifyStatus(status);
return; return;
case SHELLYRPC_METHOD_NOTIFYEVENT: case SHELLYRPC_METHOD_NOTIFYEVENT:
handler.onNotifyEvent(fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class)); Shelly2RpcNotifyEvent events = fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class);
return; 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: default:
handler.onMessage(receivedMessage); handler.onMessage(receivedMessage);
} }
@ -259,7 +287,9 @@ public class Shelly2RpcSocket {
logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName, logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName,
getString(message.src), receivedMessage); 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); 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 @NonNullByDefault
public interface Shelly2RpctInterface { 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 @NonNullByDefault
public class ShellyThingConfiguration { public class ShellyThingConfiguration {
public String deviceIp = ""; // ip address of thedevice 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 userId = ""; // userid for http basic auth
public String password = ""; // password 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 localIp = ""; // local ip addresses used to create callback url
public String localPort = "8080"; public String localPort = "8080";
public String serviceName = ""; 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 = "SPSW-004PE16EU";
public static final String SHELLYDT_PRO4PM_2 = "SPSW-104PE16EU"; 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 // Thing names
public static final String THING_TYPE_SHELLY1_STR = "shelly1"; public static final String THING_TYPE_SHELLY1_STR = "shelly1";
public static final String THING_TYPE_SHELLY1L_STR = "shelly1l"; 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_SHELLYPRO3EM_STR = "shellypro3em";
public static final String THING_TYPE_SHELLYPRO4PM_STR = "shellypro4pm"; 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_SHELLYPROTECTED_STR = "shellydevice";
public static final String THING_TYPE_SHELLYUNKNOWN_STR = "shellyunknown"; 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, public static final ThingTypeUID THING_TYPE_SHELLYPRO4PM = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYPRO4PM_STR); 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<>(); private static final Map<String, String> THING_TYPE_MAPPING = new LinkedHashMap<>();
static { static {
// mapping by device type id // 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, THING_TYPE_SHELLYPRO4PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM_2, 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 // mapping by thing type
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY1_STR, THING_TYPE_SHELLY1_STR); THING_TYPE_MAPPING.put(THING_TYPE_SHELLY1_STR, THING_TYPE_SHELLY1_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY1PM_STR, THING_TYPE_SHELLY1PM_STR); THING_TYPE_MAPPING.put(THING_TYPE_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.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi; import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc; 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.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration; import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.discovery.ShellyThingCreator; 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 int cacheCount = UPDATE_SETTINGS_INTERVAL_SECONDS / UPDATE_STATUS_INTERVAL_SECONDS;
private final boolean gen2; private final boolean gen2;
private final boolean blu;
protected boolean autoCoIoT = false; protected boolean autoCoIoT = false;
// Thing status // Thing status
@ -151,7 +153,9 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
gen = "2"; gen = "2";
} }
gen2 = "2".equals(gen); 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) { if (gen2) {
config.eventsCoIoT = false; config.eventsCoIoT = false;
} }
@ -162,7 +166,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
@Override @Override
public boolean checkRepresentation(String key) { 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()); || key.equalsIgnoreCase(config.serviceName) || key.equalsIgnoreCase(getThingName());
} }
@ -176,15 +180,13 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
boolean start = true; boolean start = true;
try { try {
initializeThingConfig(); initializeThingConfig();
logger.debug("{}: Device config: IP address={}, HTTP user/password={}/{}, update interval={}", logger.debug("{}: Device config: Device address={}, HTTP user/password={}/{}, update interval={}",
thingName, config.deviceIp, config.userId.isEmpty() ? "<non>" : config.userId, thingName, config.deviceAddress, config.userId.isEmpty() ? "<non>" : config.userId,
config.password.isEmpty() ? "<none>" : "***", config.updateInterval); config.password.isEmpty() ? "<none>" : "***", config.updateInterval);
logger.debug( logger.debug(
"{}: Configured Events: Button: {}, Switch (on/off): {}, Push: {}, Roller: {}, Sensor: {}, CoIoT: {}, Enable AutoCoIoT: {}", "{}: Configured Events: Button: {}, Switch (on/off): {}, Push: {}, Roller: {}, Sensor: {}, CoIoT: {}, Enable AutoCoIoT: {}",
thingName, config.eventsButton, config.eventsSwitch, config.eventsPush, config.eventsRoller, thingName, config.eventsButton, config.eventsSwitch, config.eventsPush, config.eventsRoller,
config.eventsSensorReport, config.eventsCoIoT, bindingConfig.autoCoIoT); config.eventsSensorReport, config.eventsCoIoT, bindingConfig.autoCoIoT);
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING,
messages.get("status.unknown.initializing"));
start = initializeThing(); start = initializeThing();
} catch (ShellyApiException e) { } catch (ShellyApiException e) {
ShellyApiResult res = e.getApiResult(); ShellyApiResult res = e.getApiResult();
@ -225,6 +227,13 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
return httpClient; return httpClient;
} }
@Override
public void startScan() {
if (api.isInitialized()) {
api.startScan();
}
}
/** /**
* This routine is called every time the Thing configuration has been changed * This routine is called every time the Thing configuration has been changed
*/ */
@ -257,12 +266,15 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
resetStats(); resetStats();
logger.debug("{}: Start initializing for thing {}, type {}, IP address {}, Gen2: {}, CoIoT: {}", thingName, logger.debug("{}: Start initializing for thing {}, type {}, IP address {}, Gen2: {}, CoIoT: {}", thingName,
getThing().getLabel(), thingType, config.deviceIp, gen2, config.eventsCoIoT); getThing().getLabel(), thingType, config.deviceAddress, gen2, config.eventsCoIoT);
if (config.deviceIp.isEmpty()) { if (config.deviceAddress.isEmpty()) {
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "config-status.error.missing-device-ip"); setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "config-status.error.missing-device-address");
return false; return false;
} }
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING,
messages.get("status.unknown.initializing"));
profile.initFromThingType(thingType); // do some basic initialization 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 // 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); checkVersion(tmpPrf, tmpPrf.status);
startCoap(config, tmpPrf); startCoap(config, tmpPrf);
if (!gen2) { if (!gen2 && !blu) {
api.setActionURLs(); // register event urls api.setActionURLs(); // register event urls
} }
@ -358,7 +370,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
return; return;
} }
if (!profile.isInitialized() || (isThingOffline() && profile.alwaysOn)) { if (!profile.isInitialized()) {
logger.debug("{}: {}", thingName, messages.get("command.init", command)); logger.debug("{}: {}", thingName, messages.get("command.init", command));
initializeThing(); initializeThing();
} else { } else {
@ -468,6 +480,15 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
logger.warn("{}: {} - {}", thingName, messages.get("command.failed", command, channelUID), logger.warn("{}: {} - {}", thingName, messages.get("command.failed", command, channelUID),
e.toString()); 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) { } catch (IllegalArgumentException e) {
logger.debug("{}: {}", thingName, messages.get("command.failed", command, channelUID)); logger.debug("{}: {}", thingName, messages.get("command.failed", command, channelUID));
} }
@ -518,8 +539,11 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
postEvent(ALARM_TYPE_RESTARTED, true); 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,
setThingOnline(); // but not while firmware update is in progress
if (getThingStatusDetail() != ThingStatusDetail.FIRMWARE_UPDATING) {
setThingOnline();
}
// map status to channels // map status to channels
updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_NAME, getStringType(profile.settings.name)); 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("{}: Shelly settings info for {}: {}", thingName, profile.hostname, profile.settingsJson);
logger.debug("{}: Device " logger.debug("{}: Device "
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{}), ext. Switch Add-On: {}" + "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, + ",alwaysOn:{}, updatePeriod:{}sec", thingName, profile.hasRelays, profile.numRelays, profile.isRoller,
profile.numRollers, profile.isDimmer, profile.numMeters, profile.isEMeter, profile.numRollers, profile.isDimmer, profile.numMeters, profile.isEMeter,
profile.settings.extSwitch != null ? "installed" : "n/a", profile.isSensor, profile.isDW, profile.settings.extSwitch != null ? "installed" : "n/a", profile.isSensor, profile.isDW,
profile.hasBattery, profile.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "", profile.hasBattery, profile.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "",
profile.isSense, profile.isMotion, profile.isLight, profile.isBulb, profile.isDuo, profile.isRGBW2, 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 if (profile.status.extTemperature != null || profile.status.extHumidity != null
|| profile.status.extVoltage != null || profile.status.extAnalogInput != null) { || profile.status.extVoltage != null || profile.status.extAnalogInput != null) {
logger.debug("{}: Shelly Add-On detected with at least 1 external sensor", thingName); 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 // Update uptime and WiFi, internal temp
ShellyComponents.updateDeviceStatus(this, status); ShellyComponents.updateDeviceStatus(this, status);
stats.wifiRssi = status.wifiSta.rssi; stats.wifiRssi = getInteger(status.wifiSta.rssi);
if (api.isInitialized()) { if (api.isInitialized()) {
stats.timeoutErrors = api.getTimeoutErrors(); stats.timeoutErrors = api.getTimeoutErrors();
@ -829,9 +853,10 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
* @return true if event was processed * @return true if event was processed
*/ */
@Override @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) { 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, logger.debug("{}: Event received: class={}, index={}, parameters={}", deviceName, type, deviceIndex,
parameters); parameters);
int idx = !deviceIndex.isEmpty() ? Integer.parseInt(deviceIndex) : 1; int idx = !deviceIndex.isEmpty() ? Integer.parseInt(deviceIndex) : 1;
@ -851,7 +876,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
String payload = ""; String payload = "";
String parmType = getString(parameters.get("type")); String parmType = getString(parameters.get("type"));
String event = !parmType.isEmpty() ? parmType : type; String event = !parmType.isEmpty() ? parmType : type;
boolean isButton = profile.inButtonMode(idx - 1); boolean isButton = profile.inButtonMode(idx - 1) || type.equals("button");
switch (event) { switch (event) {
case SHELLY_EVENT_SHORTPUSH: case SHELLY_EVENT_SHORTPUSH:
case SHELLY_EVENT_DOUBLE_SHORTPUSH: case SHELLY_EVENT_DOUBLE_SHORTPUSH:
@ -959,23 +984,31 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
thingName = getString(properties.get(PROPERTY_SERVICE_NAME)); thingName = getString(properties.get(PROPERTY_SERVICE_NAME));
if (thingName.isEmpty()) { if (thingName.isEmpty()) {
thingName = getString(thingType + "-" + getString(getThing().getUID().getId())).toLowerCase(); thingName = getString(thingType + "-" + getString(getThing().getUID().getId())).toLowerCase();
logger.debug("{}: Thing name derived from UID {}", thingName, getString(getThing().getUID().toString()));
} }
config = getConfigAs(ShellyThingConfiguration.class); config = getConfigAs(ShellyThingConfiguration.class);
if (config.deviceIp.isEmpty()) { if (config.deviceAddress.isEmpty()) {
logger.debug("{}: IP address for the device must not be empty", thingName); // may not set in .things file 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; return;
} }
try {
InetAddress addr = InetAddress.getByName(config.deviceIp); config.deviceAddress = config.deviceAddress.toLowerCase().replaceAll(":", ""); // remove : from MAC address and
String saddr = addr.getHostAddress(); // convert to lower case
if (!config.deviceIp.equals(saddr)) { if (!config.deviceIp.isEmpty()) {
logger.debug("{}: hostname {} resolved to IP address {}", thingName, config.deviceIp, saddr); try {
config.deviceIp = saddr; InetAddress addr = InetAddress.getByName(config.deviceIp);
String saddr = addr.getHostAddress();
if (!config.deviceIp.equals(saddr)) {
logger.debug("{}: hostname {} resolved to IP address {}", thingName, config.deviceIp, saddr);
config.deviceIp = saddr;
}
} catch (UnknownHostException e) {
logger.debug("{}: Unable to resolve hostname {}", thingName, config.deviceIp);
} }
} catch (UnknownHostException e) {
logger.debug("{}: Unable to resolve hostname {}", thingName, config.deviceIp);
} }
config.serviceName = getString(properties.get(PROPERTY_SERVICE_NAME)); config.serviceName = getString(properties.get(PROPERTY_SERVICE_NAME));
@ -1406,9 +1439,11 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
properties.put(PROPERTY_MAC_ADDRESS, profile.mac); properties.put(PROPERTY_MAC_ADDRESS, profile.mac);
properties.put(PROPERTY_FIRMWARE_VERSION, profile.fwVersion + "/" + profile.fwDate); properties.put(PROPERTY_FIRMWARE_VERSION, profile.fwVersion + "/" + profile.fwDate);
properties.put(PROPERTY_DEV_MODE, profile.mode); properties.put(PROPERTY_DEV_MODE, profile.mode);
properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays)); if (profile.hasRelays) {
properties.put(PROPERTY_NUM_ROLLERS, String.valueOf(profile.numRollers)); properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays));
properties.put(PROPERTY_NUM_METER, String.valueOf(profile.numMeters)); 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)); properties.put(PROPERTY_UPDATE_PERIOD, String.valueOf(profile.updatePeriod));
if (!profile.hwRev.isEmpty()) { if (!profile.hwRev.isEmpty()) {
properties.put(PROPERTY_HWREV, profile.hwRev); 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) { public static boolean updateDeviceStatus(ShellyThingInterface thingHandler, ShellySettingsStatus status) {
ShellyDeviceProfile profile = thingHandler.getProfile(); ShellyDeviceProfile profile = thingHandler.getProfile();
if (!profile.gateway.isEmpty()) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_GATEWAY, getStringType(profile.gateway));
}
if (!thingHandler.areChannelsCreated()) { if (!thingHandler.areChannelsCreated()) {
thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createDeviceChannels(thingHandler.getThing(), thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createDeviceChannels(thingHandler.getThing(),
thingHandler.getProfile(), status)); thingHandler.getProfile(), status));
@ -177,6 +180,8 @@ public class ShellyComponents {
break; break;
} }
if (pos != -1) { 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, updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT)); toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
@ -238,7 +243,7 @@ public class ShellyComponents {
// convert Watt/Min to kw/h // convert Watt/Min to kw/h
if (meter.total != null) { 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, updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
toQuantityType(kwh, DIGITS_KWH, Units.KILOWATT_HOUR)); toQuantityType(kwh, DIGITS_KWH, Units.KILOWATT_HOUR));
accumulatedTotal += kwh; accumulatedTotal += kwh;
@ -263,13 +268,15 @@ public class ShellyComponents {
.createEMeterChannels(thingHandler.getThing(), profile, emeter, groupName)); .createEMeterChannels(thingHandler.getThing(), profile, emeter, groupName));
} }
// convert Watt/Hour tok w/h // convert Watt/Hour to kw/h
double total = getDouble(emeter.total) / 1000 / 60;
double totalReturned = getDouble(emeter.totalReturned) / 1000;
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS, updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
toQuantityType(getDouble(emeter.power), DIGITS_WATT, Units.WATT)); toQuantityType(getDouble(emeter.power), DIGITS_WATT, Units.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH, updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
toQuantityType(getDouble(emeter.total) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR)); toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET, toQuantityType( updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET,
getDouble(emeter.totalReturned) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR)); toQuantityType(totalReturned, DIGITS_KWH, Units.KILOWATT_HOUR));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_REACTWATTS, updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_REACTWATTS,
toQuantityType(getDouble(emeter.reactive), DIGITS_WATT, Units.WATT)); toQuantityType(getDouble(emeter.reactive), DIGITS_WATT, Units.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_VOLTAGE, updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_VOLTAGE,
@ -280,8 +287,8 @@ public class ShellyComponents {
toQuantityType(computePF(emeter), Units.PERCENT)); toQuantityType(computePF(emeter), Units.PERCENT));
accumulatedWatts += getDouble(emeter.power); accumulatedWatts += getDouble(emeter.power);
accumulatedTotal += getDouble(emeter.total) / 1000; accumulatedTotal += total;
accumulatedReturned += getDouble(emeter.totalReturned) / 1000; accumulatedReturned += totalReturned;
if (updated) { if (updated) {
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp()); thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
} }
@ -327,7 +334,7 @@ public class ShellyComponents {
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS, updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
toQuantityType(getDouble(currentWatts), DIGITS_WATT, Units.WATT)); toQuantityType(getDouble(currentWatts), DIGITS_WATT, Units.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH, 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) { if (updated && timestamp > 0) {
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
@ -336,12 +343,14 @@ public class ShellyComponents {
} }
if (!profile.isRoller && !profile.isRGBW2) { if (!profile.isRoller && !profile.isRGBW2) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS, thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS, toQuantityType(
toQuantityType(accumulatedWatts, DIGITS_WATT, Units.WATT)); status.totalPower != null ? status.totalPower : accumulatedWatts, DIGITS_WATT, Units.WATT));
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL, 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, 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. * 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); 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 Logger logger = LoggerFactory.getLogger(ShellyLightHandler.class);
private final Map<Integer, ShellyColorUtils> channelColors; 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, public ShellyLightHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, final ShellyThingTable thingTable, final ShellyBindingConfiguration bindingConfig, final ShellyThingTable thingTable,
final Shelly1CoapServer coapServer, final HttpClient httpClient) { 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, private boolean handleColorPicker(ShellyDeviceProfile profile, Integer lightId, ShellyColorUtils col,
Command command) throws ShellyApiException { Command command) throws ShellyApiException {
boolean updated = false; boolean updated = false;

View File

@ -28,29 +28,29 @@ import org.openhab.core.types.State;
@NonNullByDefault @NonNullByDefault
public interface ShellyManagerInterface { 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 @NonNullByDefault
public interface ShellyThingInterface { public interface ShellyThingInterface {
ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException; public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
@Nullable public @Nullable List<StateOption> getStateOptions(ChannelTypeUID uid);
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 java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy; 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 * 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) @Component(service = ShellyThingTable.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
public class ShellyThingTable { public class ShellyThingTable {
private Map<String, ShellyThingInterface> thingTable = new ConcurrentHashMap<>(); private Map<String, ShellyThingInterface> thingTable = new ConcurrentHashMap<>();
private @Nullable ShellyBluDiscoveryService bluDiscoveryService;
public void addThing(String key, ShellyThingInterface thing) { public void addThing(String key, ShellyThingInterface thing) {
if (thingTable.containsKey(key)) { if (thingTable.containsKey(key)) {
@ -37,7 +43,7 @@ public class ShellyThingTable {
thingTable.put(key, thing); thingTable.put(key, thing);
} }
public ShellyThingInterface getThing(String key) { public @Nullable ShellyThingInterface findThing(String key) {
ShellyThingInterface t = thingTable.get(key); ShellyThingInterface t = thingTable.get(key);
if (t != null) { if (t != null) {
return t; return t;
@ -48,7 +54,15 @@ public class ShellyThingTable {
return t; return t;
} }
} }
throw new IllegalArgumentException(); return null;
}
public ShellyThingInterface getThing(String key) {
ShellyThingInterface t = findThing(key);
if (t == null) {
throw new IllegalArgumentException();
}
return t;
} }
public void removeThing(String key) { public void removeThing(String key) {
@ -64,4 +78,36 @@ public class ShellyThingTable {
public int size() { public int size() {
return thingTable.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() { private void cleanMap() {
long currentTime = new Date().getTime(); long currentTime = new Date().getTime();
for (K key : timeMap.keySet()) { 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_GETDEB1 = "getdebug1";
public static final String ACTION_NONE = "-"; 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 HEADER_HTML = "header.html";
public static final String OVERVIEW_HTML = "overview.html"; public static final String OVERVIEW_HTML = "overview.html";
public static final String OVERVIEW_HEADER = "ov_header.html"; public static final String OVERVIEW_HEADER = "ov_header.html";

View File

@ -164,7 +164,7 @@ public class ShellyManagerPage {
} }
String html = ""; String html = "";
String file = TEMPLATE_PATH + template; String file = BUNDLE_RESOURCE_SNIPLETS + "/" + template;
logger.debug("Read HTML from {}", file); logger.debug("Read HTML from {}", file);
ClassLoader cl = ShellyManagerInterface.class.getClassLoader(); ClassLoader cl = ShellyManagerInterface.class.getClassLoader();
if (cl != null) { if (cl != null) {

View File

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

View File

@ -125,6 +125,7 @@ public class ShellyChannelDefinitions {
CHANNEL_DEFINITIONS CHANNEL_DEFINITIONS
// Device // Device
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_NAME, "deviceName", ITEMT_STRING)) .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_ITEMP, "deviceTemp", ITEMT_TEMP))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_WAKEUP, "sensorWakeup", ITEMT_STRING)) .add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_WAKEUP, "sensorWakeup", ITEMT_STRING))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ACCUWATTS, "meterAccuWatts", ITEMT_POWER)) .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<>(); Map<String, Channel> add = new LinkedHashMap<>();
addChannel(thing, add, profile.settings.name != null, CHGR_DEVST, CHANNEL_DEVST_NAME); 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 if (!profile.isSensor && !profile.isIX
&& status.temperature != SHELLY_API_INVTEMP) { && ((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 // Only some devices report the internal device temp
addChannel(thing, add, status.tmp != null || status.temperature != null, CHGR_DEVST, CHANNEL_DEVST_ITEMP); addChannel(thing, add, 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.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.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.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.extHumidity != null, CHGR_SENSOR, CHANNEL_ESENSOR_HUMIDITY);
addChannel(thing, add, profile.status.extVoltage != null, CHGR_SENSOR, CHANNEL_ESENSOR_VOLTAGE); 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++) { for (int i = 0; i < profile.numInputs; i++) {
String group = profile.getInputGroup(i); String group = profile.getInputGroup(i);
String suffix = profile.getInputSuffix(i); // multi ? String.valueOf(i + 1) : ""; 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, addChannel(thing, add, true, group,
(!profile.isRoller ? CHANNEL_BUTTON_TRIGGER + suffix : CHANNEL_EVENT_TRIGGER)); (!profile.isRoller ? CHANNEL_BUTTON_TRIGGER + suffix : CHANNEL_EVENT_TRIGGER));
if (profile.inButtonMode(i)) { if (profile.inButtonMode(i)) {

View File

@ -6,7 +6,6 @@
<type>binding</type> <type>binding</type>
<name>@text/addon.shelly.name</name> <name>@text/addon.shelly.name</name>
<description>@text/addon.shelly.description</description> <description>@text/addon.shelly.description</description>
<connection>local</connection>
<config-description> <config-description>
<parameter name="defaultUserId" type="text"> <parameter name="defaultUserId" type="text">

View File

@ -22,6 +22,11 @@
<unitLabel>seconds</unitLabel> <unitLabel>seconds</unitLabel>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </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>
<config-description uri="thing-type:shelly:roller-gen2"> <config-description uri="thing-type:shelly:roller-gen2">
@ -52,6 +57,11 @@
<unitLabel>seconds</unitLabel> <unitLabel>seconds</unitLabel>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </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>
<config-description uri="thing-type:shelly:battery-gen2"> <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. 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 # 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 message.config-status.error.missing-userid = No user ID in the Thing configuration
# Thing status descriptions # Thing status descriptions
@ -43,7 +43,7 @@ message.event.filtered = Event filtered: {0}
message.coap.init.failed = Unable to start CoIoT: {0} message.coap.init.failed = Unable to start CoIoT: {0}
message.discovery.disabled = Device is marked as non-discoverable, will be skipped 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.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.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 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.shellyix3.description = Shelly ix3 (Activation Device with 3 inputs)
thing-type.shelly.shellypludht.description = Shelly Plus HT - Temperature and Humidity Sensor 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 # Pro Devices
thing-type.shelly.shellypro1.description = Shelly Pro 1 - Single Relay Switch 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.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.shellypro3em.description = Shelly Pro 3EM - 3xPower Meter
thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter
# Plus devices
# Plus/Pro devices
thing-type.shelly.shellyplus1.description = Shelly Plus 1 (Single Relay Switch) 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.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.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.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.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.shellyplussmoke.description = Shelly Plus Smoke - Smoke Detector with Alarm
thing-type.shelly.shellyplusi4.description = Shelly Plus i4 - 4xInput Device thing-type.shelly.shellyplusi4.description = Shelly Plus i4 - 4xInput Device
thing-type.shelly.shellyplusi4dc.description = Shelly Plus i4DC - 4xDC Input 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.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.shellypro1pm.description = Shelly Pro 1PM - Single Relay Switch with Power Meter
thing-type.shelly.shellypro2-relay.description = Shelly Pro 2 - Dual Relay Switch thing-type.shelly.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.shellypro3.description = Shelly Pro 3 - 3xRelay Switch
thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter 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 config - shellydevice
thing-type.config.shelly.deviceIp.label = IP Address thing-type.config.shelly.deviceIp.label = IP Address
thing-type.config.shelly.deviceIp.description = IP Address of the Shelly device 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.label = User ID
thing-type.config.shelly.userId.description = User ID for API access thing-type.config.shelly.userId.description = User ID for API access
thing-type.config.shelly.password.label = Password thing-type.config.shelly.password.label = Password
thing-type.config.shelly.password.description = Password for API access thing-type.config.shelly.password.description = Password for API access
thing-type.config.shelly.updateInterval.label = Status Interval thing-type.config.shelly.updateInterval.label = Status Interval
thing-type.config.shelly.updateInterval.description = Interval for the device status update 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.label = Button Events
thing-type.config.shelly.eventsButton.description = Activates the Button Action URLS thing-type.config.shelly.eventsButton.description = Activates the Button Action URLS
thing-type.config.shelly.eventsPush.label = Push Events 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.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.label = Accumulated Total Power
channel-type.shelly.meterAccuTotal.description = Accumulated Total Power in kW/h of the device (including all meters) 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.label = Accumulated Apparent Power
channel-type.shelly.meterAccuReturned.description = Accumulated Returned Power in kW/h of the device (including all meters) 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.label = Reactive Energy
channel-type.shelly.meterReactive.description = Instantaneous reactive power in Watts (W) channel-type.shelly.meterReactive.description = Instantaneous reactive power in Watts (W)
channel-type.shelly.lastPower1.label = Last Power 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.senseKey.description = Send a defined key code
channel-type.shelly.deviceName.label = Device Name channel-type.shelly.deviceName.label = Device Name
channel-type.shelly.deviceName.description = Symbolic Device Name as configured in the Shelly App 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.label = Uptime
channel-type.shelly.uptime.description = Number of seconds since the device was powered up channel-type.shelly.uptime.description = Number of seconds since the device was powered up
channel-type.shelly.heartBeat.label = Last Heartbeat channel-type.shelly.heartBeat.label = Last Heartbeat

View File

@ -59,6 +59,13 @@
<state readOnly="true"> <state readOnly="true">
</state> </state>
</channel-type> </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"> <channel-type id="calibrated" advanced="true">
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>@text/channel-type.shelly.calibrated.label</label> <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);
}