Skip to content

Commit 55c0df5

Browse files
committed
Initial support for LoRa add-on
Signed-off-by: Markus Michels <markus7017@gmail.com>
1 parent 4179b89 commit 55c0df5

13 files changed

Lines changed: 352 additions & 12 deletions

File tree

bundles/org.openhab.binding.shelly/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ A new alarm will be triggered on a new condition or every 5 minutes if the condi
474474
| VALVE_ERROR | Device reported a problem with the valve. |
475475
| VIBRATION | Device reported vibration. |
476476
| LOW_BATTERY | Device reported low battery. |
477+
| LORA_RECEIVED| A datagram has been receive via LoRa protocol |
477478

478479
### Sensors
479480

@@ -495,6 +496,25 @@ Refer to section [Full Example](#full-example) for examples how to catch alarm t
495496

496497
Depending on the device type and firmware release channels might be not available or stay with value NaN.
497498

499+
### LoRa Add-On - Channel Group lora
500+
501+
The LoRa Add-On is a radio module for the LoRa WAN Network.
502+
This can be attacxhed to various Shelly Gen3 or Gen4 devices like Shelly Plus 1PM or Shelly Shutter.
503+
Gen1 or Gen2 devices do not support the LoRa Add-On even you might be able to activate it in the device's Web UI.
504+
Shelly Dimmer Gen 3 is an exception, it does NOT support any Add-Ons incl. the LoRa Add-On.
505+
506+
507+
| Group | Channel | Type | read-only | Description |
508+
| ------- | ------------ | ------- | --------- | --------------------------------------------------------------------------------- |
509+
| lora | dataRx | String | yes | Received LoRa Datagram, BASE64-encoded. Use the LORA_RECEIVED trigger event. |
510+
| | bytesRx | Number | yes | Number of bytes received from LoRa network so far. |
511+
| | dataTx | String | write | Use this channel to send a BASE64-encoded datagram to the LoRa network. |
512+
| | bytesTx | Number | yes | Number of bytes sent to the LoRa network so far |
513+
| | errorsTx | Number | yes | Number of failed transmissions to the LoRa network |
514+
| | snr | Number | yes | SNR (signal / noise ratio) of the received packet |
515+
| | rssi | Number | yes | LoRa signal strength (0-4, 0 being worst and 4 being best strength) |
516+
| | airtime | Number | yes | Time in milliseconds of the transmission time in the last 60 minutes |
517+
498518
### Shelly 1 (thing-type: shelly1)
499519

500520
| Group | Channel | Type | read-only | Description |

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,17 @@ public class ShellyBindingConstants {
257257
public static final String CHANNEL_STATUS_EVENTCOUNT1 = CHANNEL_STATUS_EVENTCOUNT + "1";
258258
public static final String CHANNEL_STATUS_EVENTCOUNT2 = CHANNEL_STATUS_EVENTCOUNT + "2";
259259

260+
// LoRa
261+
public static final String CHANNEL_GROUP_LORA = "lora";
262+
public static final String CHANNEL_LORA_RXDATA = "dataRx";
263+
public static final String CHANNEL_LORA_TXDATA = "dataTx";
264+
public static final String CHANNEL_LORA_RXBYTES = "bytesRx";
265+
public static final String CHANNEL_LORA_TXBYTES = "bytesTx";
266+
public static final String CHANNEL_LORA_TXERRORS = "errorsTx";
267+
public static final String CHANNEL_LORA_RSSI = "rssi";
268+
public static final String CHANNEL_LORA_SNR = "snr";
269+
public static final String CHANNEL_LORA_AIRTIME = "airtime";
270+
260271
// General
261272
public static final String CHANNEL_LAST_UPDATE = "lastUpdate";
262273
public static final String CHANNEL_EVENT_TRIGGER = "event";

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,9 @@ public static class ShellySettingsGlobal {
716716
// Gen2
717717
public Boolean ethernet;
718718
public Boolean bluetooth;
719+
720+
public Boolean loraDetected = false;
721+
public Boolean loraRxEnabled = false;
719722
}
720723

721724
public static class ShellySettingsAttributes {

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiClient.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
6464
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.ShellyDeviceConfigCB;
6565
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusLight;
66+
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusLora;
6667
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
6768
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2CoverStatus;
6869
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusEm;
@@ -235,6 +236,7 @@ protected boolean fillDeviceStatus(ShellySettingsStatus status, Shelly2DeviceSta
235236
updated |= updateRollerStatus(0, status, result.cover0, channelUpdate);
236237
updated |= updateDimmerStatus(0, status, result.light0, channelUpdate);
237238
updated |= updateRGBWStatus(0, status, result.rgbw0, channelUpdate);
239+
updated |= updateLoraStatus(0, status, result.lora100, channelUpdate);
238240
if (channelUpdate) {
239241
updated |= ShellyComponents.updateMeters(getThing(), status);
240242
}
@@ -790,6 +792,18 @@ private boolean updateRGBWStatus(int id, ShellySettingsStatus status, @Nullable
790792
return channelUpdate ? ShellyComponents.updateRGBW(getThing(), status) : false;
791793
}
792794

795+
private boolean updateLoraStatus(int id, ShellySettingsStatus status, @Nullable Shelly2DeviceStatusLora value,
796+
boolean channelUpdate) throws ShellyApiException {
797+
if (value == null) {
798+
return false;
799+
}
800+
if (value.id == null) {
801+
value.id = id;
802+
}
803+
// return channelUpdate ? ShellyComponents.updateLoraStatus(getThing(), value) : false;
804+
return getProfile().settings.loraDetected ? ShellyComponents.updateLoraStatus(getThing(), value) : false;
805+
}
806+
793807
protected @Nullable Integer getDuration(@Nullable Double timerStartedAt, @Nullable Double timerDuration) {
794808
if (timerStartedAt == null || timerDuration == null) {
795809
return null;

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ public class Shelly2ApiJsonDTO {
151151
public static final String SHELLY2_EVENT_BLUSCAN = SHELLY2_EVENT_BLUPREFIX + "scan_result";
152152
public static final String SHELLY2_EVENT_BLUDATA = SHELLY2_EVENT_BLUPREFIX + "data";
153153

154+
// LoRa
155+
public static final String SHELLY2_LORA_GWSCRIPT = "oh-lora.js";
156+
public static final String SHELLY2_EVENT_LORADATA = "oh-lora.data";
157+
154158
// Error Codes
155159
public static final String SHELLY2_ERROR_OVERPOWER = "overpower";
156160
public static final String SHELLY2_ERROR_OVERTEMP = "overtemp";
@@ -453,6 +457,18 @@ public class Shelly2DeviceConfigLed {
453457
public String powerLed;
454458
}
455459

460+
public class Shelly2DeviceConfigLora {
461+
public Integer id;
462+
public Long freq;
463+
public Integer bw;
464+
public Integer dr;
465+
public Integer cr;
466+
public Integer plen;
467+
public Integer txp;
468+
@SerializedName("rx_enable")
469+
public Boolean rxEnabled;
470+
}
471+
456472
public static class Shelly2GetConfigResult {
457473

458474
public class Shelly2DevConfigCloud {
@@ -530,6 +546,9 @@ public class Shelly2DevConfigMqtt {
530546

531547
@SerializedName("smoke:0")
532548
public Shelly2ConfigSmoke smoke0;
549+
550+
@SerializedName("lora:100")
551+
public Shelly2DeviceConfigLora lora100;
533552
}
534553

535554
public class Shelly2DeviceConfigSta {
@@ -612,6 +631,19 @@ public static class Shelly2DeviceStatusLight {
612631
public Double timerDuration;
613632
}
614633

634+
public static class Shelly2DeviceStatusLora {
635+
public Integer id;
636+
@SerializedName("bytes_recd")
637+
public Long rxBytes;
638+
@SerializedName("bytes_sent")
639+
public Long txBytes;
640+
@SerializedName("send_fails")
641+
public Long txErrors;
642+
@SerializedName("air_time_hr_ms")
643+
public Long airtime;
644+
public String fw;
645+
}
646+
615647
public static class Shelly2DeviceStatusResult {
616648
public class Shelly2DeviceStatusBle {
617649

@@ -863,6 +895,9 @@ public static class Shelly2RGBWStatus {
863895

864896
@SerializedName("devicepower:0")
865897
public Shelly2DeviceStatusPower devicepower0;
898+
899+
@SerializedName("lora:100")
900+
public Shelly2DeviceStatusLora lora100;
866901
}
867902

868903
public class Shelly2DeviceStatusSys {
@@ -1213,8 +1248,8 @@ public class Shelly2NotifyEventMessage {
12131248
public Integer pid;
12141249
@SerializedName("Battery")
12151250
public Integer battery;
1216-
@SerializedName("Button")
1217-
public Integer buttonEvent;
1251+
@SerializedName("Buttons")
1252+
public Integer[] buttonEvents;
12181253
@SerializedName("Illuminance")
12191254
public Integer illuminance;
12201255
@SerializedName("Window")
@@ -1228,14 +1263,29 @@ public class Shelly2NotifyEventMessage {
12281263
@SerializedName("Humidity")
12291264
public Double humidity;
12301265

1266+
public ShellyNotifyLoraEvent info;
1267+
12311268
public Integer rssi;
12321269
public Integer tx_power;
12331270
}
12341271

1272+
// {"component":"script:1","id":1,"event":"oh-lora.data","data":{"component":"lora:100","name":"lora","id":100,"now":1752923898.77490592002,"info":{"component":"lora:100","id":100,"event":"lora_received","ts":1752923898.76999998092,"data":"MDEyMzQ1Njc4OQ==","rssi":-46,"snr":8,"tsu":50111400560}},"ts":1752923898.81}
1273+
public static class ShellyNotifyLoraEvent {
1274+
public String component;
1275+
public Integer id;
1276+
public String event;
1277+
public Double ts;
1278+
public String data;
1279+
public Integer rssi;
1280+
public Integer snr;
1281+
public Long tsu;
1282+
}
1283+
12351284
public class Shelly2NotifyEvent {
12361285
public Integer id;
12371286
public Double ts;
12381287
public String component;
1288+
public String name;
12391289
public String event;
12401290
public Shelly2NotifyEventMessage data;
12411291
public String msg;

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiRpc.java

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ public void startScan() {
161161
if (getProfile().isBlu) {
162162
installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway);
163163
}
164+
165+
if (config.enableLoRa) {
166+
installScript(SHELLY2_LORA_GWSCRIPT, true);
167+
}
164168
} catch (ShellyApiException e) {
165169
}
166170
}
@@ -332,6 +336,11 @@ public ShellyDeviceProfile getDeviceProfile(ThingTypeUID thingTypeUID, @Nullable
332336
profile.settings.ledPowerDisable = "off".equals(getString(dc.led.powerLed));
333337
}
334338

339+
if (dc.lora100 != null && config.enableLoRa) {
340+
profile.settings.loraDetected = true;
341+
profile.settings.loraRxEnabled = dc.lora100.rxEnabled;
342+
}
343+
335344
profile.initialized = true;
336345
if (!discovery) {
337346
getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
@@ -365,6 +374,10 @@ public ShellyDeviceProfile getDeviceProfile(ThingTypeUID thingTypeUID, @Nullable
365374
getThing().reinitializeThing();
366375
}
367376
}
377+
378+
if (config.enableLoRa) {
379+
installScript(SHELLY2_LORA_GWSCRIPT, true);
380+
}
368381
}
369382
} catch (ShellyApiException e) {
370383
logger.debug("{}: Device config failed", thingName, e);
@@ -488,13 +501,24 @@ protected void installScript(String script, boolean install) throws ShellyApiExc
488501
if (upload) {
489502
logger.debug("{}: Script will be installed...", thingName);
490503

491-
// Create new script, get id
492-
ShellyScriptResponse rsp = apiRequest(
493-
new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_CREATE).withName(script),
494-
ShellyScriptResponse.class);
495-
ourId = rsp.id;
496-
logger.debug("{}: Script has been created, id={}", thingName, ourId);
497-
upload = true;
504+
if (ourId == -1) {
505+
// find free script id
506+
ourId = 0;
507+
while (++ourId < 10 && testScriptId(scriptList, ourId)) {
508+
}
509+
}
510+
if (ourId < 10) {
511+
// Create new script, get id
512+
ShellyScriptResponse rsp = apiRequest(new Shelly2RpcRequest()
513+
.withMethod(SHELLYRPC_METHOD_SCRIPT_CREATE).withId(ourId).withName(script),
514+
ShellyScriptResponse.class);
515+
ourId = rsp.id;
516+
logger.debug("{}: Script has been created, id={}", thingName, ourId);
517+
upload = true;
518+
} else {
519+
logger.debug("{}: Too many scripts installed on this device", thingName);
520+
upload = false;
521+
}
498522
}
499523

500524
if (upload) {
@@ -534,6 +558,15 @@ protected void installScript(String script, boolean install) throws ShellyApiExc
534558
}
535559
}
536560

561+
private boolean testScriptId(ShellyScriptListResponse scriptList, int id) {
562+
for (ShellyScriptListEntry s : scriptList.scripts) {
563+
if (s.id == id) {
564+
return true;
565+
}
566+
}
567+
return false;
568+
}
569+
537570
private boolean startScript(int ourId, boolean start) {
538571
if (ourId != -1) {
539572
try {
@@ -734,6 +767,16 @@ public void onNotifyEvent(Shelly2RpcNotifyEvent message) {
734767
logger.debug("{}: WiFi disconnected, reason {}", thingName, getInteger(e.reason));
735768
getThing().postEvent(e.event, false);
736769
break;
770+
771+
case SHELLY2_EVENT_LORADATA:
772+
logger.debug("{}: LoRa data received, payload = {}", thingName, e.data.info);
773+
updateChannel(CHANNEL_GROUP_LORA, CHANNEL_LORA_RXDATA, getStringType(e.data.info.data));
774+
updateChannel(CHANNEL_GROUP_LORA, CHANNEL_LORA_RSSI, getDecimal(e.data.info.rssi));
775+
updateChannel(CHANNEL_GROUP_LORA, CHANNEL_LORA_SNR, getDecimal(e.data.info.snr));
776+
777+
getThing().postEvent(e.data.info.event, false);
778+
break;
779+
737780
default:
738781
logger.debug("{}: Event {} was not handled", thingName, e.event);
739782
}

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/config/ShellyThingConfiguration.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,15 @@ public class ShellyThingConfiguration {
4646

4747
public Boolean enableBluGateway = false;
4848
public Boolean enableRangeExtender = true;
49+
public Boolean enableLoRa = false;
4950

5051
@Override
5152
public String toString() {
5253
return "Device address=" + deviceAddress + ", HTTP user/password=" + userId + "/"
5354
+ (password.isEmpty() ? "<none>" : "***") + ", update interval=" + updateInterval + "\n"
5455
+ "Events: Button: " + eventsButton + ", Switch (on/off): " + eventsSwitch + ", Push: " + eventsPush
5556
+ ", Roller: " + eventsRoller + "Sensor: " + eventsSensorReport + ", CoIoT: " + eventsCoIoT + "\n"
56-
+ "Blu Gateway=" + enableBluGateway + ", Range Extender: " + enableRangeExtender;
57+
+ "Blu Gateway=" + enableBluGateway + ", Range Extender: " + enableRangeExtender + ", LoRa support"
58+
+ enableLoRa;
5759
}
5860
}

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyComponents.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyADC;
3636
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtTemperature.ShellyShortTemp;
3737
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyThermnostat;
38+
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusLora;
3839
import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
3940
import org.openhab.core.library.types.OnOffType;
4041
import org.openhab.core.library.types.StringType;
@@ -69,6 +70,8 @@ public static boolean updateDeviceStatus(ShellyThingInterface thingHandler, Shel
6970
if (!thingHandler.areChannelsCreated()) {
7071
thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createDeviceChannels(thingHandler.getThing(),
7172
thingHandler.getProfile(), status));
73+
thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createLoraChannels(thingHandler.getThing(),
74+
thingHandler.getProfile(), status));
7275
}
7376

7477
if (getLong(status.uptime) > 10) {
@@ -654,6 +657,20 @@ public static boolean updateDimmers(ShellyThingInterface thingHandler, ShellySet
654657
return updated;
655658
}
656659

660+
public static boolean updateLoraStatus(ShellyThingInterface thingHandler, Shelly2DeviceStatusLora status) {
661+
boolean updated = false;
662+
ShellyDeviceProfile profile = thingHandler.getProfile();
663+
if (profile.settings.loraDetected) {
664+
updated |= thingHandler.updateChannel(CHANNEL_GROUP_LORA, CHANNEL_LORA_RXBYTES, getDecimal(status.rxBytes));
665+
updated |= thingHandler.updateChannel(CHANNEL_GROUP_LORA, CHANNEL_LORA_TXBYTES, getDecimal(status.txBytes));
666+
updated |= thingHandler.updateChannel(CHANNEL_GROUP_LORA, CHANNEL_LORA_TXERRORS,
667+
getDecimal(status.txErrors));
668+
updated |= thingHandler.updateChannel(CHANNEL_GROUP_LORA, CHANNEL_LORA_AIRTIME, getDecimal(status.airtime));
669+
}
670+
671+
return updated;
672+
}
673+
657674
public static boolean updateTempChannel(@Nullable ShellyShortTemp sensor, ShellyThingInterface thingHandler,
658675
String channel) {
659676
return sensor != null ? updateTempChannel(thingHandler, CHANNEL_GROUP_SENSOR, channel, sensor.tC, "") : false;

0 commit comments

Comments
 (0)