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