2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.somneo.internal;
15 import static org.openhab.binding.somneo.internal.SomneoBindingConstants.*;
17 import java.io.EOFException;
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;
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;
67 * The {@link SomneoHandler} is responsible for handling commands, which are
68 * sent to one of the channels.
70 * @author Michael Myrcik - Initial contribution
73 public class SomneoHandler extends BaseThingHandler {
75 private final Logger logger = LoggerFactory.getLogger(SomneoHandler.class);
77 private final HttpClientProvider httpClientProvider;
79 private final SomneoPresetStateDescriptionProvider provider;
81 private final Pattern alarmPattern;
84 * Job to poll data from the device.
86 private @Nullable ScheduledFuture<?> pollingJob;
87 private @Nullable ScheduledFuture<?> pollingJobExtended;
90 * Job to count down the remaining program time.
92 private @Nullable ScheduledFuture<?> remainingTimerJob;
94 private @Nullable SomneoHttpConnector connector;
97 * Cache the last brightness level in order to know the correct level when the
98 * ON command is given.
100 private volatile int lastLightBrightness;
102 private volatile int remainingTimeRelax;
104 private volatile int remainingTimeSunset;
106 public SomneoHandler(Thing thing, HttpClientProvider httpClientProvider,
107 SomneoPresetStateDescriptionProvider provider) {
109 this.httpClientProvider = httpClientProvider;
110 this.provider = provider;
111 this.alarmPattern = Objects.requireNonNull(Pattern.compile(CHANNEL_ALARM_PREFIX_REGEX));
115 public void handleCommand(ChannelUID channelUID, Command command) {
116 String channelId = channelUID.getId();
117 logger.debug("Handle command '{}' for channel {}", command, channelId);
120 final SomneoHttpConnector connector = this.connector;
121 if (connector == null) {
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
130 alarmPosition = Integer.parseInt(matcher.group(1));
131 channelId = channelId.replace(alarmPosition + "#", "%d#");
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")) {
142 } else if (channelId.startsWith("light")) {
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)) {
149 } else if (channelId.startsWith("audio")) {
151 } else if (channelId.startsWith("sunset")) {
153 } else if (channelId.startsWith("relax")) {
162 case CHANNEL_AUDIO_AUX:
163 if (command instanceof OnOffType onOff) {
164 boolean isOn = OnOffType.ON.equals(command);
165 connector.switchAux(isOn);
168 updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PAUSE);
169 updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
170 updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
174 case CHANNEL_AUDIO_PRESET:
175 if (command instanceof StringType) {
176 connector.setRadioChannel(command.toFullString());
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);
186 case CHANNEL_AUDIO_RADIO:
187 if (command instanceof PlayPauseType playPause) {
188 boolean isPlaying = PlayPauseType.PLAY.equals(command);
189 connector.switchRadio(isPlaying);
192 updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
193 updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
194 updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
196 } else if (command instanceof NextPreviousType && NextPreviousType.NEXT.equals(command)) {
197 connector.radioSeekUp();
200 } else if (command instanceof NextPreviousType && NextPreviousType.PREVIOUS.equals(command)) {
201 connector.radioSeekDown();
206 case CHANNEL_AUDIO_VOLUME:
207 if (command instanceof PercentType percent) {
208 connector.setAudioVolume(percent.intValue());
211 case CHANNEL_LIGHT_MAIN:
212 if (command instanceof OnOffType) {
213 boolean isOn = OnOffType.ON.equals(command);
214 connector.switchMainLight(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);
223 if (command instanceof PercentType percent) {
224 int level = percent.intValue();
227 connector.setMainLightDimmer(level);
228 lastLightBrightness = level;
230 updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
231 updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
232 updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
234 connector.switchMainLight(false);
238 case CHANNEL_LIGHT_NIGHT:
239 if (command instanceof OnOffType) {
240 boolean isOn = OnOffType.ON.equals(command);
241 connector.switchNightLight(isOn);
244 updateState(CHANNEL_LIGHT_MAIN, OnOffType.OFF);
245 updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
246 updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
250 case CHANNEL_RELAX_BREATHING_RATE:
251 if (command instanceof DecimalType decimal) {
252 connector.setRelaxBreathingRate(decimal.intValue());
255 case CHANNEL_RELAX_DURATION:
256 if (command instanceof QuantityType quantity) {
257 connector.setRelaxDuration(quantity.intValue());
260 case CHANNEL_RELAX_GUIDANCE_TYPE:
261 if (command instanceof DecimalType decimal) {
262 connector.setRelaxGuidanceType(decimal.intValue());
265 case CHANNEL_RELAX_LIGHT_INTENSITY:
266 if (command instanceof PercentType percent) {
267 connector.setRelaxLightIntensity(percent.intValue());
270 case CHANNEL_RELAX_SWITCH:
271 if (command instanceof OnOffType) {
272 boolean isOn = OnOffType.ON.equals(command);
273 connector.switchRelaxProgram(isOn);
275 updateRemainingTimer();
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);
286 case CHANNEL_RELAX_VOLUME:
287 if (command instanceof PercentType percent) {
288 connector.setRelaxVolume(percent.intValue());
291 case CHANNEL_SUNSET_AMBIENT_NOISE:
292 if (command instanceof StringType) {
293 connector.setSunsetAmbientNoise(command.toFullString());
296 case CHANNEL_SUNSET_COLOR_SCHEMA:
297 if (command instanceof DecimalType decimal) {
298 connector.setSunsetColorSchema(decimal.intValue());
301 case CHANNEL_SUNSET_DURATION:
302 if (command instanceof QuantityType quantity) {
303 connector.setSunsetDuration(quantity.intValue());
306 case CHANNEL_SUNSET_LIGHT_INTENSITY:
307 if (command instanceof PercentType percent) {
308 connector.setSunsetLightIntensity(percent.intValue());
311 case CHANNEL_SUNSET_SWITCH:
312 if (command instanceof OnOffType) {
313 boolean isOn = OnOffType.ON.equals(command);
314 connector.switchSunsetProgram(isOn);
316 updateRemainingTimer();
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);
327 case CHANNEL_SUNSET_VOLUME:
328 if (command instanceof PercentType percent) {
329 connector.setSunsetVolume(percent.intValue());
332 case CHANNEL_ALARM_SNOOZE:
333 if (command instanceof QuantityType quantity) {
334 connector.setAlarmSnooze(quantity.intValue());
337 case CHANNEL_ALARM_CONFIGURED:
338 if (alarmPosition > 2) {
339 if (command instanceof OnOffType onOff) {
340 connector.toggleAlarmConfiguration(alarmPosition, onOff);
342 if (OnOffType.ON.equals(command)) {
343 updateAlarmExtended(alarmPosition);
345 resetAlarm(alarmPosition);
349 logger.info("Alarm 1 and 2 can not be unset");
352 case CHANNEL_ALARM_SWITCH:
353 if (command instanceof OnOffType onOff) {
354 connector.toggleAlarm(alarmPosition, onOff);
355 updateAlarmExtended(alarmPosition);
358 case CHANNEL_ALARM_TIME:
359 if (command instanceof DateTimeType decimal) {
360 connector.setAlarmTime(alarmPosition, decimal);
363 case CHANNEL_ALARM_REPEAT_DAY:
364 if (command instanceof DecimalType decimal) {
365 connector.setAlarmRepeatDay(alarmPosition, decimal);
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));
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);
383 case CHANNEL_ALARM_SUNRISE_DURATION:
384 if (command instanceof QuantityType quantity) {
385 connector.setAlarmSunriseDuration(alarmPosition, quantity);
388 case CHANNEL_ALARM_SUNRISE_BRIGHTNESS:
389 if (command instanceof PercentType percent) {
390 connector.setAlarmSunriseBrightness(alarmPosition, percent);
393 case CHANNEL_ALARM_SUNRISE_SCHEMA:
394 if (command instanceof DecimalType decimal) {
395 connector.setAlarmSunriseSchema(alarmPosition, decimal);
398 case CHANNEL_ALARM_SOUND:
399 if (command instanceof StringType stringCommand) {
400 connector.setAlarmSound(alarmPosition, stringCommand);
403 case CHANNEL_ALARM_VOLUME:
404 if (command instanceof PercentType percent) {
405 connector.setAlarmVolume(alarmPosition, percent);
409 logger.warn("Received unknown channel {}", channelId);
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());
420 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
426 public void initialize() {
427 updateStatus(ThingStatus.UNKNOWN);
430 updateThingProperties();
435 public void dispose() {
437 stopRemainingTimer();
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();
450 logger.info("Using the secure client for thing '{}'.", thing.getUID());
451 httpClient = httpClientProvider.getSecureClient();
454 connector = new SomneoHttpConnector(config, httpClient);
458 private void updateThingProperties() {
459 final SomneoHttpConnector connector = this.connector;
460 if (connector == null) {
464 Map<String, String> properties = editProperties();
465 properties.put(Thing.PROPERTY_VENDOR, PROPERTY_VENDOR_NAME);
468 final DeviceData deviceData = connector.fetchDeviceData();
469 String value = deviceData.getModelId();
471 properties.put(Thing.PROPERTY_MODEL_ID, value);
473 value = deviceData.getSerial();
475 properties.put(Thing.PROPERTY_SERIAL_NUMBER, value);
478 final WifiData wifiData = connector.fetchWifiData();
479 value = wifiData.getMacAddress();
481 properties.put(Thing.PROPERTY_MAC_ADDRESS, value);
484 final FirmwareData firmwareData = connector.fetchFirmwareData();
485 value = firmwareData.getVersion();
487 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, value);
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());
499 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
505 * Set up the connection to the receiver by starting to poll the HTTP API.
507 private void startPolling() {
508 final ScheduledFuture<?> pollingJob = this.pollingJob;
509 if (pollingJob != null && !pollingJob.isCancelled()) {
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);
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);
524 private void stopPolling() {
525 final ScheduledFuture<?> pollingJob = this.pollingJob;
526 if (pollingJob != null) {
527 pollingJob.cancel(true);
528 this.pollingJob = null;
531 final ScheduledFuture<?> pollingJobExtended = this.pollingJobExtended;
532 if (pollingJobExtended != null) {
533 pollingJobExtended.cancel(true);
534 this.pollingJobExtended = null;
537 logger.debug("HTTP polling stopped.");
540 private void poll() {
541 final SomneoHttpConnector connector = this.connector;
542 if (connector == null) {
561 updateRemainingTimer();
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());
572 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
577 private void updateAudio() throws TimeoutException, InterruptedException, ExecutionException {
578 final SomneoHttpConnector connector = this.connector;
579 if (connector == null) {
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());
589 private void updateRelax() throws TimeoutException, InterruptedException, ExecutionException {
590 final SomneoHttpConnector connector = this.connector;
591 if (connector == null) {
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());
603 private void updateSunset() throws TimeoutException, InterruptedException, ExecutionException {
604 final SomneoHttpConnector connector = this.connector;
605 if (connector == null) {
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());
617 private void updateLights() throws TimeoutException, InterruptedException, ExecutionException {
618 final SomneoHttpConnector connector = this.connector;
619 if (connector == null) {
622 final LightData lightData = connector.fetchLightData();
623 updateState(CHANNEL_LIGHT_MAIN, lightData.getMainLightState());
624 updateState(CHANNEL_LIGHT_NIGHT, lightData.getNightLightState());
625 lastLightBrightness = lightData.getMainLightLevel();
628 private void updateSensors() throws TimeoutException, InterruptedException, ExecutionException {
629 final SomneoHttpConnector connector = this.connector;
630 if (connector == null) {
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());
641 private void updateFrequency() throws TimeoutException, InterruptedException, ExecutionException {
642 final SomneoHttpConnector connector = this.connector;
643 if (connector == null) {
647 RadioData radioData = connector.getRadioData();
648 updateState(CHANNEL_AUDIO_FREQUENCY, radioData.getFrequency());
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());
657 private void updateRemainingTimer() throws TimeoutException, InterruptedException, ExecutionException {
658 final SomneoHttpConnector connector = this.connector;
659 if (connector == null) {
663 TimerData timerData = connector.fetchTimerData();
665 remainingTimeRelax = timerData.remainingTimeRelax();
666 remainingTimeSunset = timerData.remainingTimeSunset();
668 if (remainingTimeRelax > 0 || remainingTimeSunset > 0) {
669 startRemainingTimer();
671 State state = new QuantityType<>(0, Units.SECOND);
672 updateState(CHANNEL_RELAX_REMAINING_TIME, state);
673 updateState(CHANNEL_SUNSET_REMAINING_TIME, state);
677 private void startRemainingTimer() {
678 final ScheduledFuture<?> remainingTimerJob = this.remainingTimerJob;
679 if (remainingTimerJob != null && !remainingTimerJob.isCancelled()) {
683 logger.debug("Start remaining timer ticker job");
684 this.remainingTimerJob = scheduler.scheduleWithFixedDelay(this::remainingTimerTick, 0, 1, TimeUnit.SECONDS);
687 private void stopRemainingTimer() {
688 final ScheduledFuture<?> remainingTimerJob = this.remainingTimerJob;
689 if (remainingTimerJob == null || remainingTimerJob.isCancelled()) {
693 remainingTimerJob.cancel(true);
694 this.remainingTimerJob = null;
695 logger.debug("Remaining timer ticker stopped.");
698 private void remainingTimerTick() {
699 if (remainingTimeRelax > 0) {
700 remainingTimeRelax--;
702 State state = new QuantityType<>(remainingTimeRelax, Units.SECOND);
703 updateState(CHANNEL_RELAX_REMAINING_TIME, state);
706 if (remainingTimeSunset > 0) {
707 remainingTimeSunset--;
709 State state = new QuantityType<>(remainingTimeSunset, Units.SECOND);
710 updateState(CHANNEL_SUNSET_REMAINING_TIME, state);
713 if (remainingTimeRelax <= 0 && remainingTimeSunset <= 0) {
714 stopRemainingTimer();
718 private void updateAlarm() throws TimeoutException, InterruptedException, ExecutionException {
719 final SomneoHttpConnector connector = this.connector;
720 if (connector == null) {
724 final State snooze = connector.fetchSnoozeDuration();
725 updateState(CHANNEL_ALARM_SNOOZE, snooze);
727 final AlarmStateData alarmState = connector.fetchAlarmStateData();
728 final AlarmSchedulesData alarmSchedulesData = connector.fetchAlarmScheduleData();
730 for (int i = 1; i <= alarmState.getAlarmCount(); i++) {
731 updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_CONFIGURED, i), alarmState.getConfiguredState(i));
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)));
748 private void pollAlarmExtended() {
749 final SomneoHttpConnector connector = this.connector;
750 if (connector == null) {
755 final AlarmStateData alarmState = connector.fetchAlarmStateData();
757 for (int i = 1; i <= alarmState.getAlarmCount(); i++) {
758 if (OnOffType.ON.equals(alarmState.getConfiguredState(i))) {
759 updateAlarmExtended(i);
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());
772 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
777 private void updateAlarmExtended(int position) throws TimeoutException, InterruptedException, ExecutionException {
778 final SomneoHttpConnector connector = this.connector;
779 if (connector == null) {
783 final AlarmSettingsData alarmSettings = connector.fetchAlarmSettingsData(position);
785 updateState(formatAlarmChannelIdByIndex(CHANNEL_ALARM_CONFIGURED, position),
786 alarmSettings.getConfiguredState());
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());
806 resetAlarm(position);
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);
823 private String formatAlarmChannelIdByIndex(String channelId, int index) {
824 final String channelIdFormated = String.format(channelId, index);
825 if (channelIdFormated == null) {
828 return channelIdFormated;