]> git.basschouten.com Git - openhab-addons.git/blob
c56893043ae32ce6247232f26f2d486dda5f6c3e
[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.somneo.internal;
14
15 import static org.openhab.binding.somneo.internal.SomneoBindingConstants.*;
16
17 import java.io.EOFException;
18 import java.util.Map;
19 import java.util.Objects;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.openhab.binding.somneo.internal.model.AlarmSchedulesData;
31 import org.openhab.binding.somneo.internal.model.AlarmSettingsData;
32 import org.openhab.binding.somneo.internal.model.AlarmStateData;
33 import org.openhab.binding.somneo.internal.model.AudioData;
34 import org.openhab.binding.somneo.internal.model.DeviceData;
35 import org.openhab.binding.somneo.internal.model.FirmwareData;
36 import org.openhab.binding.somneo.internal.model.LightData;
37 import org.openhab.binding.somneo.internal.model.PresetData;
38 import org.openhab.binding.somneo.internal.model.RadioData;
39 import org.openhab.binding.somneo.internal.model.RelaxData;
40 import org.openhab.binding.somneo.internal.model.SensorData;
41 import org.openhab.binding.somneo.internal.model.SunsetData;
42 import org.openhab.binding.somneo.internal.model.TimerData;
43 import org.openhab.binding.somneo.internal.model.WifiData;
44 import org.openhab.core.library.types.DateTimeType;
45 import org.openhab.core.library.types.DecimalType;
46 import org.openhab.core.library.types.NextPreviousType;
47 import org.openhab.core.library.types.OnOffType;
48 import org.openhab.core.library.types.PercentType;
49 import org.openhab.core.library.types.PlayPauseType;
50 import org.openhab.core.library.types.QuantityType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.library.unit.Units;
53 import org.openhab.core.thing.Channel;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.thing.ThingStatus;
57 import org.openhab.core.thing.ThingStatusDetail;
58 import org.openhab.core.thing.binding.BaseThingHandler;
59 import org.openhab.core.types.Command;
60 import org.openhab.core.types.RefreshType;
61 import org.openhab.core.types.State;
62 import org.openhab.core.types.UnDefType;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66 /**
67  * The {@link SomneoHandler} is responsible for handling commands, which are
68  * sent to one of the channels.
69  *
70  * @author Michael Myrcik - Initial contribution
71  */
72 @NonNullByDefault
73 public class SomneoHandler extends BaseThingHandler {
74
75     private final Logger logger = LoggerFactory.getLogger(SomneoHandler.class);
76
77     private final HttpClientProvider httpClientProvider;
78
79     private final SomneoPresetStateDescriptionProvider provider;
80
81     private final Pattern alarmPattern;
82
83     /**
84      * Job to poll data from the device.
85      */
86     private @Nullable ScheduledFuture<?> pollingJob;
87     private @Nullable ScheduledFuture<?> pollingJobExtended;
88
89     /**
90      * Job to count down the remaining program time.
91      */
92     private @Nullable ScheduledFuture<?> remainingTimerJob;
93
94     private @Nullable SomneoHttpConnector connector;
95
96     /**
97      * Cache the last brightness level in order to know the correct level when the
98      * ON command is given.
99      */
100     private volatile int lastLightBrightness;
101
102     private volatile int remainingTimeRelax;
103
104     private volatile int remainingTimeSunset;
105
106     public SomneoHandler(Thing thing, HttpClientProvider httpClientProvider,
107             SomneoPresetStateDescriptionProvider provider) {
108         super(thing);
109         this.httpClientProvider = httpClientProvider;
110         this.provider = provider;
111         this.alarmPattern = Objects.requireNonNull(Pattern.compile(CHANNEL_ALARM_PREFIX_REGEX));
112     }
113
114     @Override
115     public void handleCommand(ChannelUID channelUID, Command command) {
116         String channelId = channelUID.getId();
117         logger.debug("Handle command '{}' for channel {}", command, channelId);
118
119         try {
120             final SomneoHttpConnector connector = this.connector;
121             if (connector == null) {
122                 return;
123             }
124
125             final Matcher matcher = alarmPattern.matcher(channelId);
126             int alarmPosition = 0;
127             if (matcher.matches()) {
128                 // Replace alarm channel index with string format placeholder to match
129                 // constants.
130                 alarmPosition = Integer.parseInt(matcher.group(1));
131                 channelId = channelId.replace(alarmPosition + "#", "%d#");
132             }
133
134             if (command instanceof RefreshType) {
135                 if (channelId.equals(CHANNEL_ALARM_SNOOZE)) {
136                     final State snooze = connector.fetchSnoozeDuration();
137                     updateState(CHANNEL_ALARM_SNOOZE, snooze);
138                 } else if (channelId.startsWith("alarm")) {
139                     updateAlarmExtended(alarmPosition);
140                 } else if (channelId.startsWith("sensor")) {
141                     updateSensors();
142                 } else if (channelId.startsWith("light")) {
143                     updateLights();
144                 } else if (channelId.equals(CHANNEL_RELAX_REMAINING_TIME)
145                         || channelId.equals(CHANNEL_SUNSET_REMAINING_TIME)) {
146                     updateRemainingTimer();
147                 } else if (channelId.equals(CHANNEL_AUDIO_FREQUENCY)) {
148                     updateFrequency();
149                 } else if (channelId.startsWith("audio")) {
150                     updateLights();
151                 } else if (channelId.startsWith("sunset")) {
152                     updateSunset();
153                 } else if (channelId.startsWith("relax")) {
154                     updateRelax();
155                 } else {
156                     this.poll();
157                 }
158                 return;
159             }
160
161             switch (channelId) {
162                 case CHANNEL_AUDIO_AUX:
163                     if (command instanceof OnOffType onOff) {
164                         boolean isOn = OnOffType.ON.equals(command);
165                         connector.switchAux(isOn);
166
167                         if (isOn) {
168                             updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PAUSE);
169                             updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
170                             updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
171                         }
172                     }
173                     break;
174                 case CHANNEL_AUDIO_PRESET:
175                     if (command instanceof StringType) {
176                         connector.setRadioChannel(command.toFullString());
177
178                         updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PLAY);
179                         updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
180                         updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
181                         updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
182
183                         updateFrequency();
184                     }
185                     break;
186                 case CHANNEL_AUDIO_RADIO:
187                     if (command instanceof PlayPauseType playPause) {
188                         boolean isPlaying = PlayPauseType.PLAY.equals(command);
189                         connector.switchRadio(isPlaying);
190
191                         if (isPlaying) {
192                             updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
193                             updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
194                             updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
195                         }
196                     } else if (command instanceof NextPreviousType && NextPreviousType.NEXT.equals(command)) {
197                         connector.radioSeekUp();
198
199                         updateFrequency();
200                     } else if (command instanceof NextPreviousType && NextPreviousType.PREVIOUS.equals(command)) {
201                         connector.radioSeekDown();
202
203                         updateFrequency();
204                     }
205                     break;
206                 case CHANNEL_AUDIO_VOLUME:
207                     if (command instanceof PercentType percent) {
208                         connector.setAudioVolume(percent.intValue());
209                     }
210                     break;
211                 case CHANNEL_LIGHT_MAIN:
212                     if (command instanceof OnOffType) {
213                         boolean isOn = OnOffType.ON.equals(command);
214                         connector.switchMainLight(isOn);
215
216                         if (isOn) {
217                             updateState(CHANNEL_LIGHT_MAIN, new PercentType(lastLightBrightness));
218                             updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
219                             updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
220                             updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
221                         }
222                     }
223                     if (command instanceof PercentType percent) {
224                         int level = percent.intValue();
225
226                         if (level > 0) {
227                             connector.setMainLightDimmer(level);
228                             lastLightBrightness = level;
229
230                             updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
231                             updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
232                             updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
233                         } else {
234                             connector.switchMainLight(false);
235                         }
236                     }
237                     break;
238                 case CHANNEL_LIGHT_NIGHT:
239                     if (command instanceof OnOffType) {
240                         boolean isOn = OnOffType.ON.equals(command);
241                         connector.switchNightLight(isOn);
242
243                         if (isOn) {
244                             updateState(CHANNEL_LIGHT_MAIN, OnOffType.OFF);
245                             updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
246                             updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
247                         }
248                     }
249                     break;
250                 case CHANNEL_RELAX_BREATHING_RATE:
251                     if (command instanceof DecimalType decimal) {
252                         connector.setRelaxBreathingRate(decimal.intValue());
253                     }
254                     break;
255                 case CHANNEL_RELAX_DURATION:
256                     if (command instanceof QuantityType quantity) {
257                         connector.setRelaxDuration(quantity.intValue());
258                     }
259                     break;
260                 case CHANNEL_RELAX_GUIDANCE_TYPE:
261                     if (command instanceof DecimalType decimal) {
262                         connector.setRelaxGuidanceType(decimal.intValue());
263                     }
264                     break;
265                 case CHANNEL_RELAX_LIGHT_INTENSITY:
266                     if (command instanceof PercentType percent) {
267                         connector.setRelaxLightIntensity(percent.intValue());
268                     }
269                     break;
270                 case CHANNEL_RELAX_SWITCH:
271                     if (command instanceof OnOffType) {
272                         boolean isOn = OnOffType.ON.equals(command);
273                         connector.switchRelaxProgram(isOn);
274
275                         updateRemainingTimer();
276
277                         if (isOn) {
278                             updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
279                             updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PAUSE);
280                             updateState(CHANNEL_LIGHT_MAIN, OnOffType.OFF);
281                             updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
282                             updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
283                         }
284                     }
285                     break;
286                 case CHANNEL_RELAX_VOLUME:
287                     if (command instanceof PercentType percent) {
288                         connector.setRelaxVolume(percent.intValue());
289                     }
290                     break;
291                 case CHANNEL_SUNSET_AMBIENT_NOISE:
292                     if (command instanceof StringType) {
293                         connector.setSunsetAmbientNoise(command.toFullString());
294                     }
295                     break;
296                 case CHANNEL_SUNSET_COLOR_SCHEMA:
297                     if (command instanceof DecimalType decimal) {
298                         connector.setSunsetColorSchema(decimal.intValue());
299                     }
300                     break;
301                 case CHANNEL_SUNSET_DURATION:
302                     if (command instanceof QuantityType quantity) {
303                         connector.setSunsetDuration(quantity.intValue());
304                     }
305                     break;
306                 case CHANNEL_SUNSET_LIGHT_INTENSITY:
307                     if (command instanceof PercentType percent) {
308                         connector.setSunsetLightIntensity(percent.intValue());
309                     }
310                     break;
311                 case CHANNEL_SUNSET_SWITCH:
312                     if (command instanceof OnOffType) {
313                         boolean isOn = OnOffType.ON.equals(command);
314                         connector.switchSunsetProgram(isOn);
315
316                         updateRemainingTimer();
317
318                         if (isOn) {
319                             updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
320                             updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PAUSE);
321                             updateState(CHANNEL_LIGHT_MAIN, OnOffType.OFF);
322                             updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
323                             updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
324                         }
325                     }
326                     break;
327                 case CHANNEL_SUNSET_VOLUME:
328                     if (command instanceof PercentType percent) {
329                         connector.setSunsetVolume(percent.intValue());
330                     }
331                     break;
332                 case CHANNEL_ALARM_SNOOZE:
333                     if (command instanceof QuantityType quantity) {
334                         connector.setAlarmSnooze(quantity.intValue());
335                     }
336                     break;
337                 case CHANNEL_ALARM_CONFIGURED:
338                     if (alarmPosition > 2) {
339                         if (command instanceof OnOffType onOff) {
340                             connector.toggleAlarmConfiguration(alarmPosition, onOff);
341
342                             if (OnOffType.ON.equals(command)) {
343                                 updateAlarmExtended(alarmPosition);
344                             } else {
345                                 resetAlarm(alarmPosition);
346                             }
347                         }
348                     } else {
349                         logger.info("Alarm 1 and 2 can not be unset");
350                     }
351                     break;
352                 case CHANNEL_ALARM_SWITCH:
353                     if (command instanceof OnOffType onOff) {
354                         connector.toggleAlarm(alarmPosition, onOff);
355                         updateAlarmExtended(alarmPosition);
356                     }
357                     break;
358                 case CHANNEL_ALARM_TIME:
359                     if (command instanceof DateTimeType decimal) {
360                         connector.setAlarmTime(alarmPosition, decimal);
361                     }
362                     break;
363                 case CHANNEL_ALARM_REPEAT_DAY:
364                     if (command instanceof DecimalType decimal) {
365                         connector.setAlarmRepeatDay(alarmPosition, decimal);
366                     }
367                     break;
368                 case CHANNEL_ALARM_POWER_WAKE:
369                     if (command instanceof OnOffType onOff) {
370                         connector.toggleAlarmPowerWake(alarmPosition, onOff);
371                         if (OnOffType.OFF.equals(command)) {
372                             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_POWER_WAKE_DELAY, alarmPosition),
373                                     QuantityType.valueOf(0, Units.MINUTE));
374                         }
375                     }
376                     break;
377                 case CHANNEL_ALARM_POWER_WAKE_DELAY:
378                     if (command instanceof QuantityType quantity) {
379                         connector.setAlarmPowerWakeDelay(alarmPosition, quantity);
380                         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_POWER_WAKE, alarmPosition), OnOffType.ON);
381                     }
382                     break;
383                 case CHANNEL_ALARM_SUNRISE_DURATION:
384                     if (command instanceof QuantityType quantity) {
385                         connector.setAlarmSunriseDuration(alarmPosition, quantity);
386                     }
387                     break;
388                 case CHANNEL_ALARM_SUNRISE_BRIGHTNESS:
389                     if (command instanceof PercentType percent) {
390                         connector.setAlarmSunriseBrightness(alarmPosition, percent);
391                     }
392                     break;
393                 case CHANNEL_ALARM_SUNRISE_SCHEMA:
394                     if (command instanceof DecimalType decimal) {
395                         connector.setAlarmSunriseSchema(alarmPosition, decimal);
396                     }
397                     break;
398                 case CHANNEL_ALARM_SOUND:
399                     if (command instanceof StringType) {
400                         connector.setAlarmSound(alarmPosition, (StringType) command);
401                     }
402                     break;
403                 case CHANNEL_ALARM_VOLUME:
404                     if (command instanceof PercentType percent) {
405                         connector.setAlarmVolume(alarmPosition, percent);
406                     }
407                     break;
408                 default:
409                     logger.warn("Received unknown channel {}", channelId);
410                     break;
411             }
412         } catch (InterruptedException e) {
413             logger.debug("Handle command interrupted");
414             Thread.currentThread().interrupt();
415         } catch (TimeoutException | ExecutionException e) {
416             if (e.getCause() instanceof EOFException) {
417                 // Occurs on parallel mobile app access
418                 logger.debug("EOF: {}", e.getMessage());
419             } else {
420                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
421             }
422         }
423     }
424
425     @Override
426     public void initialize() {
427         updateStatus(ThingStatus.UNKNOWN);
428
429         initConnector();
430         updateThingProperties();
431         startPolling();
432     }
433
434     @Override
435     public void dispose() {
436         stopPolling();
437         stopRemainingTimer();
438
439         super.dispose();
440     }
441
442     private void initConnector() {
443         if (connector == null) {
444             SomneoConfiguration config = getConfigAs(SomneoConfiguration.class);
445             HttpClient httpClient;
446             if (config.ignoreSSLErrors) {
447                 logger.info("Using the insecure client for thing '{}'.", thing.getUID());
448                 httpClient = httpClientProvider.getInsecureClient();
449             } else {
450                 logger.info("Using the secure client for thing '{}'.", thing.getUID());
451                 httpClient = httpClientProvider.getSecureClient();
452             }
453
454             connector = new SomneoHttpConnector(config, httpClient);
455         }
456     }
457
458     private void updateThingProperties() {
459         final SomneoHttpConnector connector = this.connector;
460         if (connector == null) {
461             return;
462         }
463
464         Map<String, String> properties = editProperties();
465         properties.put(Thing.PROPERTY_VENDOR, PROPERTY_VENDOR_NAME);
466
467         try {
468             final DeviceData deviceData = connector.fetchDeviceData();
469             String value = deviceData.getModelId();
470             if (value != null) {
471                 properties.put(Thing.PROPERTY_MODEL_ID, value);
472             }
473             value = deviceData.getSerial();
474             if (value != null) {
475                 properties.put(Thing.PROPERTY_SERIAL_NUMBER, value);
476             }
477
478             final WifiData wifiData = connector.fetchWifiData();
479             value = wifiData.getMacAddress();
480             if (value != null) {
481                 properties.put(Thing.PROPERTY_MAC_ADDRESS, value);
482             }
483
484             final FirmwareData firmwareData = connector.fetchFirmwareData();
485             value = firmwareData.getVersion();
486             if (value != null) {
487                 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, value);
488             }
489
490             updateProperties(properties);
491         } catch (InterruptedException e) {
492             logger.debug("Update properties interrupted");
493             Thread.currentThread().interrupt();
494         } catch (TimeoutException | ExecutionException e) {
495             if (e.getCause() instanceof EOFException) {
496                 // Occurs on parallel mobile app access
497                 logger.debug("EOF: {}", e.getMessage());
498             } else {
499                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
500             }
501         }
502     }
503
504     /**
505      * Set up the connection to the receiver by starting to poll the HTTP API.
506      */
507     private void startPolling() {
508         final ScheduledFuture<?> pollingJob = this.pollingJob;
509         if (pollingJob != null && !pollingJob.isCancelled()) {
510             return;
511         }
512
513         final SomneoConfiguration config = getConfigAs(SomneoConfiguration.class);
514         final int refreshInterval = config.refreshInterval;
515         logger.debug("Start default polling job at interval {}s", refreshInterval);
516         this.pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, refreshInterval, TimeUnit.SECONDS);
517
518         final int refreshIntervalAlarmExtended = config.refreshIntervalAlarmExtended;
519         logger.debug("Start extended alarm polling job at interval {}s", refreshIntervalAlarmExtended);
520         this.pollingJobExtended = scheduler.scheduleWithFixedDelay(this::pollAlarmExtended, 0,
521                 refreshIntervalAlarmExtended, TimeUnit.SECONDS);
522     }
523
524     private void stopPolling() {
525         final ScheduledFuture<?> pollingJob = this.pollingJob;
526         if (pollingJob != null) {
527             pollingJob.cancel(true);
528             this.pollingJob = null;
529         }
530
531         final ScheduledFuture<?> pollingJobExtended = this.pollingJobExtended;
532         if (pollingJobExtended != null) {
533             pollingJobExtended.cancel(true);
534             this.pollingJobExtended = null;
535         }
536
537         logger.debug("HTTP polling stopped.");
538     }
539
540     private void poll() {
541         final SomneoHttpConnector connector = this.connector;
542         if (connector == null) {
543             return;
544         }
545
546         try {
547             updateSensors();
548
549             updateLights();
550
551             updateSunset();
552
553             updateRelax();
554
555             updateAudio();
556
557             updateFrequency();
558
559             updateAlarm();
560
561             updateRemainingTimer();
562
563             updateStatus(ThingStatus.ONLINE);
564         } catch (InterruptedException e) {
565             logger.debug("Polling data interrupted");
566             Thread.currentThread().interrupt();
567         } catch (TimeoutException | ExecutionException e) {
568             if (e.getCause() instanceof EOFException) {
569                 // Occurs on parallel mobile app access
570                 logger.debug("EOF: {}", e.getMessage());
571             } else {
572                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
573             }
574         }
575     }
576
577     private void updateAudio() throws TimeoutException, InterruptedException, ExecutionException {
578         final SomneoHttpConnector connector = this.connector;
579         if (connector == null) {
580             return;
581         }
582         final AudioData audioData = connector.fetchAudioData();
583         updateState(CHANNEL_AUDIO_RADIO, audioData.getRadioState());
584         updateState(CHANNEL_AUDIO_AUX, audioData.getAuxState());
585         updateState(CHANNEL_AUDIO_VOLUME, audioData.getVolumeState());
586         updateState(CHANNEL_AUDIO_PRESET, audioData.getPresetState());
587     }
588
589     private void updateRelax() throws TimeoutException, InterruptedException, ExecutionException {
590         final SomneoHttpConnector connector = this.connector;
591         if (connector == null) {
592             return;
593         }
594         final RelaxData relaxData = connector.fetchRelaxData();
595         updateState(CHANNEL_RELAX_SWITCH, relaxData.getSwitchState());
596         updateState(CHANNEL_RELAX_BREATHING_RATE, relaxData.getBreathingRate());
597         updateState(CHANNEL_RELAX_DURATION, relaxData.getDurationInMin());
598         updateState(CHANNEL_RELAX_GUIDANCE_TYPE, relaxData.getGuidanceType());
599         updateState(CHANNEL_RELAX_LIGHT_INTENSITY, relaxData.getLightIntensity());
600         updateState(CHANNEL_RELAX_VOLUME, relaxData.getSoundVolume());
601     }
602
603     private void updateSunset() throws TimeoutException, InterruptedException, ExecutionException {
604         final SomneoHttpConnector connector = this.connector;
605         if (connector == null) {
606             return;
607         }
608         final SunsetData sunsetData = connector.fetchSunsetData();
609         updateState(CHANNEL_SUNSET_SWITCH, sunsetData.getSwitchState());
610         updateState(CHANNEL_SUNSET_LIGHT_INTENSITY, sunsetData.getLightIntensity());
611         updateState(CHANNEL_SUNSET_DURATION, sunsetData.getDurationInMin());
612         updateState(CHANNEL_SUNSET_COLOR_SCHEMA, sunsetData.getColorSchema());
613         updateState(CHANNEL_SUNSET_AMBIENT_NOISE, sunsetData.getAmbientNoise());
614         updateState(CHANNEL_SUNSET_VOLUME, sunsetData.getSoundVolume());
615     }
616
617     private void updateLights() throws TimeoutException, InterruptedException, ExecutionException {
618         final SomneoHttpConnector connector = this.connector;
619         if (connector == null) {
620             return;
621         }
622         final LightData lightData = connector.fetchLightData();
623         updateState(CHANNEL_LIGHT_MAIN, lightData.getMainLightState());
624         updateState(CHANNEL_LIGHT_NIGHT, lightData.getNightLightState());
625         lastLightBrightness = lightData.getMainLightLevel();
626     }
627
628     private void updateSensors() throws TimeoutException, InterruptedException, ExecutionException {
629         final SomneoHttpConnector connector = this.connector;
630         if (connector == null) {
631             return;
632         }
633
634         final SensorData sensorData = connector.fetchSensorData();
635         updateState(CHANNEL_SENSOR_HUMIDITY, sensorData.getCurrentHumidity());
636         updateState(CHANNEL_SENSOR_ILLUMINANCE, sensorData.getCurrentIlluminance());
637         updateState(CHANNEL_SENSOR_NOISE, sensorData.getCurrentNoise());
638         updateState(CHANNEL_SENSOR_TEMPERATURE, sensorData.getCurrentTemperature());
639     }
640
641     private void updateFrequency() throws TimeoutException, InterruptedException, ExecutionException {
642         final SomneoHttpConnector connector = this.connector;
643         if (connector == null) {
644             return;
645         }
646
647         RadioData radioData = connector.getRadioData();
648         updateState(CHANNEL_AUDIO_FREQUENCY, radioData.getFrequency());
649
650         final PresetData presetData = connector.fetchPresetData();
651         final Channel presetChannel = getThing().getChannel(CHANNEL_AUDIO_PRESET);
652         if (presetChannel != null) {
653             provider.setStateOptions(presetChannel.getUID(), presetData.createPresetOptions());
654         }
655     }
656
657     private void updateRemainingTimer() throws TimeoutException, InterruptedException, ExecutionException {
658         final SomneoHttpConnector connector = this.connector;
659         if (connector == null) {
660             return;
661         }
662
663         TimerData timerData = connector.fetchTimerData();
664
665         remainingTimeRelax = timerData.remainingTimeRelax();
666         remainingTimeSunset = timerData.remainingTimeSunset();
667
668         if (remainingTimeRelax > 0 || remainingTimeSunset > 0) {
669             startRemainingTimer();
670         } else {
671             State state = new QuantityType<>(0, Units.SECOND);
672             updateState(CHANNEL_RELAX_REMAINING_TIME, state);
673             updateState(CHANNEL_SUNSET_REMAINING_TIME, state);
674         }
675     }
676
677     private void startRemainingTimer() {
678         final ScheduledFuture<?> remainingTimerJob = this.remainingTimerJob;
679         if (remainingTimerJob != null && !remainingTimerJob.isCancelled()) {
680             return;
681         }
682
683         logger.debug("Start remaining timer ticker job");
684         this.remainingTimerJob = scheduler.scheduleWithFixedDelay(this::remainingTimerTick, 0, 1, TimeUnit.SECONDS);
685     }
686
687     private void stopRemainingTimer() {
688         final ScheduledFuture<?> remainingTimerJob = this.remainingTimerJob;
689         if (remainingTimerJob == null || remainingTimerJob.isCancelled()) {
690             return;
691         }
692
693         remainingTimerJob.cancel(true);
694         this.remainingTimerJob = null;
695         logger.debug("Remaining timer ticker stopped.");
696     }
697
698     private void remainingTimerTick() {
699         if (remainingTimeRelax > 0) {
700             remainingTimeRelax--;
701
702             State state = new QuantityType<>(remainingTimeRelax, Units.SECOND);
703             updateState(CHANNEL_RELAX_REMAINING_TIME, state);
704         }
705
706         if (remainingTimeSunset > 0) {
707             remainingTimeSunset--;
708
709             State state = new QuantityType<>(remainingTimeSunset, Units.SECOND);
710             updateState(CHANNEL_SUNSET_REMAINING_TIME, state);
711         }
712
713         if (remainingTimeRelax <= 0 && remainingTimeSunset <= 0) {
714             stopRemainingTimer();
715         }
716     }
717
718     private void updateAlarm() throws TimeoutException, InterruptedException, ExecutionException {
719         final SomneoHttpConnector connector = this.connector;
720         if (connector == null) {
721             return;
722         }
723
724         final State snooze = connector.fetchSnoozeDuration();
725         updateState(CHANNEL_ALARM_SNOOZE, snooze);
726
727         final AlarmStateData alarmState = connector.fetchAlarmStateData();
728         final AlarmSchedulesData alarmSchedulesData = connector.fetchAlarmScheduleData();
729
730         for (int i = 1; i <= alarmState.getAlarmCount(); i++) {
731             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_CONFIGURED, i), alarmState.getConfiguredState(i));
732
733             if (OnOffType.ON.equals(alarmState.getConfiguredState(i))) {
734                 updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_SWITCH, i), alarmState.getEnabledState(i));
735                 updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_TIME, i),
736                         alarmSchedulesData.getAlarmTimeState(i));
737                 updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_REPEAT_DAY, i),
738                         alarmSchedulesData.getRepeatDayState(i));
739                 updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_POWER_WAKE, i), alarmState.getPowerWakeState(i));
740                 updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_POWER_WAKE_DELAY, i),
741                         alarmState.getPowerWakeDelayState(i, alarmSchedulesData.getAlarmTime(i)));
742             } else {
743                 resetAlarm(i);
744             }
745         }
746     }
747
748     private void pollAlarmExtended() {
749         final SomneoHttpConnector connector = this.connector;
750         if (connector == null) {
751             return;
752         }
753
754         try {
755             final AlarmStateData alarmState = connector.fetchAlarmStateData();
756
757             for (int i = 1; i <= alarmState.getAlarmCount(); i++) {
758                 if (OnOffType.ON.equals(alarmState.getConfiguredState(i))) {
759                     updateAlarmExtended(i);
760                 } else {
761                     resetAlarm(i);
762                 }
763             }
764         } catch (InterruptedException e) {
765             logger.debug("Polling extended alarm data interrupted");
766             Thread.currentThread().interrupt();
767         } catch (TimeoutException | ExecutionException e) {
768             if (e.getCause() instanceof EOFException) {
769                 // Occurs on parallel mobile app access
770                 logger.debug("EOF: {}", e.getMessage());
771             } else {
772                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
773             }
774         }
775     }
776
777     private void updateAlarmExtended(int position) throws TimeoutException, InterruptedException, ExecutionException {
778         final SomneoHttpConnector connector = this.connector;
779         if (connector == null) {
780             return;
781         }
782
783         final AlarmSettingsData alarmSettings = connector.fetchAlarmSettingsData(position);
784
785         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_CONFIGURED, position),
786                 alarmSettings.getConfiguredState());
787
788         if (OnOffType.ON.equals(alarmSettings.getConfiguredState())) {
789             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_SWITCH, position), alarmSettings.getEnabledState());
790             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_POWER_WAKE, position),
791                     alarmSettings.getPowerWakeState());
792             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_POWER_WAKE_DELAY, position),
793                     alarmSettings.getPowerWakeDelayState());
794             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_TIME, position), alarmSettings.getAlarmTimeState());
795             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_REPEAT_DAY, position),
796                     alarmSettings.getRepeatDayState());
797             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_SUNRISE_DURATION, position),
798                     alarmSettings.getSunriseDurationInMin());
799             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_SUNRISE_BRIGHTNESS, position),
800                     alarmSettings.getSunriseBrightness());
801             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_SUNRISE_SCHEMA, position),
802                     alarmSettings.getSunriseSchema());
803             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_SOUND, position), alarmSettings.getSound());
804             updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_VOLUME, position), alarmSettings.getSoundVolume());
805         } else {
806             resetAlarm(position);
807         }
808     }
809
810     private void resetAlarm(int position) throws TimeoutException, InterruptedException, ExecutionException {
811         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_SWITCH, position), UnDefType.UNDEF);
812         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_POWER_WAKE, position), UnDefType.UNDEF);
813         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_POWER_WAKE_DELAY, position), UnDefType.UNDEF);
814         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_TIME, position), UnDefType.UNDEF);
815         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_REPEAT_DAY, position), UnDefType.UNDEF);
816         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_SUNRISE_DURATION, position), UnDefType.UNDEF);
817         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_SUNRISE_BRIGHTNESS, position), UnDefType.UNDEF);
818         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_SUNRISE_SCHEMA, position), UnDefType.UNDEF);
819         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_SOUND, position), UnDefType.UNDEF);
820         updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_VOLUME, position), UnDefType.UNDEF);
821     }
822
823     private String formatAlarmChannelIdByIndex(String channelId, int index) {
824         final String channelIdFormated = String.format(channelId, index);
825         if (channelIdFormated == null) {
826             return "";
827         }
828         return channelIdFormated;
829     }
830 }