]> git.basschouten.com Git - openhab-addons.git/blob
a1d9f69f5cfbbf8f466ee3b81750a8e004d93188
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.shelly.internal.api2;
14
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
16 import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
17 import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
18 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
19
20 import java.io.BufferedReader;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.InputStreamReader;
24 import java.io.UncheckedIOException;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.stream.Collectors;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.http.HttpStatus;
34 import org.eclipse.jetty.websocket.api.StatusCode;
35 import org.openhab.binding.shelly.internal.api.ShellyApiException;
36 import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
37 import org.openhab.binding.shelly.internal.api.ShellyApiResult;
38 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
39 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
40 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
41 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
42 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorSleepMode;
43 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
44 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
45 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
46 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
47 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
48 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
49 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
50 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsWiFiNetwork;
51 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
52 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
53 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
54 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
55 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
56 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthResponse;
57 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2ConfigParms;
58 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DeviceConfigSta;
59 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
60 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp;
61 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp.Shelly2DeviceConfigApRE;
62 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceSettings;
63 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
64 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusSys.Shelly2DeviceStatusSysAvlUpdate;
65 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
66 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
67 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
68 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
69 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus.Shelly2NotifyStatus;
70 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest;
71 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest.Shelly2RpcRequestParams;
72 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse;
73 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse.Shelly2WsConfigResult;
74 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptListResponse;
75 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptListResponse.ShellyScriptListEntry;
76 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptPutCodeParams;
77 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptResponse;
78 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
79 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
80 import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
81 import org.openhab.core.library.unit.SIUnits;
82 import org.openhab.core.thing.ThingStatusDetail;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
85
86 /**
87  * {@link Shelly2ApiRpc} implements Gen2 RPC interface
88  *
89  * @author Markus Michels - Initial contribution
90  */
91 @NonNullByDefault
92 public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterface, Shelly2RpctInterface {
93     private final Logger logger = LoggerFactory.getLogger(Shelly2ApiRpc.class);
94     private final @Nullable ShellyThingTable thingTable;
95
96     protected boolean initialized = false;
97     private boolean discovery = false;
98     private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
99     private Shelly2AuthResponse authInfo = new Shelly2AuthResponse();
100
101     /**
102      * Regular constructor - called by Thing handler
103      *
104      * @param thingName Symbolic thing name
105      * @param thing Thing Handler (ThingHandlerInterface)
106      */
107     public Shelly2ApiRpc(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
108         super(thingName, thing);
109         this.thingName = thingName;
110         this.thing = thing;
111         this.thingTable = thingTable;
112         try {
113             getProfile().initFromThingType(thing.getThingType());
114         } catch (ShellyApiException e) {
115             logger.info("{}: Shelly2 API initialization failed!", thingName, e);
116         }
117     }
118
119     /**
120      * Simple initialization - called by discovery handler
121      *
122      * @param thingName Symbolic thing name
123      * @param config Thing Configuration
124      * @param httpClient HTTP Client to be passed to ShellyHttpClient
125      */
126     public Shelly2ApiRpc(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
127         super(thingName, config, httpClient);
128         this.thingName = thingName;
129         this.thingTable = null;
130         this.discovery = true;
131     }
132
133     @Override
134     public void initialize() throws ShellyApiException {
135         if (!initialized) {
136             rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp);
137             rpcSocket.addMessageHandler(this);
138             initialized = true;
139         } else {
140             if (rpcSocket.isConnected()) {
141                 logger.debug("{}: Disconnect Rpc Socket on initialize", thingName);
142                 disconnect();
143             }
144         }
145     }
146
147     @Override
148     public boolean isInitialized() {
149         return initialized;
150     }
151
152     @Override
153     public void startScan() {
154         if (config.enableBluGateway) {
155             try {
156                 installScript(SHELLY2_BLU_GWSCRIPT);
157             } catch (ShellyApiException e) {
158             }
159         }
160     }
161
162     @SuppressWarnings("null")
163     @Override
164     public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
165         ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
166
167         Shelly2GetConfigResult dc = apiRequest(SHELLYRPC_METHOD_GETCONFIG, null, Shelly2GetConfigResult.class);
168         profile.isGen2 = true;
169         profile.settingsJson = gson.toJson(dc);
170         profile.thingName = thingName;
171         profile.settings.name = profile.status.name = dc.sys.device.name;
172         profile.name = getString(profile.settings.name);
173         profile.settings.timezone = getString(dc.sys.location.tz);
174         profile.settings.discoverable = getBool(dc.sys.device.discoverable);
175         if (dc.wifi != null && dc.wifi.ap != null && dc.wifi.ap.rangeExtender != null) {
176             profile.settings.wifiAp.rangeExtender = getBool(dc.wifi.ap.rangeExtender.enable);
177         }
178         if (dc.cloud != null) {
179             profile.settings.cloud.enabled = getBool(dc.cloud.enable);
180         }
181         if (dc.mqtt != null) {
182             profile.settings.mqtt.enable = getBool(dc.mqtt.enable);
183         }
184         if (dc.sys.sntp != null) {
185             profile.settings.sntp.server = dc.sys.sntp.server;
186         }
187
188         profile.isRoller = dc.cover0 != null;
189         profile.settings.relays = fillRelaySettings(profile, dc);
190         profile.settings.inputs = fillInputSettings(profile, dc);
191         profile.settings.rollers = fillRollerSettings(profile, dc);
192
193         profile.isEMeter = true;
194         profile.numInputs = profile.settings.inputs != null ? profile.settings.inputs.size() : 0;
195         profile.numRelays = profile.settings.relays != null ? profile.settings.relays.size() : 0;
196         profile.numRollers = profile.settings.rollers != null ? profile.settings.rollers.size() : 0;
197         profile.hasRelays = profile.numRelays > 0 || profile.numRollers > 0;
198         profile.mode = "";
199         if (profile.hasRelays) {
200             profile.mode = profile.isRoller ? SHELLY_CLASS_ROLLER : SHELLY_CLASS_RELAY;
201         }
202
203         ShellySettingsDevice device = getDeviceInfo();
204         profile.settings.device = device;
205         profile.hostname = device.hostname;
206         profile.deviceType = device.type;
207         profile.mac = device.mac;
208         profile.auth = device.auth;
209         if (config.serviceName.isEmpty()) {
210             config.serviceName = getString(profile.hostname);
211         }
212         profile.fwDate = substringBefore(device.fw, "/");
213         profile.fwVersion = substringBefore(ShellyDeviceProfile.extractFwVersion(device.fw.replace("/", "/v")), "-");
214         profile.status.update.oldVersion = profile.fwVersion;
215         profile.status.hasUpdate = profile.status.update.hasUpdate = false;
216
217         if (dc.eth != null) {
218             profile.settings.ethernet = getBool(dc.eth.enable);
219         }
220         if (dc.ble != null) {
221             profile.settings.bluetooth = getBool(dc.ble.enable);
222         }
223
224         profile.settings.wifiSta = new ShellySettingsWiFiNetwork();
225         profile.settings.wifiSta1 = new ShellySettingsWiFiNetwork();
226         fillWiFiSta(dc.wifi.sta, profile.settings.wifiSta);
227         fillWiFiSta(dc.wifi.sta1, profile.settings.wifiSta1);
228
229         profile.numMeters = 0;
230         if (profile.hasRelays) {
231             profile.status.relays = new ArrayList<>();
232             relayStatus.relays = new ArrayList<>();
233             profile.numMeters = profile.isRoller ? profile.numRollers : profile.numRelays;
234             for (int i = 0; i < profile.numRelays; i++) {
235                 profile.status.relays.add(new ShellySettingsRelay());
236                 relayStatus.relays.add(new ShellyShortStatusRelay());
237             }
238         }
239
240         if (profile.numInputs > 0) {
241             profile.status.inputs = new ArrayList<>();
242             relayStatus.inputs = new ArrayList<>();
243             for (int i = 0; i < profile.numInputs; i++) {
244                 ShellyInputState input = new ShellyInputState();
245                 input.input = 0;
246                 input.event = "";
247                 input.eventCount = 0;
248                 profile.status.inputs.add(input);
249                 relayStatus.inputs.add(input);
250             }
251         }
252
253         if (dc.em0 != null) {
254             profile.numMeters = 3;
255         }
256
257         if (profile.numMeters > 0) {
258             profile.status.meters = new ArrayList<>();
259             profile.status.emeters = new ArrayList<>();
260             relayStatus.meters = new ArrayList<>();
261
262             for (int i = 0; i < profile.numMeters; i++) {
263                 profile.status.meters.add(new ShellySettingsMeter());
264                 profile.status.emeters.add(new ShellySettingsEMeter());
265                 relayStatus.meters.add(new ShellySettingsMeter());
266             }
267         }
268
269         if (profile.isRoller) {
270             profile.status.rollers = new ArrayList<>();
271             for (int i = 0; i < profile.numRollers; i++) {
272                 ShellyRollerStatus rs = new ShellyRollerStatus();
273                 profile.status.rollers.add(rs);
274                 rollerStatus.add(rs);
275             }
276         }
277
278         profile.status.dimmers = profile.isDimmer ? new ArrayList<>() : null;
279         profile.status.lights = profile.isBulb ? new ArrayList<>() : null;
280         profile.status.thermostats = profile.isTRV ? new ArrayList<>() : null;
281
282         if (profile.hasBattery) {
283             profile.settings.sleepMode = new ShellySensorSleepMode();
284             profile.settings.sleepMode.unit = "m";
285             profile.settings.sleepMode.period = dc.sys.sleep != null ? dc.sys.sleep.wakeupPeriod / 60 : 720;
286             checkSetWsCallback();
287         }
288
289         profile.initialized = true;
290         if (!discovery) {
291             getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
292             asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device
293
294             try {
295                 logger.debug("{}: BLU Gateway support enabled for this device: {}", thingName, config.enableBluGateway);
296                 if (config.enableBluGateway) {
297                     if (getBool(profile.settings.bluetooth)) {
298                         installScript(SHELLY2_BLU_GWSCRIPT);
299                     } else {
300                         logger.debug("{}: Bluetooth needs to be enabled to activate BLU Gateway mode", thingName);
301                     }
302                 }
303             } catch (ShellyApiException e) {
304                 logger.debug("{}: Device config failed", thingName, e);
305             }
306         }
307
308         return profile;
309     }
310
311     private void fillWiFiSta(@Nullable Shelly2DeviceConfigSta from, ShellySettingsWiFiNetwork to) {
312         to.enabled = from != null && !getString(from.ssid).isEmpty();
313         if (from != null) {
314             to.ssid = from.ssid;
315             to.ip = from.ip;
316             to.mask = from.netmask;
317             to.dns = from.nameserver;
318         }
319     }
320
321     private void checkSetWsCallback() throws ShellyApiException {
322         Shelly2ConfigParms wsConfig = apiRequest(SHELLYRPC_METHOD_WSGETCONFIG, null, Shelly2ConfigParms.class);
323         String url = "ws://" + config.localIp + ":" + config.localPort + "/shelly/wsevent";
324         if (!config.localIp.isEmpty() && !getBool(wsConfig.enable)
325                 || !url.equalsIgnoreCase(getString(wsConfig.server))) {
326             logger.debug("{}: A battery device was detected without correct callback, fix it", thingName);
327             wsConfig.enable = true;
328             wsConfig.server = url;
329             Shelly2RpcRequest request = new Shelly2RpcRequest();
330             request.id = 0;
331             request.method = SHELLYRPC_METHOD_WSSETCONFIG;
332             request.params.config = wsConfig;
333             Shelly2WsConfigResponse response = apiRequest(SHELLYRPC_METHOD_WSSETCONFIG, request.params,
334                     Shelly2WsConfigResponse.class);
335             if (response.result != null && response.result.restartRequired) {
336                 logger.info("{}: WebSocket callback was updated, device is restarting", thingName);
337                 getThing().getApi().deviceReboot();
338                 getThing().reinitializeThing();
339             }
340         }
341     }
342
343     protected void installScript(String script) throws ShellyApiException {
344         String json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_LIST));
345         ShellyScriptListResponse scriptList = gson.fromJson(json, ShellyScriptListResponse.class);
346         Integer ourId = -1;
347         String code = "";
348
349         logger.debug("{}: Install or restart script {} on Shelly Device", thingName, script);
350         boolean running = false, upload = false;
351         if (scriptList != null) {
352             for (ShellyScriptListEntry s : scriptList.scripts) {
353                 if (s.name.startsWith(script)) {
354                     ourId = s.id;
355                     running = s.running;
356                     logger.debug("{}: Script {} is already installed, id={}", thingName, script, ourId);
357                 }
358             }
359         }
360
361         // get script code from bundle resources
362         String file = BUNDLE_RESOURCE_SCRIPTS + "/" + script;
363         ClassLoader cl = Shelly2ApiRpc.class.getClassLoader();
364         if (cl != null) {
365             try (InputStream inputStream = cl.getResourceAsStream(file)) {
366                 if (inputStream != null) {
367                     code = new BufferedReader(new InputStreamReader(inputStream)).lines()
368                             .collect(Collectors.joining("\n"));
369                 }
370             } catch (IOException | UncheckedIOException e) {
371                 logger.debug("{}: Installation of script {} failed: Unable to read {} from bundle resources!",
372                         thingName, script, file, e);
373             }
374         }
375
376         boolean restart = false;
377         if (ourId == -1) {
378             // script not installed -> install it
379             upload = true;
380         } else {
381             try {
382                 // verify that the same code version is active (avoid unnesesary flash updates)
383                 json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_GETCODE).withId(ourId));
384                 ShellyScriptResponse rsp = gson.fromJson(json, ShellyScriptResponse.class);
385                 if (!rsp.data.trim().equals(code.trim())) {
386                     logger.debug("{}: A script version was found, update to newest one", thingName);
387                     upload = true;
388                 } else {
389                     logger.debug("{}: Same script version was found, restart", thingName);
390                     restart = true;
391                 }
392             } catch (ShellyApiException e) {
393                 logger.debug("{}: Unable to read current script code -> force update (deviced returned: {})", thingName,
394                         e.getMessage());
395                 upload = true;
396             }
397         }
398
399         if (restart || (running && upload)) {
400             json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_STOP).withId(ourId));
401             // first stop running script
402             running = false;
403         }
404         if (upload && ourId != -1) {
405             // Delete existing script
406             logger.debug("{}: Delete existing script", thingName);
407             json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_DELETE).withId(ourId));
408         }
409
410         if (upload) {
411             logger.debug("{}: Script will be installed...", thingName);
412
413             // Create new script, get id
414             json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_CREATE).withName(script));
415             ShellyScriptResponse rsp = gson.fromJson(json, ShellyScriptResponse.class);
416             if (rsp != null) {
417                 ourId = rsp.id;
418                 logger.debug("{}: Script has been created, id={}", thingName, ourId);
419                 upload = true;
420             }
421         }
422
423         if (upload) {
424             // Put script code for generated id
425             ShellyScriptPutCodeParams parms = new ShellyScriptPutCodeParams();
426             parms.id = ourId;
427             parms.append = false;
428             int length = code.length(), processed = 0, chunk = 1;
429             do {
430                 int nextlen = Math.min(1024, length - processed);
431                 parms.code = code.substring(processed, processed + nextlen);
432                 logger.debug("{}: Uploading chunk {} of script (total {} chars, {} processed)", thingName, chunk,
433                         length, processed);
434                 apiRequest(SHELLYRPC_METHOD_SCRIPT_PUTCODE, parms, String.class);
435                 processed += nextlen;
436                 chunk++;
437                 parms.append = true;
438             } while (processed < length);
439             running = false;
440
441             Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
442             params.config.enable = true;
443             apiRequest(SHELLYRPC_METHOD_SCRIPT_SETCONFIG, params, String.class);
444         }
445
446         if (!running) {
447             // Script was created or is there and stopped -> start it
448             json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_START).withId(ourId));
449             logger.debug("{}: Script {} was {} successful", thingName, script,
450                     restart ? "restarted" : "installed and started");
451         }
452     }
453
454     @Override
455     public void onConnect(String deviceIp, boolean connected) {
456         if (thing == null && thingTable != null) {
457             thing = thingTable.getThing(deviceIp);
458             logger.debug("{}: Get thing from thingTable", thingName);
459         }
460     }
461
462     @Override
463     public void onNotifyStatus(Shelly2RpcNotifyStatus message) {
464         logger.debug("{}: NotifyStatus update received: {}", thingName, gson.toJson(message));
465         try {
466             ShellyThingInterface t = thing;
467             if (t == null) {
468                 logger.debug("{}: No matching thing on NotifyStatus for {}, ignore (src={}, dst={}, discovery={})",
469                         thingName, thingName, message.src, message.dst, discovery);
470                 return;
471             }
472             if (t.isStopping()) {
473                 logger.debug("{}: Thing is shutting down, ignore WebSocket message", thingName);
474                 return;
475             }
476             if (!t.isThingOnline() && t.getThingStatusDetail() != ThingStatusDetail.CONFIGURATION_PENDING) {
477                 logger.debug("{}: Thing is not in online state/connectable, ignore NotifyStatus", thingName);
478                 return;
479             }
480
481             getThing().incProtMessages();
482             if (message.error != null) {
483                 if (message.error.code == HttpStatus.UNAUTHORIZED_401 && !getString(message.error.message).isEmpty()) {
484                     // Save nonce for notification
485                     Shelly2AuthResponse auth = gson.fromJson(message.error.message, Shelly2AuthResponse.class);
486                     if (auth != null && auth.realm == null) {
487                         logger.debug("{}: Authentication data received: {}", thingName, message.error.message);
488                         authInfo = auth;
489                     }
490                 } else {
491                     logger.debug("{}: Error status received - {} {}", thingName, message.error.code,
492                             message.error.message);
493                     incProtErrors();
494                 }
495             }
496
497             Shelly2NotifyStatus params = message.params;
498             if (params != null) {
499                 if (getThing().getThingStatusDetail() != ThingStatusDetail.FIRMWARE_UPDATING) {
500                     getThing().setThingOnline();
501                 }
502
503                 boolean updated = false;
504                 ShellyDeviceProfile profile = getProfile();
505                 ShellySettingsStatus status = profile.status;
506                 if (params.sys != null) {
507                     if (getBool(params.sys.restartRequired)) {
508                         logger.warn("{}: Device requires restart to activate changes", thingName);
509                     }
510                     status.uptime = params.sys.uptime;
511                 }
512                 status.temperature = SHELLY_API_INVTEMP; // mark invalid
513                 updated |= fillDeviceStatus(status, message.params, true);
514                 if (getDouble(status.temperature) == SHELLY_API_INVTEMP) {
515                     // no device temp available
516                     status.temperature = null;
517                 } else {
518                     updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
519                             toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
520                 }
521
522                 profile.status = status;
523                 if (updated) {
524                     getThing().restartWatchdog();
525                 }
526             }
527         } catch (ShellyApiException e) {
528             logger.debug("{}: Unable to process status update", thingName, e);
529             incProtErrors();
530         }
531     }
532
533     @Override
534     public void onNotifyEvent(Shelly2RpcNotifyEvent message) {
535         try {
536             logger.debug("{}: NotifyEvent  received: {}", thingName, gson.toJson(message));
537             ShellyDeviceProfile profile = getProfile();
538
539             getThing().incProtMessages();
540             getThing().restartWatchdog();
541
542             for (Shelly2NotifyEvent e : message.params.events) {
543                 switch (e.event) {
544                     case SHELLY2_EVENT_BTNUP:
545                     case SHELLY2_EVENT_BTNDOWN:
546                         String bgroup = getProfile().getInputGroup(e.id);
547                         updateChannel(bgroup, CHANNEL_INPUT + profile.getInputSuffix(e.id),
548                                 getOnOff(SHELLY2_EVENT_BTNDOWN.equals(getString(e.event))));
549                         getThing().triggerButton(profile.getInputGroup(e.id), e.id,
550                                 mapValue(MAP_INPUT_EVENT_ID, e.event));
551                         break;
552
553                     case SHELLY2_EVENT_1PUSH:
554                     case SHELLY2_EVENT_2PUSH:
555                     case SHELLY2_EVENT_3PUSH:
556                     case SHELLY2_EVENT_LPUSH:
557                     case SHELLY2_EVENT_SLPUSH:
558                     case SHELLY2_EVENT_LSPUSH:
559                         if (e.id < profile.numInputs) {
560                             ShellyInputState input = relayStatus.inputs.get(e.id);
561                             input.event = getString(MAP_INPUT_EVENT_TYPE.get(e.event));
562                             input.eventCount = getInteger(input.eventCount) + 1;
563                             relayStatus.inputs.set(e.id, input);
564                             profile.status.inputs.set(e.id, input);
565
566                             String group = getProfile().getInputGroup(e.id);
567                             updateChannel(group, CHANNEL_STATUS_EVENTTYPE + profile.getInputSuffix(e.id),
568                                     getStringType(input.event));
569                             updateChannel(group, CHANNEL_STATUS_EVENTCOUNT + profile.getInputSuffix(e.id),
570                                     getDecimal(input.eventCount));
571                             getThing().triggerButton(profile.getInputGroup(e.id), e.id,
572                                     mapValue(MAP_INPUT_EVENT_ID, e.event));
573                         }
574                         break;
575                     case SHELLY2_EVENT_CFGCHANGED:
576                         logger.debug("{}: Configuration update detected, re-initialize", thingName);
577                         getThing().requestUpdates(1, true); // refresh config
578                         break;
579
580                     case SHELLY2_EVENT_OTASTART:
581                         logger.debug("{}: Firmware update started: {}", thingName, getString(e.msg));
582                         getThing().postEvent(e.event, true);
583                         getThing().setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING,
584                                 "offline.status-error-fwupgrade");
585                         break;
586                     case SHELLY2_EVENT_OTAPROGRESS:
587                         logger.debug("{}: Firmware update in progress: {}", thingName, getString(e.msg));
588                         getThing().postEvent(e.event, false);
589                         break;
590                     case SHELLY2_EVENT_OTADONE:
591                         logger.debug("{}: Firmware update completed: {}", thingName, getString(e.msg));
592                         getThing().setThingOffline(ThingStatusDetail.CONFIGURATION_PENDING,
593                                 "offline.status-error-restarted");
594                         getThing().requestUpdates(1, true); // refresh config
595                         break;
596                     case SHELLY2_EVENT_SLEEP:
597                         logger.debug("{}: Device went to sleep mode", thingName);
598                         break;
599                     case SHELLY2_EVENT_WIFICONNFAILED:
600                         logger.debug("{}: WiFi connect failed, check setup, reason {}", thingName,
601                                 getInteger(e.reason));
602                         getThing().postEvent(e.event, false);
603                         break;
604                     case SHELLY2_EVENT_WIFIDISCONNECTED:
605                         logger.debug("{}: WiFi disconnected, reason {}", thingName, getInteger(e.reason));
606                         getThing().postEvent(e.event, false);
607                         break;
608                     default:
609                         logger.debug("{}: Event {} was not handled", thingName, e.event);
610                 }
611             }
612         } catch (ShellyApiException e) {
613             logger.debug("{}: Unable to process event", thingName, e);
614             incProtErrors();
615         }
616     }
617
618     @Override
619     public void onMessage(String message) {
620         logger.debug("{}: Unexpected RPC message received: {}", thingName, message);
621         incProtErrors();
622     }
623
624     @Override
625     public void onClose(int statusCode, String reason) {
626         try {
627             logger.debug("{}: WebSocket connection closed, status = {}/{}", thingName, statusCode, getString(reason));
628             if (statusCode == StatusCode.ABNORMAL && !discovery && getProfile().alwaysOn) { // e.g. device rebooted
629                 thingOffline("WebSocket connection closed abnormal");
630             }
631         } catch (ShellyApiException e) {
632             logger.debug("{}: Exception on onClose()", thingName, e);
633             incProtErrors();
634         }
635     }
636
637     @Override
638     public void onError(Throwable cause) {
639         logger.debug("{}: WebSocket error", thingName);
640         if (thing != null && thing.getProfile().alwaysOn) {
641             thingOffline("WebSocket error");
642         }
643     }
644
645     private void thingOffline(String reason) {
646         if (thing != null) { // do not reinit of battery powered devices with sleep mode
647             thing.setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "offline.status-error-unexpected-error",
648                     reason);
649         }
650     }
651
652     @Override
653     public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
654         Shelly2DeviceSettings device = callApi("/shelly", Shelly2DeviceSettings.class);
655         ShellySettingsDevice info = new ShellySettingsDevice();
656         info.hostname = getString(device.id);
657         info.fw = getString(device.firmware);
658         info.type = getString(device.model);
659         info.mac = getString(device.mac);
660         info.auth = getBool(device.authEnable);
661         info.gen = getInteger(device.gen);
662         return info;
663     }
664
665     @Override
666     public ShellySettingsStatus getStatus() throws ShellyApiException {
667         ShellyDeviceProfile profile = getProfile();
668         ShellySettingsStatus status = profile.status;
669         Shelly2DeviceStatusResult ds = apiRequest(SHELLYRPC_METHOD_GETSTATUS, null, Shelly2DeviceStatusResult.class);
670         status.time = ds.sys.time;
671         status.uptime = ds.sys.uptime;
672         status.cloud.connected = getBool(ds.cloud.connected);
673         status.mqtt.connected = getBool(ds.mqtt.connected);
674         status.wifiSta.ssid = getString(ds.wifi.ssid);
675         status.wifiSta.enabled = !status.wifiSta.ssid.isEmpty();
676         status.wifiSta.ip = getString(ds.wifi.staIP);
677         status.wifiSta.rssi = getInteger(ds.wifi.rssi);
678         status.fsFree = ds.sys.fsFree;
679         status.fsSize = ds.sys.fsSize;
680         status.discoverable = getBool(profile.settings.discoverable);
681
682         if (ds.sys.wakeupPeriod != null) {
683             profile.settings.sleepMode.period = ds.sys.wakeupPeriod / 60;
684         }
685
686         status.hasUpdate = status.update.hasUpdate = false;
687         status.update.oldVersion = getProfile().fwVersion;
688         if (ds.sys.availableUpdates != null) {
689             status.update.hasUpdate = ds.sys.availableUpdates.stable != null;
690             if (ds.sys.availableUpdates.stable != null) {
691                 status.update.newVersion = "v" + getString(ds.sys.availableUpdates.stable.version);
692             }
693             if (ds.sys.availableUpdates.beta != null) {
694                 status.update.betaVersion = "v" + getString(ds.sys.availableUpdates.beta.version);
695             }
696         }
697
698         if (ds.sys.wakeUpReason != null && ds.sys.wakeUpReason.boot != null) {
699             List<Object> values = new ArrayList<>();
700             String boot = getString(ds.sys.wakeUpReason.boot);
701             String cause = getString(ds.sys.wakeUpReason.cause);
702
703             // Index 0 is aggregated status, 1 boot, 2 cause
704             String reason = boot.equals(SHELLY2_WAKEUPO_BOOT_RESTART) ? ALARM_TYPE_RESTARTED : cause;
705             values.add(reason);
706             values.add(ds.sys.wakeUpReason.boot);
707             values.add(ds.sys.wakeUpReason.cause);
708             getThing().updateWakeupReason(values);
709         }
710
711         fillDeviceStatus(status, ds, false);
712         return status;
713     }
714
715     @Override
716     public void setSleepTime(int value) throws ShellyApiException {
717     }
718
719     @Override
720     public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException {
721         if (getProfile().status.wifiSta.ssid == null) {
722             // Update status when not yet initialized
723             getStatus();
724         }
725         return relayStatus;
726     }
727
728     @Override
729     public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
730         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
731         params.id = id;
732         params.on = SHELLY_API_ON.equals(turnMode);
733         apiRequest(SHELLYRPC_METHOD_SWITCH_SET, params, String.class);
734     }
735
736     @Override
737     public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException {
738         if (rollerIndex < rollerStatus.size()) {
739             return rollerStatus.get(rollerIndex);
740         }
741         throw new IllegalArgumentException("Invalid rollerIndex on getRollerStatus");
742     }
743
744     @Override
745     public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException {
746         String operation = "";
747         switch (turnMode) {
748             case SHELLY_ALWD_ROLLER_TURN_OPEN:
749                 operation = SHELLY2_COVER_CMD_OPEN;
750                 break;
751             case SHELLY_ALWD_ROLLER_TURN_CLOSE:
752                 operation = SHELLY2_COVER_CMD_CLOSE;
753                 break;
754             case SHELLY_ALWD_ROLLER_TURN_STOP:
755                 operation = SHELLY2_COVER_CMD_STOP;
756                 break;
757         }
758
759         apiRequest(new Shelly2RpcRequest().withMethod("Cover." + operation).withId(relayIndex));
760     }
761
762     @Override
763     public void setRollerPos(int relayIndex, int position) throws ShellyApiException {
764         apiRequest(
765                 new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_COVER_SETPOS).withId(relayIndex).withPos(position));
766     }
767
768     @Override
769     public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
770         return sensorData;
771     }
772
773     @Override
774     public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException {
775         Shelly2RpcRequest req = new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SWITCH_SETCONFIG).withId(index);
776
777         req.params.withConfig();
778         req.params.config.name = "Switch" + index;
779         if (timerName.equals(SHELLY_TIMER_AUTOON)) {
780             req.params.config.autoOn = value > 0;
781             req.params.config.autoOnDelay = value;
782         } else {
783             req.params.config.autoOff = value > 0;
784             req.params.config.autoOffDelay = value;
785         }
786         apiRequest(req);
787     }
788
789     @Override
790     public void resetMeterTotal(int id) throws ShellyApiException {
791     }
792
793     @Override
794     public void muteSmokeAlarm(int index) throws ShellyApiException {
795         apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SMOKE_MUTE).withId(index));
796     }
797
798     @Override
799     public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
800         return new ShellySettingsLogin();
801     }
802
803     @Override
804     public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
805         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
806         params.user = "admin";
807         params.realm = config.serviceName;
808         params.ha1 = sha256(params.user + ":" + params.realm + ":" + password);
809         apiRequest(SHELLYRPC_METHOD_AUTHSET, params, String.class);
810
811         ShellySettingsLogin res = new ShellySettingsLogin();
812         res.enabled = true;
813         res.username = params.user;
814         res.password = password;
815         return new ShellySettingsLogin();
816     }
817
818     @Override
819     public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException {
820         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
821         params.config.ap = new Shelly2DeviceConfigAp();
822         params.config.ap.rangeExtender = new Shelly2DeviceConfigApRE();
823         params.config.ap.rangeExtender.enable = enable;
824         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_WIFISETCONG, params, Shelly2WsConfigResult.class);
825         return res.restartRequired;
826     }
827
828     @Override
829     public boolean setEthernet(boolean enable) throws ShellyApiException {
830         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
831         params.config.enable = enable;
832         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_ETHSETCONG, params, Shelly2WsConfigResult.class);
833         return res.restartRequired;
834     }
835
836     @Override
837     public boolean setBluetooth(boolean enable) throws ShellyApiException {
838         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
839         params.config.enable = enable;
840         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_BLESETCONG, params, Shelly2WsConfigResult.class);
841         return res.restartRequired;
842     }
843
844     @Override
845     public String deviceReboot() throws ShellyApiException {
846         return apiRequest(SHELLYRPC_METHOD_REBOOT, null, String.class);
847     }
848
849     @Override
850     public String factoryReset() throws ShellyApiException {
851         return apiRequest(SHELLYRPC_METHOD_RESET, null, String.class);
852     }
853
854     @Override
855     public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
856         Shelly2DeviceStatusSysAvlUpdate status = apiRequest(SHELLYRPC_METHOD_CHECKUPD, null,
857                 Shelly2DeviceStatusSysAvlUpdate.class);
858         ShellyOtaCheckResult result = new ShellyOtaCheckResult();
859         result.status = status.stable != null || status.beta != null ? "new" : "ok";
860         return result;
861     }
862
863     @Override
864     public ShellySettingsUpdate firmwareUpdate(String fwurl) throws ShellyApiException {
865         ShellySettingsUpdate res = new ShellySettingsUpdate();
866         boolean prod = fwurl.contains("update");
867         boolean beta = fwurl.contains("beta");
868
869         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
870         if (prod || beta) {
871             params.stage = prod || beta ? "stable" : "beta";
872         } else {
873             params.url = fwurl;
874         }
875         apiRequest(SHELLYRPC_METHOD_UPDATE, params, String.class);
876         res.status = "Update initiated";
877         return res;
878     }
879
880     @Override
881     public String setCloud(boolean enable) throws ShellyApiException {
882         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
883         params.config.enable = enable;
884         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_CLOUDSET, params, Shelly2WsConfigResult.class);
885         return res.restartRequired ? "restart required" : "ok";
886     }
887
888     @Override
889     public String setDebug(boolean enabled) throws ShellyApiException {
890         return "failed";
891     }
892
893     @Override
894     public String getDebugLog(String id) throws ShellyApiException {
895         return ""; // Gen2 uses WS to publish debug log
896     }
897
898     /*
899      * The following API calls are not yet relevant, because currently there a no Plus/Pro (Gen2) devices of those
900      * categories (e.g. bulbs)
901      */
902     @Override
903     public void setLedStatus(String ledName, boolean value) throws ShellyApiException {
904         throw new ShellyApiException("API call not implemented");
905     }
906
907     @Override
908     public ShellyStatusLight getLightStatus() throws ShellyApiException {
909         throw new ShellyApiException("API call not implemented");
910     }
911
912     @Override
913     public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
914         throw new ShellyApiException("API call not implemented");
915     }
916
917     @Override
918     public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
919         throw new ShellyApiException("API call not implemented");
920     }
921
922     @Override
923     public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
924         throw new ShellyApiException("API call not implemented");
925     }
926
927     @Override
928     public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
929         throw new ShellyApiException("API call not implemented");
930     }
931
932     @Override
933     public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
934         throw new ShellyApiException("API call not implemented");
935     }
936
937     @Override
938     public void setLightMode(String mode) throws ShellyApiException {
939         throw new ShellyApiException("API call not implemented");
940     }
941
942     @Override
943     public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
944         throw new ShellyApiException("API call not implemented");
945     }
946
947     @Override
948     public void setValvePosition(int valveId, double value) throws ShellyApiException {
949         throw new ShellyApiException("API call not implemented");
950     }
951
952     @Override
953     public void setValveTemperature(int valveId, int value) throws ShellyApiException {
954         throw new ShellyApiException("API call not implemented");
955     }
956
957     @Override
958     public void setValveProfile(int valveId, int value) throws ShellyApiException {
959         throw new ShellyApiException("API call not implemented");
960     }
961
962     @Override
963     public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
964         throw new ShellyApiException("API call not implemented");
965     }
966
967     @Override
968     public void startValveBoost(int valveId, int value) throws ShellyApiException {
969         throw new ShellyApiException("API call not implemented");
970     }
971
972     @Override
973     public String resetStaCache() throws ShellyApiException {
974         throw new ShellyApiException("API call not implemented");
975     }
976
977     @Override
978     public void setActionURLs() throws ShellyApiException {
979         // not relevant for Gen2
980     }
981
982     @Override
983     public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
984         // not relevant for Gen2
985         return new ShellySettingsLogin();
986     }
987
988     @Override
989     public String getCoIoTDescription() {
990         return ""; // not relevant to Gen2
991     }
992
993     @Override
994     public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
995         throw new ShellyApiException("API call not implemented");
996     }
997
998     @Override
999     public String setWiFiRecovery(boolean enable) throws ShellyApiException {
1000         return "failed"; // not supported by Gen2
1001     }
1002
1003     @Override
1004     public String setApRoaming(boolean enable) throws ShellyApiException {
1005         return "false";// not supported by Gen2
1006     }
1007
1008     private void asyncApiRequest(String method) throws ShellyApiException {
1009         Shelly2RpcBaseMessage request = buildRequest(method, null);
1010         reconnect();
1011         rpcSocket.sendMessage(gson.toJson(request)); // submit, result wull be async
1012     }
1013
1014     @SuppressWarnings("null")
1015     public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
1016         String json = "";
1017         Shelly2RpcBaseMessage req = buildRequest(method, params);
1018         try {
1019             reconnect(); // make sure WS is connected
1020
1021             if (authInfo.realm != null) {
1022                 req.auth = buildAuthRequest(authInfo, config.userId, config.serviceName, config.password);
1023             }
1024             json = rpcPost(gson.toJson(req));
1025         } catch (ShellyApiException e) {
1026             ShellyApiResult res = e.getApiResult();
1027             String auth = getString(res.authResponse);
1028             if (res.isHttpAccessUnauthorized() && !auth.isEmpty()) {
1029                 String[] options = auth.split(",");
1030                 for (String o : options) {
1031                     String key = substringBefore(o, "=").stripLeading().trim();
1032                     String value = substringAfter(o, "=").replaceAll("\"", "").trim();
1033                     switch (key) {
1034                         case "Digest qop":
1035                             break;
1036                         case "realm":
1037                             authInfo.realm = value;
1038                             break;
1039                         case "nonce":
1040                             authInfo.nonce = Long.parseLong(value, 16);
1041                             break;
1042                         case "algorithm":
1043                             authInfo.algorithm = value;
1044                             break;
1045                     }
1046                 }
1047                 authInfo.nc = 1;
1048                 req.auth = buildAuthRequest(authInfo, config.userId, authInfo.realm, config.password);
1049                 json = rpcPost(gson.toJson(req));
1050             } else {
1051                 throw e;
1052             }
1053         }
1054         Shelly2RpcBaseMessage response = gson.fromJson(json, Shelly2RpcBaseMessage.class);
1055         if (response == null) {
1056             throw new IllegalArgumentException("Unable to cover API result to obhect");
1057         }
1058         if (response.result != null) {
1059             // return sub element result as requested class type
1060             json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
1061             return fromJson(gson, json, classOfT);
1062         } else {
1063             // return direct format
1064             return gson.fromJson(json, classOfT);
1065         }
1066     }
1067
1068     public <T> T apiRequest(Shelly2RpcRequest request, Class<T> classOfT) throws ShellyApiException {
1069         return apiRequest(request.method, request.params, classOfT);
1070     }
1071
1072     public String apiRequest(Shelly2RpcRequest request) throws ShellyApiException {
1073         return apiRequest(request.method, request.params, String.class);
1074     }
1075
1076     private String rpcPost(String postData) throws ShellyApiException {
1077         return httpPost("/rpc", postData);
1078     }
1079
1080     private void reconnect() throws ShellyApiException {
1081         if (!rpcSocket.isConnected()) {
1082             logger.debug("{}: Connect Rpc Socket (discovery = {})", thingName, discovery);
1083             rpcSocket.connect();
1084         }
1085     }
1086
1087     private void disconnect() {
1088         if (rpcSocket.isConnected()) {
1089             rpcSocket.disconnect();
1090         }
1091     }
1092
1093     public Shelly2RpctInterface getRpcHandler() {
1094         return this;
1095     }
1096
1097     @Override
1098     public void close() {
1099         logger.debug("{}: Closing Rpc API (socket is {}, discovery={})", thingName,
1100                 rpcSocket.isConnected() ? "connected" : "disconnected", discovery);
1101         disconnect();
1102         initialized = false;
1103     }
1104
1105     private void incProtErrors() {
1106         if (thing != null) {
1107             thing.incProtErrors();
1108         }
1109     }
1110 }