]> git.basschouten.com Git - openhab-addons.git/blob
48da20713fcf6c5435df4599989e6b059b6aaa57
[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.bticinosmarther.internal.handler;
14
15 import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*;
16
17 import java.time.Duration;
18 import java.util.List;
19 import java.util.concurrent.Future;
20 import java.util.concurrent.TimeUnit;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.bticinosmarther.internal.api.dto.Chronothermostat;
25 import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.BoostTime;
26 import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.Mode;
27 import org.openhab.binding.bticinosmarther.internal.api.dto.ModuleStatus;
28 import org.openhab.binding.bticinosmarther.internal.api.dto.Notification;
29 import org.openhab.binding.bticinosmarther.internal.api.dto.Program;
30 import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
31 import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException;
32 import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherSubscriptionAlreadyExistsException;
33 import org.openhab.binding.bticinosmarther.internal.config.SmartherModuleConfiguration;
34 import org.openhab.binding.bticinosmarther.internal.model.ModuleSettings;
35 import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
36 import org.openhab.core.cache.ExpiringCache;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.library.unit.SIUnits;
42 import org.openhab.core.scheduler.CronScheduler;
43 import org.openhab.core.scheduler.ScheduledCompletableFuture;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.Channel;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.ThingStatusInfo;
51 import org.openhab.core.thing.binding.BaseThingHandler;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.openhab.core.types.State;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * The {@code SmartherModuleHandler} class is responsible of a single Smarther Chronothermostat, handling the commands
60  * that are sent to one of its channels.
61  * Each Smarther Chronothermostat communicates with the Smarther API via its assigned {@code SmartherBridgeHandler}.
62  *
63  * @author Fabio Possieri - Initial contribution
64  */
65 @NonNullByDefault
66 public class SmartherModuleHandler extends BaseThingHandler {
67
68     private static final String DAILY_MIDNIGHT = "1 0 0 * * ? *";
69     private static final long POLL_INITIAL_DELAY = 5;
70
71     private final Logger logger = LoggerFactory.getLogger(SmartherModuleHandler.class);
72
73     private final CronScheduler cronScheduler;
74     private final SmartherDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
75     private final ChannelUID programChannelUID;
76     private final ChannelUID endDateChannelUID;
77
78     // Module configuration
79     private SmartherModuleConfiguration config;
80
81     // Field members assigned in initialize method
82     private @Nullable ScheduledCompletableFuture<Void> jobFuture;
83     private @Nullable Future<?> pollFuture;
84     private @Nullable SmartherBridgeHandler bridgeHandler;
85     private @Nullable ExpiringCache<List<Program>> programCache;
86     private @Nullable ModuleSettings moduleSettings;
87
88     // Chronothermostat local status
89     private @Nullable Chronothermostat chronothermostat;
90
91     /**
92      * Constructs a {@code SmartherModuleHandler} for the given thing, scheduler and dynamic state description provider.
93      *
94      * @param thing
95      *            the {@link Thing} thing to be used
96      * @param scheduler
97      *            the {@link CronScheduler} periodic job scheduler to be used
98      * @param provider
99      *            the {@link SmartherDynamicStateDescriptionProvider} dynamic state description provider to be used
100      */
101     public SmartherModuleHandler(Thing thing, CronScheduler scheduler,
102             SmartherDynamicStateDescriptionProvider provider) {
103         super(thing);
104         this.cronScheduler = scheduler;
105         this.dynamicStateDescriptionProvider = provider;
106         this.programChannelUID = new ChannelUID(thing.getUID(), CHANNEL_SETTINGS_PROGRAM);
107         this.endDateChannelUID = new ChannelUID(thing.getUID(), CHANNEL_SETTINGS_ENDDATE);
108         this.config = new SmartherModuleConfiguration();
109     }
110
111     // ===========================================================================
112     //
113     // Chronothermostat thing lifecycle management methods
114     //
115     // ===========================================================================
116
117     @Override
118     public void initialize() {
119         logger.debug("Module[{}] Initialize handler", thing.getUID());
120
121         final Bridge localBridge = getBridge();
122         if (localBridge == null) {
123             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
124             return;
125         }
126
127         final SmartherBridgeHandler localBridgeHandler = (SmartherBridgeHandler) localBridge.getHandler();
128         this.bridgeHandler = localBridgeHandler;
129         if (localBridgeHandler == null) {
130             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
131                     "Missing configuration from the Smarther Bridge (UID:%s). Fix configuration or report if this problem remains.",
132                     localBridge.getBridgeUID()));
133             return;
134         }
135
136         this.config = getConfigAs(SmartherModuleConfiguration.class);
137         if (StringUtil.isBlank(config.getPlantId())) {
138             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
139                     "The 'Plant Id' property is not set or empty. If you have an older thing please recreate it.");
140             return;
141         }
142         if (StringUtil.isBlank(config.getModuleId())) {
143             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
144                     "The 'Module Id' property is not set or empty. If you have an older thing please recreate it.");
145             return;
146         }
147         if (config.getProgramsRefreshPeriod() <= 0) {
148             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
149                     "The 'Programs Refresh Period' must be > 0. If you have an older thing please recreate it.");
150             return;
151         }
152         if (config.getStatusRefreshPeriod() <= 0) {
153             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
154                     "The 'Module Status Refresh Period' must be > 0. If you have an older thing please recreate it.");
155             return;
156         }
157
158         // Initialize automatic mode programs local cache
159         final ExpiringCache<List<Program>> localProgramCache = new ExpiringCache<>(
160                 Duration.ofHours(config.getProgramsRefreshPeriod()), this::programCacheAction);
161         this.programCache = localProgramCache;
162
163         // Initialize module local settings
164         final ModuleSettings localModuleSettings = new ModuleSettings(config.getPlantId(), config.getModuleId());
165         this.moduleSettings = localModuleSettings;
166
167         updateStatus(ThingStatus.UNKNOWN);
168
169         scheduleJob();
170         schedulePoll();
171
172         logger.debug("Module[{}] Finished initializing!", thing.getUID());
173     }
174
175     @Override
176     public void handleCommand(ChannelUID channelUID, Command command) {
177         try {
178             handleCommandInternal(channelUID, command);
179             updateModuleStatus();
180         } catch (SmartherIllegalPropertyValueException e) {
181             logger.warn("Module[{}] Received command {} with illegal value {} on channel {}", thing.getUID(), command,
182                     e.getMessage(), channelUID.getId());
183         } catch (SmartherGatewayException e) {
184             // catch exceptions and handle it in your binding
185             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
186         }
187     }
188
189     /**
190      * Handles the command sent to a given Channel of this Chronothermostat.
191      *
192      * @param channelUID
193      *            the identifier of the Channel
194      * @param command
195      *            the command sent to the given Channel
196      *
197      * @throws {@link SmartherIllegalPropertyValueException}
198      *             if the command contains an illegal value that cannot be mapped to any valid enum value
199      * @throws {@link SmartherGatewayException}
200      *             in case of communication issues with the Smarther API
201      */
202     private void handleCommandInternal(ChannelUID channelUID, Command command)
203             throws SmartherIllegalPropertyValueException, SmartherGatewayException {
204         final ModuleSettings localModuleSettings = this.moduleSettings;
205         if (localModuleSettings == null) {
206             return;
207         }
208
209         switch (channelUID.getId()) {
210             case CHANNEL_SETTINGS_MODE:
211                 if (command instanceof StringType) {
212                     localModuleSettings.setMode(Mode.fromValue(command.toString()));
213                     return;
214                 }
215                 break;
216             case CHANNEL_SETTINGS_TEMPERATURE:
217                 if (changeTemperature(command, localModuleSettings)) {
218                     return;
219                 }
220                 break;
221             case CHANNEL_SETTINGS_PROGRAM:
222                 if (command instanceof DecimalType) {
223                     localModuleSettings.setProgram(((DecimalType) command).intValue());
224                     return;
225                 }
226                 break;
227             case CHANNEL_SETTINGS_BOOSTTIME:
228                 if (command instanceof DecimalType) {
229                     localModuleSettings.setBoostTime(BoostTime.fromValue(((DecimalType) command).intValue()));
230                     return;
231                 }
232                 break;
233             case CHANNEL_SETTINGS_ENDDATE:
234                 if (command instanceof StringType) {
235                     localModuleSettings.setEndDate(command.toString());
236                     return;
237                 }
238                 break;
239             case CHANNEL_SETTINGS_ENDHOUR:
240                 if (changeTimeHour(command, localModuleSettings)) {
241                     return;
242                 }
243                 break;
244             case CHANNEL_SETTINGS_ENDMINUTE:
245                 if (changeTimeMinute(command, localModuleSettings)) {
246                     return;
247                 }
248                 break;
249             case CHANNEL_SETTINGS_POWER:
250                 if (command instanceof OnOffType) {
251                     if (OnOffType.ON.equals(command)) {
252                         // Apply module settings to the remote module
253                         if (getBridgeHandler().setModuleStatus(localModuleSettings)) {
254                             // Change applied, update module status
255                             logger.debug("Module[{}] New settings applied!", thing.getUID());
256                         }
257                         updateChannelState(CHANNEL_SETTINGS_POWER, OnOffType.OFF);
258                     }
259                     return;
260                 }
261                 break;
262             case CHANNEL_CONFIG_FETCH_PROGRAMS:
263                 if (command instanceof OnOffType) {
264                     if (OnOffType.ON.equals(command)) {
265                         logger.debug(
266                                 "Module[{}] Manually triggered channel to remotely fetch the updated programs list",
267                                 thing.getUID());
268                         expireCache();
269                         refreshProgramsList();
270                         updateChannelState(CHANNEL_CONFIG_FETCH_PROGRAMS, OnOffType.OFF);
271                     }
272                     return;
273                 }
274                 break;
275         }
276
277         if (command instanceof RefreshType) {
278             // Avoid logging wrong command when refresh command is sent
279             return;
280         }
281
282         logger.debug("Module[{}] Received command {} of wrong type {} on channel {}", thing.getUID(), command,
283                 command.getClass().getTypeName(), channelUID.getId());
284     }
285
286     /**
287      * Changes the "temperature" in module settings, based on the received Command.
288      * The new value is checked against the temperature limits allowed by the device.
289      *
290      * @param command
291      *            the command received on temperature Channel
292      *
293      * @return {@code true} if the change succeeded, {@code false} otherwise
294      */
295     private boolean changeTemperature(Command command, final ModuleSettings settings) {
296         if (!(command instanceof QuantityType)) {
297             return false;
298         }
299
300         QuantityType<?> quantity = (QuantityType<?>) command;
301         QuantityType<?> newMeasure = quantity.toUnit(SIUnits.CELSIUS);
302
303         // Check remote device temperature limits
304         if (newMeasure != null && newMeasure.doubleValue() >= 7.1 && newMeasure.doubleValue() <= 40.0) {
305             // Only tenth degree increments are allowed
306             double newTemperature = Math.round(newMeasure.doubleValue() * 10) / 10.0;
307
308             settings.setSetPointTemperature(QuantityType.valueOf(newTemperature, SIUnits.CELSIUS));
309             return true;
310         }
311         return false;
312     }
313
314     /**
315      * Changes the "end hour" for manual mode in module settings, based on the received Command.
316      * The new value is checked against the 24-hours clock allowed range.
317      *
318      * @param command
319      *            the command received on end hour Channel
320      *
321      * @return {@code true} if the change succeeded, {@code false} otherwise
322      */
323     private boolean changeTimeHour(Command command, final ModuleSettings settings) {
324         if (command instanceof DecimalType) {
325             int endHour = ((DecimalType) command).intValue();
326             if (endHour >= 0 && endHour <= 23) {
327                 settings.setEndHour(endHour);
328                 return true;
329             }
330         }
331         return false;
332     }
333
334     /**
335      * Changes the "end minute" for manual mode in module settings, based on the received Command.
336      * The new value is modified to match a 15 min step increment.
337      *
338      * @param command
339      *            the command received on end minute Channel
340      *
341      * @return {@code true} if the change succeeded, {@code false} otherwise
342      */
343     private boolean changeTimeMinute(Command command, final ModuleSettings settings) {
344         if (command instanceof DecimalType) {
345             int endMinute = ((DecimalType) command).intValue();
346             if (endMinute >= 0 && endMinute <= 59) {
347                 // Only 15 min increments are allowed
348                 endMinute = Math.round(endMinute / 15) * 15;
349                 settings.setEndMinute(endMinute);
350                 return true;
351             }
352         }
353         return false;
354     }
355
356     /**
357      * Handles the notification dispatched to this Chronothermostat from the reference Smarther Bridge.
358      *
359      * @param notification
360      *            the notification to handle
361      */
362     public void handleNotification(Notification notification) {
363         try {
364             final Chronothermostat notificationChrono = notification.getChronothermostat();
365             if (notificationChrono != null) {
366                 this.chronothermostat = notificationChrono;
367                 if (config.isSettingsAutoupdate()) {
368                     final ModuleSettings localModuleSettings = this.moduleSettings;
369                     if (localModuleSettings != null) {
370                         localModuleSettings.updateFromChronothermostat(notificationChrono);
371                     }
372                 }
373                 logger.debug("Module[{}] Handle notification: [{}]", thing.getUID(), this.chronothermostat);
374                 updateModuleStatus();
375             }
376         } catch (SmartherIllegalPropertyValueException e) {
377             logger.warn("Module[{}] Notification has illegal value: [{}]", thing.getUID(), e.getMessage());
378         }
379     }
380
381     @Override
382     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
383         if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
384             // Put module offline when the parent bridge goes offline
385             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Smarther Bridge Offline");
386             logger.debug("Module[{}] Bridge switched {}", thing.getUID(), bridgeStatusInfo.getStatus());
387         } else {
388             // Update the module status when the parent bridge return online
389             logger.debug("Module[{}] Bridge is back ONLINE", thing.getUID());
390             // Restart polling to collect module data
391             schedulePoll();
392         }
393     }
394
395     @Override
396     public void handleRemoval() {
397         super.handleRemoval();
398         stopPoll(true);
399         stopJob(true);
400     }
401
402     @Override
403     public void dispose() {
404         logger.debug("Module[{}] Dispose handler", thing.getUID());
405         stopPoll(true);
406         stopJob(true);
407         try {
408             getBridgeHandler().unregisterNotification(config.getPlantId());
409         } catch (SmartherGatewayException e) {
410             logger.warn("Module[{}] API Gateway error during disposing: {}", thing.getUID(), e.getMessage());
411         }
412         logger.debug("Module[{}] Finished disposing!", thing.getUID());
413     }
414
415     // ===========================================================================
416     //
417     // Chronothermostat data cache management methods
418     //
419     // ===========================================================================
420
421     /**
422      * Returns the available automatic mode programs to be cached for this Chronothermostat.
423      *
424      * @return the available programs to be cached for this Chronothermostat, or {@code null} if the list of available
425      *         programs cannot be retrieved
426      */
427     private @Nullable List<Program> programCacheAction() {
428         try {
429             final List<Program> programs = getBridgeHandler().getModulePrograms(config.getPlantId(),
430                     config.getModuleId());
431             logger.debug("Module[{}] Available programs: {}", thing.getUID(), programs);
432
433             return programs;
434
435         } catch (SmartherGatewayException e) {
436             logger.warn("Module[{}] Cannot retrieve available programs: {}", thing.getUID(), e.getMessage());
437             return null;
438         }
439     }
440
441     /**
442      * Sets all the cache to "expired" for this Chronothermostat.
443      */
444     private void expireCache() {
445         logger.debug("Module[{}] Invalidating program cache", thing.getUID());
446         final ExpiringCache<List<Program>> localProgramCache = this.programCache;
447         if (localProgramCache != null) {
448             localProgramCache.invalidateValue();
449         }
450     }
451
452     // ===========================================================================
453     //
454     // Chronothermostat job scheduler methods
455     //
456     // ===========================================================================
457
458     /**
459      * Starts a new cron scheduler to execute the internal recurring jobs.
460      */
461     private synchronized void scheduleJob() {
462         stopJob(false);
463
464         // Schedule daily job to start daily, at midnight
465         final ScheduledCompletableFuture<Void> localJobFuture = cronScheduler.schedule(this::dailyJob, DAILY_MIDNIGHT);
466         this.jobFuture = localJobFuture;
467
468         logger.debug("Module[{}] Scheduled recurring job {} to start at midnight", thing.getUID(),
469                 Integer.toHexString(localJobFuture.hashCode()));
470
471         // Execute daily job immediately at startup
472         this.dailyJob();
473     }
474
475     /**
476      * Cancels all running jobs.
477      *
478      * @param mayInterruptIfRunning
479      *            {@code true} if the thread executing this task should be interrupted, {@code false} if the in-progress
480      *            tasks are allowed to complete
481      */
482     private synchronized void stopJob(boolean mayInterruptIfRunning) {
483         final ScheduledCompletableFuture<Void> localJobFuture = this.jobFuture;
484         if (localJobFuture != null) {
485             if (!localJobFuture.isCancelled()) {
486                 localJobFuture.cancel(mayInterruptIfRunning);
487             }
488             this.jobFuture = null;
489         }
490     }
491
492     /**
493      * Action to be executed by the daily job: refresh the end dates list for "manual" mode.
494      */
495     private void dailyJob() {
496         logger.debug("Module[{}] Daily job, refreshing the end dates list for \"manual\" mode", thing.getUID());
497         // Refresh the end dates list for "manual" mode
498         dynamicStateDescriptionProvider.setEndDates(endDateChannelUID, config.getNumberOfEndDays());
499         // If expired, update EndDate in module settings
500         final ModuleSettings localModuleSettings = this.moduleSettings;
501         if (localModuleSettings != null && localModuleSettings.isEndDateExpired()) {
502             localModuleSettings.refreshEndDate();
503             updateChannelState(CHANNEL_SETTINGS_ENDDATE, new StringType(localModuleSettings.getEndDate()));
504         }
505     }
506
507     // ===========================================================================
508     //
509     // Chronothermostat status polling mechanism methods
510     //
511     // ===========================================================================
512
513     /**
514      * Starts a new scheduler to periodically poll and update this Chronothermostat status.
515      */
516     private void schedulePoll() {
517         stopPoll(false);
518
519         // Schedule poll to start after POLL_INITIAL_DELAY sec and run periodically based on status refresh period
520         final Future<?> localPollFuture = scheduler.scheduleWithFixedDelay(this::poll, POLL_INITIAL_DELAY,
521                 config.getStatusRefreshPeriod() * 60, TimeUnit.SECONDS);
522         this.pollFuture = localPollFuture;
523
524         logger.debug("Module[{}] Scheduled poll for {} sec out, then every {} min", thing.getUID(), POLL_INITIAL_DELAY,
525                 config.getStatusRefreshPeriod());
526     }
527
528     /**
529      * Cancels all running poll schedulers.
530      *
531      * @param mayInterruptIfRunning
532      *            {@code true} if the thread executing this task should be interrupted, {@code false} if the in-progress
533      *            tasks are allowed to complete
534      */
535     private synchronized void stopPoll(boolean mayInterruptIfRunning) {
536         final Future<?> localPollFuture = this.pollFuture;
537         if (localPollFuture != null) {
538             if (!localPollFuture.isCancelled()) {
539                 localPollFuture.cancel(mayInterruptIfRunning);
540             }
541             this.pollFuture = null;
542         }
543     }
544
545     /**
546      * Polls to update this Chronothermostat status.
547      *
548      * @return {@code true} if the method completes without errors, {@code false} otherwise
549      */
550     private synchronized boolean poll() {
551         try {
552             final Bridge bridge = getBridge();
553             if (bridge != null) {
554                 final ThingStatusInfo bridgeStatusInfo = bridge.getStatusInfo();
555                 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
556                     ModuleStatus moduleStatus = getBridgeHandler().getModuleStatus(config.getPlantId(),
557                             config.getModuleId());
558
559                     final Chronothermostat statusChrono = moduleStatus.toChronothermostat();
560                     if (statusChrono != null) {
561                         if ((this.chronothermostat == null) || config.isSettingsAutoupdate()) {
562                             final ModuleSettings localModuleSettings = this.moduleSettings;
563                             if (localModuleSettings != null) {
564                                 localModuleSettings.updateFromChronothermostat(statusChrono);
565                             }
566                         }
567                         this.chronothermostat = statusChrono;
568                         logger.debug("Module[{}] Status: [{}]", thing.getUID(), this.chronothermostat);
569                     } else {
570                         throw new SmartherGatewayException("No chronothermostat data found");
571                     }
572
573                     // Refresh the programs list for "automatic" mode
574                     refreshProgramsList();
575
576                     updateModuleStatus();
577
578                     getBridgeHandler().registerNotification(config.getPlantId());
579
580                     // Everything is ok > set the Thing state to Online
581                     updateStatus(ThingStatus.ONLINE);
582                     return true;
583                 } else if (thing.getStatus() != ThingStatus.OFFLINE) {
584                     logger.debug("Module[{}] Switched {} as Bridge is not online", thing.getUID(),
585                             bridgeStatusInfo.getStatus());
586                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Smarther Bridge Offline");
587                 }
588             }
589             return false;
590         } catch (SmartherIllegalPropertyValueException e) {
591             logger.debug("Module[{}] Illegal property value error during polling: {}", thing.getUID(), e.getMessage());
592             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
593         } catch (SmartherSubscriptionAlreadyExistsException e) {
594             logger.debug("Module[{}] Subscription error during polling: {}", thing.getUID(), e.getMessage());
595             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
596         } catch (SmartherGatewayException e) {
597             logger.warn("Module[{}] API Gateway error during polling: {}", thing.getUID(), e.getMessage());
598             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
599         } catch (RuntimeException e) {
600             // All other exceptions apart from Subscription and Gateway issues
601             logger.warn("Module[{}] Unexpected error during polling, please report if this keeps occurring: ",
602                     thing.getUID(), e);
603             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
604         }
605         schedulePoll();
606         return false;
607     }
608
609     // ===========================================================================
610     //
611     // Chronothermostat convenience methods
612     //
613     // ===========================================================================
614
615     /**
616      * Convenience method to check and get the Smarther Bridge handler instance for this Module.
617      *
618      * @return the Smarther Bridge handler instance
619      *
620      * @throws {@link SmartherGatewayException}
621      *             in case the Smarther Bridge handler instance is {@code null}
622      */
623     private SmartherBridgeHandler getBridgeHandler() throws SmartherGatewayException {
624         final SmartherBridgeHandler localBridgeHandler = this.bridgeHandler;
625         if (localBridgeHandler == null) {
626             throw new SmartherGatewayException("Smarther Bridge handler instance is null");
627         }
628         return localBridgeHandler;
629     }
630
631     /**
632      * Returns this Chronothermostat plant identifier
633      *
634      * @return a string containing the plant identifier
635      */
636     public String getPlantId() {
637         return config.getPlantId();
638     }
639
640     /**
641      * Returns this Chronothermostat module identifier
642      *
643      * @return a string containing the module identifier
644      */
645     public String getModuleId() {
646         return config.getModuleId();
647     }
648
649     /**
650      * Checks whether this Chronothermostat matches with the given plant and module identifiers.
651      *
652      * @param plantId
653      *            the plant identifier to match to
654      * @param moduleId
655      *            the module identifier to match to
656      *
657      * @return {@code true} if the Chronothermostat matches the given plant and module identifiers, {@code false}
658      *         otherwise
659      */
660     public boolean isLinkedTo(String plantId, String moduleId) {
661         return (config.getPlantId().equals(plantId) && config.getModuleId().equals(moduleId));
662     }
663
664     /**
665      * Convenience method to refresh the module programs list from cache.
666      */
667     private void refreshProgramsList() {
668         final ExpiringCache<List<Program>> localProgramCache = this.programCache;
669         if (localProgramCache != null) {
670             final List<Program> programs = localProgramCache.getValue();
671             if (programs != null) {
672                 dynamicStateDescriptionProvider.setPrograms(programChannelUID, programs);
673             }
674         }
675     }
676
677     /**
678      * Convenience method to update the given Channel state "only" if the Channel is linked.
679      *
680      * @param channelId
681      *            the identifier of the Channel to be updated
682      * @param state
683      *            the new state to be applied to the given Channel
684      */
685     private void updateChannelState(String channelId, State state) {
686         final Channel channel = thing.getChannel(channelId);
687
688         if (channel != null && isLinked(channel.getUID())) {
689             updateState(channel.getUID(), state);
690         }
691     }
692
693     /**
694      * Convenience method to update the whole status of the Chronothermostat associated to this handler.
695      * Channels are updated based on the local {@code chronothermostat} and {@code moduleSettings} objects.
696      *
697      * @throws {@link SmartherIllegalPropertyValueException}
698      *             if at least one of the module properties cannot be mapped to any valid enum value
699      */
700     private void updateModuleStatus() throws SmartherIllegalPropertyValueException {
701         final Chronothermostat localChrono = this.chronothermostat;
702         if (localChrono != null) {
703             // Update the Measures channels
704             updateChannelState(CHANNEL_MEASURES_TEMPERATURE, localChrono.getThermometer().toState());
705             updateChannelState(CHANNEL_MEASURES_HUMIDITY, localChrono.getHygrometer().toState());
706             // Update the Status channels
707             updateChannelState(CHANNEL_STATUS_STATE, (localChrono.isActive() ? OnOffType.ON : OnOffType.OFF));
708             updateChannelState(CHANNEL_STATUS_FUNCTION,
709                     new StringType(StringUtil.capitalize(localChrono.getFunction().toLowerCase())));
710             updateChannelState(CHANNEL_STATUS_MODE,
711                     new StringType(StringUtil.capitalize(localChrono.getMode().toLowerCase())));
712             updateChannelState(CHANNEL_STATUS_TEMPERATURE, localChrono.getSetPointTemperature().toState());
713             updateChannelState(CHANNEL_STATUS_ENDTIME, new StringType(localChrono.getActivationTimeLabel()));
714             updateChannelState(CHANNEL_STATUS_TEMP_FORMAT, new StringType(localChrono.getTemperatureFormat()));
715             final Program localProgram = localChrono.getProgram();
716             if (localProgram != null) {
717                 updateChannelState(CHANNEL_STATUS_PROGRAM, new StringType(String.valueOf(localProgram.getNumber())));
718             }
719         }
720
721         final ModuleSettings localSettings = this.moduleSettings;
722         if (localSettings != null) {
723             // Update the Settings channels
724             updateChannelState(CHANNEL_SETTINGS_MODE, new StringType(localSettings.getMode().getValue()));
725             updateChannelState(CHANNEL_SETTINGS_TEMPERATURE, localSettings.getSetPointTemperature());
726             updateChannelState(CHANNEL_SETTINGS_PROGRAM, new DecimalType(localSettings.getProgram()));
727             updateChannelState(CHANNEL_SETTINGS_BOOSTTIME, new DecimalType(localSettings.getBoostTime().getValue()));
728             updateChannelState(CHANNEL_SETTINGS_ENDDATE, new StringType(localSettings.getEndDate()));
729             updateChannelState(CHANNEL_SETTINGS_ENDHOUR, new DecimalType(localSettings.getEndHour()));
730             updateChannelState(CHANNEL_SETTINGS_ENDMINUTE, new DecimalType(localSettings.getEndMinute()));
731             updateChannelState(CHANNEL_SETTINGS_POWER, OnOffType.OFF);
732         }
733     }
734 }