]> git.basschouten.com Git - openhab-addons.git/blob
5a3e203af6c61526caf155a23e87e400a6e36281
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.hdpowerview.internal.handler;
14
15 import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
16 import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
17
18 import java.util.HashMap;
19 import java.util.Map;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import javax.ws.rs.NotSupportedException;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
28 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
29 import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem;
30 import org.openhab.binding.hdpowerview.internal.api.Firmware;
31 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
32 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
33 import org.openhab.binding.hdpowerview.internal.api.responses.Survey;
34 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
35 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
36 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
37 import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
38 import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException;
39 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
40 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
41 import org.openhab.binding.hdpowerview.internal.exceptions.HubShadeTimeoutException;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.OnOffType;
44 import org.openhab.core.library.types.PercentType;
45 import org.openhab.core.library.types.QuantityType;
46 import org.openhab.core.library.types.StopMoveType;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.library.types.UpDownType;
49 import org.openhab.core.library.unit.Units;
50 import org.openhab.core.thing.Bridge;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.types.Command;
56 import org.openhab.core.types.RefreshType;
57 import org.openhab.core.types.UnDefType;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60
61 /**
62  * Handles commands for an HD PowerView Shade
63  *
64  * @author Andy Lintner - Initial contribution
65  * @author Andrew Fiddian-Green - Added support for secondary rail positions
66  */
67 @NonNullByDefault
68 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
69
70     private enum RefreshKind {
71         POSITION,
72         SURVEY,
73         BATTERY_LEVEL
74     }
75
76     private static final String COMMAND_CALIBRATE = "CALIBRATE";
77     private static final String COMMAND_IDENTIFY = "IDENTIFY";
78
79     private static final String DETECTED_SECONDARY_RAIL = "secondaryRailDetected";
80     private static final String DETECTED_TILT_ANYWHERE = "tiltAnywhereDetected";
81     private final Map<String, String> detectedCapabilities = new HashMap<>();
82
83     private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
84     private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
85
86     private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
87     private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
88     private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
89     private @Nullable Capabilities capabilities;
90     private int shadeId;
91     private boolean isDisposing;
92
93     public HDPowerViewShadeHandler(Thing thing) {
94         super(thing);
95     }
96
97     @Override
98     public void initialize() {
99         isDisposing = false;
100         shadeId = getConfigAs(HDPowerViewShadeConfiguration.class).id;
101         logger.debug("Initializing shade handler for shade {}", shadeId);
102         Bridge bridge = getBridge();
103         if (bridge == null) {
104             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
105                     "@text/offline.conf-error.invalid-bridge-handler");
106             return;
107         }
108
109         updateStatus(ThingStatus.UNKNOWN);
110     }
111
112     @Override
113     public void dispose() {
114         logger.debug("Disposing shade handler for shade {}", shadeId);
115         isDisposing = true;
116         ScheduledFuture<?> future = refreshPositionFuture;
117         if (future != null) {
118             future.cancel(true);
119         }
120         refreshPositionFuture = null;
121         future = refreshSignalFuture;
122         if (future != null) {
123             future.cancel(true);
124         }
125         refreshSignalFuture = null;
126         future = refreshBatteryLevelFuture;
127         if (future != null) {
128             future.cancel(true);
129         }
130         refreshBatteryLevelFuture = null;
131         capabilities = null;
132     }
133
134     @Override
135     public void handleCommand(ChannelUID channelUID, Command command) {
136         String channelId = channelUID.getId();
137
138         if (RefreshType.REFRESH == command) {
139             switch (channelId) {
140                 case CHANNEL_SHADE_POSITION:
141                 case CHANNEL_SHADE_SECONDARY_POSITION:
142                 case CHANNEL_SHADE_VANE:
143                     requestRefreshShadePosition();
144                     break;
145                 case CHANNEL_SHADE_LOW_BATTERY:
146                 case CHANNEL_SHADE_BATTERY_LEVEL:
147                 case CHANNEL_SHADE_BATTERY_VOLTAGE:
148                     requestRefreshShadeBatteryLevel();
149                     break;
150                 case CHANNEL_SHADE_SIGNAL_STRENGTH:
151                     requestRefreshShadeSurvey();
152                     break;
153             }
154             return;
155         }
156
157         HDPowerViewHubHandler bridge = getBridgeHandler();
158         if (bridge == null) {
159             logger.warn("Missing bridge handler");
160             return;
161         }
162         HDPowerViewWebTargets webTargets = bridge.getWebTargets();
163         if (webTargets == null) {
164             logger.warn("Web targets not initialized");
165             return;
166         }
167         try {
168             handleShadeCommand(channelId, command, webTargets, shadeId);
169         } catch (HubInvalidResponseException e) {
170             Throwable cause = e.getCause();
171             if (cause == null) {
172                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
173             } else {
174                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
175             }
176         } catch (HubMaintenanceException e) {
177             // exceptions are logged in HDPowerViewWebTargets
178         } catch (HubShadeTimeoutException e) {
179             logger.warn("Shade {} timeout when sending command {}", shadeId, command);
180         } catch (HubException e) {
181             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
182             // for any ongoing requests. Logging this would only cause confusion.
183             if (!isDisposing) {
184                 logger.warn("Unexpected error: {}", e.getMessage());
185             }
186         }
187     }
188
189     private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
190             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
191             HubShadeTimeoutException {
192         switch (channelId) {
193             case CHANNEL_SHADE_POSITION:
194                 if (command instanceof PercentType) {
195                     moveShade(PRIMARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
196                 } else if (command instanceof UpDownType) {
197                     moveShade(PRIMARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
198                 } else if (command instanceof StopMoveType) {
199                     if (StopMoveType.STOP == command) {
200                         stopShade(webTargets, shadeId);
201                     } else {
202                         logger.warn("Unexpected StopMoveType command");
203                     }
204                 }
205                 break;
206
207             case CHANNEL_SHADE_VANE:
208                 if (command instanceof PercentType) {
209                     moveShade(VANE_TILT_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
210                 } else if (command instanceof OnOffType) {
211                     moveShade(VANE_TILT_POSITION, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
212                 }
213                 break;
214
215             case CHANNEL_SHADE_SECONDARY_POSITION:
216                 if (command instanceof PercentType) {
217                     moveShade(SECONDARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
218                 } else if (command instanceof UpDownType) {
219                     moveShade(SECONDARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
220                 } else if (command instanceof StopMoveType) {
221                     if (StopMoveType.STOP == command) {
222                         stopShade(webTargets, shadeId);
223                     } else {
224                         logger.warn("Unexpected StopMoveType command");
225                     }
226                 }
227                 break;
228
229             case CHANNEL_SHADE_COMMAND:
230                 if (command instanceof StringType) {
231                     if (COMMAND_IDENTIFY.equals(((StringType) command).toString())) {
232                         logger.debug("Identify shade {}", shadeId);
233                         identifyShade(webTargets, shadeId);
234                     } else if (COMMAND_CALIBRATE.equals(((StringType) command).toString())) {
235                         logger.debug("Calibrate shade {}", shadeId);
236                         calibrateShade(webTargets, shadeId);
237                     }
238                 } else {
239                     logger.warn("Unsupported command: {}. Supported commands are: " + COMMAND_CALIBRATE, command);
240                 }
241                 break;
242         }
243     }
244
245     /**
246      * Update the state of the channels based on the ShadeData provided.
247      *
248      * @param shadeData the ShadeData to be used.
249      */
250     protected void onReceiveUpdate(ShadeData shadeData) {
251         updateStatus(ThingStatus.ONLINE);
252         updateCapabilities(shadeData);
253         updateSoftProperties(shadeData);
254         updateFirmwareProperties(shadeData);
255         ShadePosition shadePosition = shadeData.positions;
256         if (shadePosition != null) {
257             updatePositionStates(shadePosition);
258         }
259         updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
260         updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
261     }
262
263     private void updateCapabilities(ShadeData shade) {
264         if (capabilities != null) {
265             // Already cached.
266             return;
267         }
268         Capabilities capabilities = db.getCapabilities(shade.type, shade.capabilities);
269         if (capabilities.getValue() < 0) {
270             logger.debug("Unable to set capabilities for shade {}", shade.id);
271             return;
272         }
273         logger.debug("Caching capabilities {} for shade {}", capabilities.getValue(), shade.id);
274         this.capabilities = capabilities;
275     }
276
277     private Capabilities getCapabilitiesOrDefault() {
278         Capabilities capabilities = this.capabilities;
279         if (capabilities == null) {
280             return new Capabilities();
281         }
282         return capabilities;
283     }
284
285     /**
286      * Update the Thing's properties based on the contents of the provided ShadeData.
287      *
288      * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
289      * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
290      *
291      * @param shadeData
292      */
293     private void updateSoftProperties(ShadeData shadeData) {
294         final Map<String, String> properties = getThing().getProperties();
295         boolean propChanged = false;
296
297         // update 'type' property
298         final int type = shadeData.type;
299         String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
300         String propOldVal = properties.getOrDefault(propKey, "");
301         String propNewVal = db.getType(type).toString();
302         if (!propNewVal.equals(propOldVal)) {
303             propChanged = true;
304             getThing().setProperty(propKey, propNewVal);
305             if ((type > 0) && !db.isTypeInDatabase(type)) {
306                 db.logTypeNotInDatabase(type);
307             }
308         }
309
310         // update 'capabilities' property
311         Capabilities capabilities = db.getCapabilities(shadeData.capabilities);
312         final int capabilitiesVal = capabilities.getValue();
313         propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
314         propOldVal = properties.getOrDefault(propKey, "");
315         propNewVal = capabilities.toString();
316         if (!propNewVal.equals(propOldVal)) {
317             propChanged = true;
318             getThing().setProperty(propKey, propNewVal);
319             if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
320                 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
321             }
322         }
323
324         if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
325                 && (capabilitiesVal != db.getType(type).getCapabilities())) {
326             db.logCapabilitiesMismatch(type, capabilitiesVal);
327         }
328     }
329
330     private void updateFirmwareProperties(ShadeData shadeData) {
331         Map<String, String> properties = editProperties();
332         Firmware shadeFirmware = shadeData.firmware;
333         Firmware motorFirmware = shadeData.motor;
334         if (shadeFirmware != null) {
335             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
336         }
337         if (motorFirmware != null) {
338             properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
339         }
340         updateProperties(properties);
341     }
342
343     /**
344      * After a hard refresh, update the Thing's detected capabilities based on the contents of the provided ShadeData.
345      *
346      * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
347      * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
348      * potential need to add support for that type resp. capabilities.
349      *
350      * @param shadeData
351      */
352     private void updateDetectedCapabilities(ShadeData shadeData) {
353         final ShadePosition positions = shadeData.positions;
354         if (positions == null) {
355             return;
356         }
357         Capabilities capabilities = getCapabilitiesOrDefault();
358
359         // update 'secondary rail' detected capability
360         String capsKey = DETECTED_SECONDARY_RAIL;
361         String capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
362         boolean capsNewBool = positions.secondaryRailDetected();
363         String capsNewVal = String.valueOf(capsNewBool);
364         if (!capsNewVal.equals(capsOldVal)) {
365             detectedCapabilities.put(capsKey, capsNewVal);
366             if (capsNewBool != capabilities.supportsSecondary()) {
367                 db.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
368             }
369         }
370
371         // update 'tilt anywhere' detected capability
372         capsKey = DETECTED_TILT_ANYWHERE;
373         capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
374         capsNewBool = positions.tiltAnywhereDetected();
375         capsNewVal = String.valueOf(capsNewBool);
376         if (!capsNewVal.equals(capsOldVal)) {
377             detectedCapabilities.put(capsKey, capsNewVal);
378             if (capsNewBool != capabilities.supportsTiltAnywhere()) {
379                 db.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
380             }
381         }
382     }
383
384     private void updatePositionStates(ShadePosition shadePos) {
385         Capabilities capabilities = this.capabilities;
386         if (capabilities == null) {
387             logger.debug("The 'shadeCapabilities' field has not yet been initialized");
388             updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
389             updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
390             updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
391             return;
392         }
393         updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_POSITION));
394         updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_POSITION));
395         updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_POSITION));
396     }
397
398     private void updateBatteryStates(int batteryStatus, double batteryStrength) {
399         updateBatteryLevelStates(batteryStatus);
400         updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
401                 batteryStrength > 0 ? new QuantityType<>(batteryStrength / 10, Units.VOLT) : UnDefType.UNDEF);
402     }
403
404     private void updateBatteryLevelStates(int batteryStatus) {
405         int mappedValue;
406         switch (batteryStatus) {
407             case 1: // Low
408                 mappedValue = 10;
409                 break;
410             case 2: // Medium
411                 mappedValue = 50;
412                 break;
413             case 3: // High
414             case 4: // Plugged in
415                 mappedValue = 100;
416                 break;
417             default: // No status available (0) or invalid
418                 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
419                 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
420                 return;
421         }
422         updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
423         updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
424     }
425
426     private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
427             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
428             HubShadeTimeoutException {
429         ShadePosition newPosition = null;
430         // (try to) read the positions from the hub
431         ShadeData shadeData = webTargets.getShade(shadeId);
432         updateCapabilities(shadeData);
433         newPosition = shadeData.positions;
434         // if no positions returned, then create a new position
435         if (newPosition == null) {
436             newPosition = new ShadePosition();
437         }
438         Capabilities capabilities = getCapabilitiesOrDefault();
439         // set the new position value, and write the positions to the hub
440         shadeData = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
441         updateShadePositions(shadeData);
442     }
443
444     private void stopShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
445             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
446         updateShadePositions(webTargets.stopShade(shadeId));
447         // Positions in response from stop motion is not updated to to actual positions yet,
448         // so we need to request hard refresh.
449         requestRefreshShadePosition();
450     }
451
452     private void identifyShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
453             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
454         updateShadePositions(webTargets.jogShade(shadeId));
455     }
456
457     private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
458             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
459         updateShadePositions(webTargets.calibrateShade(shadeId));
460     }
461
462     private void updateShadePositions(ShadeData shadeData) {
463         ShadePosition shadePosition = shadeData.positions;
464         if (shadePosition == null) {
465             return;
466         }
467         updateCapabilities(shadeData);
468         updatePositionStates(shadePosition);
469     }
470
471     /**
472      * Request that the shade shall undergo a 'hard' refresh for querying its current position
473      */
474     protected synchronized void requestRefreshShadePosition() {
475         if (refreshPositionFuture == null) {
476             refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
477         }
478     }
479
480     /**
481      * Request that the shade shall undergo a 'hard' refresh for querying its survey data
482      */
483     protected synchronized void requestRefreshShadeSurvey() {
484         if (refreshSignalFuture == null) {
485             refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
486         }
487     }
488
489     /**
490      * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
491      */
492     protected synchronized void requestRefreshShadeBatteryLevel() {
493         if (refreshBatteryLevelFuture == null) {
494             refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
495         }
496     }
497
498     private void doRefreshShadePosition() {
499         this.doRefreshShade(RefreshKind.POSITION);
500         refreshPositionFuture = null;
501     }
502
503     private void doRefreshShadeSignal() {
504         this.doRefreshShade(RefreshKind.SURVEY);
505         refreshSignalFuture = null;
506     }
507
508     private void doRefreshShadeBatteryLevel() {
509         this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
510         refreshBatteryLevelFuture = null;
511     }
512
513     private void doRefreshShade(RefreshKind kind) {
514         try {
515             HDPowerViewHubHandler bridge;
516             if ((bridge = getBridgeHandler()) == null) {
517                 throw new HubProcessingException("Missing bridge handler");
518             }
519             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
520             if (webTargets == null) {
521                 throw new HubProcessingException("Web targets not initialized");
522             }
523             ShadeData shadeData;
524             switch (kind) {
525                 case POSITION:
526                     shadeData = webTargets.refreshShadePosition(shadeId);
527                     updateShadePositions(shadeData);
528                     updateDetectedCapabilities(shadeData);
529                     break;
530                 case SURVEY:
531                     Survey survey = webTargets.getShadeSurvey(shadeId);
532                     if (survey.surveyData != null) {
533                         logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
534                     } else {
535                         logger.warn("No response from shade {} survey", shadeId);
536                     }
537                     break;
538                 case BATTERY_LEVEL:
539                     shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
540                     updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
541                     break;
542                 default:
543                     throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
544             }
545         } catch (HubInvalidResponseException e) {
546             Throwable cause = e.getCause();
547             if (cause == null) {
548                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
549             } else {
550                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
551             }
552         } catch (HubMaintenanceException e) {
553             // exceptions are logged in HDPowerViewWebTargets
554         } catch (HubShadeTimeoutException e) {
555             logger.info("Shade {} wireless refresh time out", shadeId);
556         } catch (HubException e) {
557             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
558             // for any ongoing requests. Logging this would only cause confusion.
559             if (!isDisposing) {
560                 logger.warn("Unexpected error: {}", e.getMessage());
561             }
562         }
563     }
564 }