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