[shelly] Fix thing re-init after power cycle for firmware update (#17163)

* Fixes thing re-init during/after firmware update. API will not be closed
on ota_success message.
* Don't disconnect from device during firmware update, this ensures to get
further events throughout the upgrade process
* suspress COMMUNICATION_ERROR on expected restart after fw upgrade

Signed-off-by: Markus Michels <markus7017@gmail.com>
This commit is contained in:
Markus Michels 2024-08-02 23:14:38 +02:00 committed by Jacob Laursen
parent e5148607bf
commit 353315537f
10 changed files with 67 additions and 41 deletions

View File

@ -285,14 +285,19 @@ Values 1-4 are selecting the corresponding favorite id in the Shelly App, 0 mean
The binding sets the following Thing status depending on the device status: The binding sets the following Thing status depending on the device status:
| Status | Description | | Status | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| INITIALIZING | This is the default status while initializing the Thing. Once the initialization is triggered the Thing switches to Status UNKNOWN. | | INITIALIZING | This is the default status while initializing the Thing. Once the initialization is triggered the Thing switches to Status ONLINE.CONFIGURATION_PENDING. |
| UNKNOWN | Indicates that the status is currently unknown, which must not show a problem. Usually the Thing stays in this status when the device is in sleep mode. Once the device is reachable and was initialized the Thing switches to status ONLINE. | | UNKNOWN | Indicates that the status is currently unknown, which must not show a problem. Once the device is reachable and was initialized the Thing switches to status ONLINE. |
| ONLINE | ONLINE indicates that the device can be accessed and is responding properly. Battery powered devices also stay ONLINE when in sleep mode. The binding has an integrated watchdog timer supervising the device, see below. The Thing switches to status OFFLINE when some type of communication error occurs. | | CONFIGURATION_PENDING | The Thing has been initialized, but device initialization is in progress or pending (e.g. waiting for device wake-up). |
| OFFLINE | Communication with the device failed. Check the Thing status in the UI and openHAB's log for an indication of the error. Try restarting OH or deleting and re-discovering the Thing. You could also post to the community thread if the problem persists. | | ONLINE | ONLINE indicates that the device can be accessed and is responding properly. Once initialized battery powered devices also stay ONLINE when in sleep mode. The binding has an integrated watchdog timer supervising the device, see below. The Thing switches to status OFFLINE when some type of communication error occurs. |
| CONFIG PENDING | The thing has been initialized, but device initialization is in progress or pending (e.g. waiting for device wake-up) | | OFFLINE | Communication with the device failed. Check the Thing status in the UI and openHAB's log for an indication of the error. |
| ERROR: COMM | Communication with the device has reported an error, check detailed status. | | COMMUNICATION_ERROR | Communication with the device has reported an error, check detailed status. If the problem persists make sure to have stable WiFi, set the correct password etc. Try restarting OH or deleting and re-discovering the Thing. |
| FIRMWARE_UPDATING | Device firmware is updating, just wait. The device should come back to ONLINE within 2 minutes. |
| DUTY_CYCLE | The device is re-initializing and reported a restart event, e.g. after a firmware update or manual reboot. |
`Note:`
For more details see [Thing Concept](https://www.openhab.org/docs/concepts/things.html#status-details) in openHAB documentation.
`Battery powered devices:` `Battery powered devices:`
If the device is in sleep mode and can't be reached by the binding, the Thing will change into CONFIG_PENDING. If the device is in sleep mode and can't be reached by the binding, the Thing will change into CONFIG_PENDING.

View File

@ -135,6 +135,7 @@ public class Shelly2ApiJsonDTO {
public static final String SHELLY2_EVENT_OTASTART = "ota_begin"; public static final String SHELLY2_EVENT_OTASTART = "ota_begin";
public static final String SHELLY2_EVENT_OTAPROGRESS = "ota_progress"; public static final String SHELLY2_EVENT_OTAPROGRESS = "ota_progress";
public static final String SHELLY2_EVENT_OTADONE = "ota_success"; public static final String SHELLY2_EVENT_OTADONE = "ota_success";
public static final String SHELLY2_EVENT_RESTART = "scheduled_restart";
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";

View File

@ -85,6 +85,7 @@ 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.openhab.binding.shelly.internal.util.ShellyVersionDTO; import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -171,7 +172,6 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
} }
Shelly2GetConfigResult dc = apiRequest(SHELLYRPC_METHOD_GETCONFIG, null, Shelly2GetConfigResult.class); Shelly2GetConfigResult dc = apiRequest(SHELLYRPC_METHOD_GETCONFIG, null, Shelly2GetConfigResult.class);
profile.isGen2 = true;
profile.settingsJson = gson.toJson(dc); profile.settingsJson = gson.toJson(dc);
profile.thingName = thingName; profile.thingName = thingName;
profile.settings.name = profile.status.name = dc.sys.device.name; profile.settings.name = profile.status.name = dc.sys.device.name;
@ -684,19 +684,22 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
case SHELLY2_EVENT_OTASTART: case SHELLY2_EVENT_OTASTART:
logger.debug("{}: Firmware update started: {}", thingName, getString(e.msg)); logger.debug("{}: Firmware update started: {}", thingName, getString(e.msg));
getThing().postEvent(e.event, true); getThing().setThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.FIRMWARE_UPDATING,
getThing().setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING,
"offline.status-error-fwupgrade"); "offline.status-error-fwupgrade");
break; break;
case SHELLY2_EVENT_OTAPROGRESS: case SHELLY2_EVENT_OTAPROGRESS:
logger.debug("{}: Firmware update in progress: {}", thingName, getString(e.msg)); logger.debug("{}: Firmware update in progress: {}", thingName, getString(e.msg));
getThing().postEvent(e.event, false);
break; break;
case SHELLY2_EVENT_OTADONE: case SHELLY2_EVENT_OTADONE:
logger.debug("{}: Firmware update completed: {}", thingName, getString(e.msg)); logger.debug("{}: Firmware update completed with status {}", thingName, getString(e.msg));
getThing().setThingOffline(ThingStatusDetail.CONFIGURATION_PENDING, getThing().setThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE,
"message.offline.status-error-fwcompleted");
break;
case SHELLY2_EVENT_RESTART:
logger.debug("{}: Device was restarted: {}", thingName, getString(e.msg));
getThing().setThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE,
"offline.status-error-restarted"); "offline.status-error-restarted");
getThing().requestUpdates(1, true); // refresh config getThing().postEvent(ALARM_TYPE_RESTARTED, true);
break; break;
case SHELLY2_EVENT_SLEEP: case SHELLY2_EVENT_SLEEP:
logger.debug("{}: Connection terminated, e.g. device in sleep mode", thingName); logger.debug("{}: Connection terminated, e.g. device in sleep mode", thingName);
@ -732,10 +735,12 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
String reason = getString(description); String reason = getString(description);
logger.debug("{}: WebSocket connection closed, status = {}/{}", thingName, statusCode, reason); logger.debug("{}: WebSocket connection closed, status = {}/{}", thingName, statusCode, reason);
if ("Bye".equalsIgnoreCase(reason)) { if ("Bye".equalsIgnoreCase(reason)) {
logger.debug("{}: Device went to sleep mode", thingName); logger.debug("{}: Device went to sleep mode or was restarted", thingName);
} else if (statusCode == StatusCode.ABNORMAL && !discovery && getProfile().alwaysOn) { } else if (statusCode == StatusCode.ABNORMAL && !discovery && getProfile().alwaysOn) {
// e.g. device rebooted // e.g. device rebooted
thingOffline("WebSocket connection closed abnormal"); if (getThing().getThingStatusDetail() != ThingStatusDetail.DUTY_CYCLE) {
thingOffline("WebSocket connection closed abnormally");
}
} }
} catch (ShellyApiException e) { } catch (ShellyApiException e) {
logger.debug("{}: Exception on onClose()", thingName, e); logger.debug("{}: Exception on onClose()", thingName, e);
@ -753,8 +758,8 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
private void thingOffline(String reason) { private void thingOffline(String reason) {
if (thing != null) { // do not reinit of battery powered devices with sleep mode if (thing != null) { // do not reinit of battery powered devices with sleep mode
thing.setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "offline.status-error-unexpected-error", thing.setThingOfflineAndDisconnect(ThingStatusDetail.COMMUNICATION_ERROR,
reason); "offline.status-error-unexpected-error", reason);
} }
} }

View File

@ -231,7 +231,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
} }
if (!status.isEmpty()) { if (!status.isEmpty()) {
setThingOffline(errorCode, status, e.toString()); setThingOfflineAndDisconnect(errorCode, status, e.toString());
} else { } else {
logger.debug("{}: Unable to initialize: {}, retrying later", thingName, e.toString()); logger.debug("{}: Unable to initialize: {}, retrying later", thingName, e.toString());
} }
@ -299,13 +299,17 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
thingName, getThing().getLabel(), thingType, config.deviceAddress.toUpperCase(), gen2, profile.isBlu, thingName, getThing().getLabel(), thingType, config.deviceAddress.toUpperCase(), gen2, profile.isBlu,
profile.alwaysOn, profile.hasBattery, config.eventsCoIoT); profile.alwaysOn, profile.hasBattery, config.eventsCoIoT);
if (config.deviceAddress.isEmpty()) { if (config.deviceAddress.isEmpty()) {
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "config-status.error.missing-device-address"); setThingOfflineAndDisconnect(ThingStatusDetail.CONFIGURATION_ERROR,
"config-status.error.missing-device-address");
return false; return false;
} }
if (profile.alwaysOn || !profile.isInitialized()) { if (profile.alwaysOn || !profile.isInitialized()) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, ThingStatusDetail detail = getThingStatusDetail();
messages.get("status.unknown.initializing")); if (detail != ThingStatusDetail.DUTY_CYCLE) {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
messages.get("status.config_pending"));
}
} }
// 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
@ -319,7 +323,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
api.initialize(); api.initialize();
ShellySettingsDevice device = profile.device = api.getDeviceInfo(); ShellySettingsDevice device = profile.device = api.getDeviceInfo();
if (getBool(device.auth) && config.password.isEmpty()) { if (getBool(device.auth) && config.password.isEmpty()) {
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-no-credentials"); setThingOfflineAndDisconnect(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-no-credentials");
return false; return false;
} }
if (config.serviceName.isEmpty()) { if (config.serviceName.isEmpty()) {
@ -336,7 +340,8 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
// Validate device mode // Validate device mode
String reqMode = thingType.contains("-") ? substringAfter(thingType, "-") : ""; String reqMode = thingType.contains("-") ? substringAfter(thingType, "-") : "";
if (!reqMode.isEmpty() && !mode.equals(reqMode)) { if (!reqMode.isEmpty() && !mode.equals(reqMode)) {
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-wrong-mode", mode, reqMode); setThingOfflineAndDisconnect(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-wrong-mode", mode,
reqMode);
return false; return false;
} }
if (!getString(tmpPrf.device.coiot).isEmpty()) { if (!getString(tmpPrf.device.coiot).isEmpty()) {
@ -543,7 +548,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
ThingStatus thingStatus = getThing().getStatus(); ThingStatus thingStatus = getThing().getStatus();
if (refreshSettings || (scheduledUpdates > 0) || (skipUpdate % skipCount == 0)) { if (refreshSettings || (scheduledUpdates > 0) || (skipUpdate % skipCount == 0)) {
if (!profile.isInitialized() || ((thingStatus == ThingStatus.OFFLINE)) if (!profile.isInitialized() || ((thingStatus == ThingStatus.OFFLINE))
|| (thingStatus == ThingStatus.UNKNOWN)) { || (getThingStatusDetail() == ThingStatusDetail.CONFIGURATION_PENDING)) {
logger.debug("{}: Status update triggered thing initialization", thingName); logger.debug("{}: Status update triggered thing initialization", thingName);
initializeThing(); // may fire an exception if initialization failed initializeThing(); // may fire an exception if initialization failed
} }
@ -671,7 +676,8 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
@Override @Override
public boolean isThingOnline() { public boolean isThingOnline() {
return getThingStatus() == ThingStatus.ONLINE; return getThingStatus() == ThingStatus.ONLINE
&& getThingStatusDetail() != ThingStatusDetail.CONFIGURATION_PENDING;
} }
public boolean isThingOffline() { public boolean isThingOffline() {
@ -692,13 +698,18 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
} }
@Override @Override
public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments) { public void setThingOfflineAndDisconnect(ThingStatusDetail detail, String messageKey, Object... arguments) {
if (!isThingOffline()) { if (!isThingOffline()) {
updateStatus(ThingStatus.OFFLINE, detail, messages.get(messageKey, arguments)); updateStatus(ThingStatus.OFFLINE, detail, messages.get(messageKey, arguments));
api.close(); // Gen2: disconnect WS/close http sessions
watchdog = 0;
channelsCreated = false; // check for new channels after devices gets re-initialized (e.g. new
} }
api.close(); // Gen2: disconnect WS/close http sessions
watchdog = 0;
channelsCreated = false; // check for new channels after devices gets re-initialized (e.g. new
}
@Override
public void setThingStatus(ThingStatus status, ThingStatusDetail detail, String messageKey, Object... arguments) {
updateStatus(status, detail, messages.get(messageKey, arguments));
} }
@Override @Override
@ -724,7 +735,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
logger.debug("{}: Handler is shutting down, ignore", thingName); logger.debug("{}: Handler is shutting down, ignore", thingName);
return; return;
} }
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
messages.get("offline.status-error-restarted")); messages.get("offline.status-error-restarted"));
requestUpdates(0, true); requestUpdates(0, true);
} }
@ -1025,7 +1036,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
config.localIp = bindingConfig.localIP; config.localIp = bindingConfig.localIP;
config.localPort = String.valueOf(bindingConfig.httpPort); config.localPort = String.valueOf(bindingConfig.httpPort);
if (config.localIp.startsWith("169.254")) { if (config.localIp.startsWith("169.254")) {
setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "config-status.error.network-config", setThingOfflineAndDisconnect(ThingStatusDetail.COMMUNICATION_ERROR, "config-status.error.network-config",
config.localIp); config.localIp);
return false; return false;
} }
@ -1159,7 +1170,8 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
changeThingType(thingTypeUID, getConfig()); changeThingType(thingTypeUID, getConfig());
} else { } else {
logger.debug("{}: to {}", thingName, thingType); logger.debug("{}: to {}", thingName, thingType);
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Unable to change thing type to " + thingType); setThingOfflineAndDisconnect(ThingStatusDetail.CONFIGURATION_ERROR,
"Unable to change thing type to " + thingType);
} }
} }

View File

@ -46,7 +46,7 @@ public interface ShellyManagerInterface {
public void setThingOnline(); public void setThingOnline();
public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments); public void setThingOfflineAndDisconnect(ThingStatusDetail detail, String messageKey, Object... arguments);
public boolean requestUpdates(int requestCount, boolean refreshSettings); public boolean requestUpdates(int requestCount, boolean refreshSettings);

View File

@ -52,7 +52,9 @@ public interface ShellyThingInterface {
void setThingOnline(); void setThingOnline();
void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments); void setThingOfflineAndDisconnect(ThingStatusDetail detail, String messageKey, Object... arguments);
void setThingStatus(ThingStatus status, ThingStatusDetail detail, String messageKey, Object... arguments);
boolean isStopping(); boolean isStopping();

View File

@ -442,7 +442,7 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
} }
private void setRestarted(ShellyManagerInterface th, String uid) { private void setRestarted(ShellyManagerInterface th, String uid) {
th.setThingOffline(ThingStatusDetail.GONE, "offline.status-error-restarted"); th.setThingOfflineAndDisconnect(ThingStatusDetail.GONE, "offline.status-error-restarted");
scheduleUpdate(th, uid + "_upgrade", 25); // wait 25s before refresh scheduleUpdate(th, uid + "_upgrade", 25); // wait 25s before refresh
} }
} }

View File

@ -118,7 +118,7 @@ public class ShellyManagerOtaPage extends ShellyManagerPage {
if ("yes".equalsIgnoreCase(update)) { if ("yes".equalsIgnoreCase(update)) {
// do the update // do the update
th.setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING, "offline.status-error-fwupgrade"); th.setThingOfflineAndDisconnect(ThingStatusDetail.FIRMWARE_UPDATING, "offline.status-error-fwupgrade");
html += loadHTML(FWUPDATE2_HTML, properties); html += loadHTML(FWUPDATE2_HTML, properties);
new Thread(() -> { // schedule asynchronous reboot new Thread(() -> { // schedule asynchronous reboot

View File

@ -263,7 +263,7 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class); ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
TreeMap<String, String> result = new TreeMap<>(); TreeMap<String, String> result = new TreeMap<>();
if ((status != ThingStatus.ONLINE) && (status != ThingStatus.UNKNOWN)) { if (status != ThingStatus.ONLINE && status != ThingStatus.UNKNOWN) {
result.put("Thing Status", status.toString()); result.put("Thing Status", status.toString());
} }
State wifiSignal = handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI); State wifiSignal = handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI);

View File

@ -25,7 +25,8 @@ message.offline.status-error-unexpected-error = Unexpected error: {0}
message.offline.status-error-unexpected-api-result = An unexpected API response. Please verify the logfile to get more detailed information. message.offline.status-error-unexpected-api-result = An unexpected API response. Please verify the logfile to get more detailed information.
message.offline.status-error-watchdog = Device is not responding, seems to be unavailable. message.offline.status-error-watchdog = Device is not responding, seems to be unavailable.
message.offline.status-error-restarted = The device has restarted and will be re-initialized. message.offline.status-error-restarted = The device has restarted and will be re-initialized.
message.offline.status-error-fwupgrade = Firmware upgrade in progress message.offline.status-error-fwupgrade = Firmware upgrade in progress.
message.offline.status-error-fwcompleted = Firmware upgrade completed, device is restarting.
# General messages # General messages
message.versioncheck.failed = Unable to check firmware version: {0} message.versioncheck.failed = Unable to check firmware version: {0}
@ -36,7 +37,7 @@ message.versioncheck.autocoiot = INFO: Firmware is full-filling the minimum vers
message.init.noipaddress = Unable to detect local IP address. Please make sure that IPv4 is enabled for this interface and check openHAB Network Configuration. message.init.noipaddress = Unable to detect local IP address. Please make sure that IPv4 is enabled for this interface and check openHAB Network Configuration.
message.command.failed = ERROR: Unable to process command {0} for channel {1} message.command.failed = ERROR: Unable to process command {0} for channel {1}
message.command.init = Thing not yet initialized, command {0} triggered initialization message.command.init = Thing not yet initialized, command {0} triggered initialization
message.status.unknown.initializing = Initializing or device in sleep mode. message.status.config_pending = Device is initializing or in sleep mode.
message.statusupdate.failed = Unable to update status message.statusupdate.failed = Unable to update status
message.status.managerstarted = Shelly Manager started at http(s)://{0}:{1}/shelly/manager message.status.managerstarted = Shelly Manager started at http(s)://{0}:{1}/shelly/manager
message.event.triggered = Event triggered: {0} message.event.triggered = Event triggered: {0}