2 * Copyright (c) 2010-2022 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.concurrent.ExecutionException;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.TimeoutException;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.openhab.binding.somneo.internal.model.AudioData;
28 import org.openhab.binding.somneo.internal.model.DeviceData;
29 import org.openhab.binding.somneo.internal.model.FirmwareData;
30 import org.openhab.binding.somneo.internal.model.LightData;
31 import org.openhab.binding.somneo.internal.model.PresetData;
32 import org.openhab.binding.somneo.internal.model.RadioData;
33 import org.openhab.binding.somneo.internal.model.RelaxData;
34 import org.openhab.binding.somneo.internal.model.SensorData;
35 import org.openhab.binding.somneo.internal.model.SunsetData;
36 import org.openhab.binding.somneo.internal.model.TimerData;
37 import org.openhab.binding.somneo.internal.model.WifiData;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.NextPreviousType;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.library.types.PercentType;
42 import org.openhab.core.library.types.PlayPauseType;
43 import org.openhab.core.library.types.QuantityType;
44 import org.openhab.core.library.types.StringType;
45 import org.openhab.core.library.unit.Units;
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.binding.BaseThingHandler;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.openhab.core.types.State;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
59 * The {@link SomneoHandler} is responsible for handling commands, which are
60 * sent to one of the channels.
62 * @author Michael Myrcik - Initial contribution
65 public class SomneoHandler extends BaseThingHandler {
67 private final Logger logger = LoggerFactory.getLogger(SomneoHandler.class);
69 private final HttpClientProvider httpClientProvider;
71 private final SomneoPresetStateDescriptionProvider provider;
74 * Job to poll data from the device.
76 private @Nullable ScheduledFuture<?> pollingJob;
79 * Job to count down the remaining program time.
81 private @Nullable ScheduledFuture<?> remainingTimerJob;
83 private @Nullable SomneoHttpConnector connector;
86 * Cache the last brightness level in order to know the correct level when the
87 * ON command is given.
89 private volatile int lastLightBrightness;
91 private volatile int remainingTimeRelax;
93 private volatile int remainingTimeSunset;
95 public SomneoHandler(Thing thing, HttpClientProvider httpClientProvider,
96 SomneoPresetStateDescriptionProvider provider) {
98 this.httpClientProvider = httpClientProvider;
99 this.provider = provider;
103 public void handleCommand(ChannelUID channelUID, Command command) {
104 String channelId = channelUID.getId();
105 logger.debug("Handle command '{}' for channel {}", command, channelId);
107 if (command instanceof RefreshType) {
112 final SomneoHttpConnector connector = this.connector;
113 if (connector == null) {
119 case CHANNEL_AUDIO_AUX:
120 if (command instanceof OnOffType) {
121 boolean isOn = OnOffType.ON.equals(command);
122 connector.switchAux(isOn);
125 updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PAUSE);
126 updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
127 updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
131 case CHANNEL_AUDIO_PRESET:
132 if (command instanceof StringType) {
133 connector.setRadioChannel(command.toFullString());
135 updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PLAY);
136 updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
137 updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
138 updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
143 case CHANNEL_AUDIO_RADIO:
144 if (command instanceof PlayPauseType) {
145 boolean isPlaying = PlayPauseType.PLAY.equals(command);
146 connector.switchRadio(isPlaying);
149 updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
150 updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
151 updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
153 } else if (command instanceof NextPreviousType && NextPreviousType.NEXT.equals(command)) {
154 connector.radioSeekUp();
157 } else if (command instanceof NextPreviousType && NextPreviousType.PREVIOUS.equals(command)) {
158 connector.radioSeekDown();
163 case CHANNEL_AUDIO_VOLUME:
164 if (command instanceof PercentType) {
165 connector.setAudioVolume(Integer.parseInt(command.toFullString()));
168 case CHANNEL_LIGHT_MAIN:
169 if (command instanceof OnOffType) {
170 boolean isOn = OnOffType.ON.equals(command);
171 connector.switchMainLight(isOn);
174 updateState(CHANNEL_LIGHT_MAIN, new PercentType(lastLightBrightness));
175 updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
176 updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
177 updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
180 if (command instanceof PercentType) {
181 int level = Integer.parseInt(command.toFullString());
184 connector.setMainLightDimmer(level);
185 lastLightBrightness = level;
187 updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
188 updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
189 updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
191 connector.switchMainLight(false);
195 case CHANNEL_LIGHT_NIGHT:
196 if (command instanceof OnOffType) {
197 boolean isOn = OnOffType.ON.equals(command);
198 connector.switchNightLight(isOn);
201 updateState(CHANNEL_LIGHT_MAIN, OnOffType.OFF);
202 updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
203 updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
207 case CHANNEL_RELAX_BREATHING_RATE:
208 if (command instanceof DecimalType) {
209 connector.setRelaxBreathingRate(Integer.parseInt(command.toFullString()));
212 case CHANNEL_RELAX_DURATION:
213 if (command instanceof DecimalType) {
214 connector.setRelaxDuration(Integer.parseInt(command.toFullString()));
217 case CHANNEL_RELAX_GUIDANCE_TYPE:
218 if (command instanceof DecimalType) {
219 connector.setRelaxGuidanceType(Integer.parseInt(command.toFullString()));
222 case CHANNEL_RELAX_LIGHT_INTENSITY:
223 if (command instanceof PercentType) {
224 connector.setRelaxLightIntensity(Integer.parseInt(command.toFullString()));
227 case CHANNEL_RELAX_SWITCH:
228 if (command instanceof OnOffType) {
229 boolean isOn = OnOffType.ON.equals(command);
230 connector.switchRelaxProgram(isOn);
232 updateRemainingTimer();
235 updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
236 updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PAUSE);
237 updateState(CHANNEL_LIGHT_MAIN, OnOffType.OFF);
238 updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
239 updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
243 case CHANNEL_RELAX_VOLUME:
244 if (command instanceof PercentType) {
245 connector.setRelaxVolume(Integer.parseInt(command.toFullString()));
248 case CHANNEL_SUNSET_AMBIENT_NOISE:
249 if (command instanceof StringType) {
250 connector.setSunsetAmbientNoise(command.toFullString());
253 case CHANNEL_SUNSET_COLOR_SCHEMA:
254 if (command instanceof DecimalType) {
255 connector.setSunsetColorSchema(Integer.parseInt(command.toFullString()));
258 case CHANNEL_SUNSET_DURATION:
259 if (command instanceof DecimalType) {
260 connector.setSunsetDuration(Integer.parseInt(command.toFullString()));
263 case CHANNEL_SUNSET_LIGHT_INTENSITY:
264 if (command instanceof PercentType) {
265 connector.setSunsetLightIntensity(Integer.parseInt(command.toFullString()));
268 case CHANNEL_SUNSET_SWITCH:
269 if (command instanceof OnOffType) {
270 boolean isOn = OnOffType.ON.equals(command);
271 connector.switchSunsetProgram(isOn);
273 updateRemainingTimer();
276 updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
277 updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PAUSE);
278 updateState(CHANNEL_LIGHT_MAIN, OnOffType.OFF);
279 updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
280 updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
284 case CHANNEL_SUNSET_VOLUME:
285 if (command instanceof PercentType) {
286 connector.setSunsetVolume(Integer.parseInt(command.toFullString()));
290 logger.warn("Received unknown channel {}", channelId);
293 } catch (InterruptedException e) {
294 logger.debug("Handle command interrupted");
295 Thread.currentThread().interrupt();
296 } catch (TimeoutException | ExecutionException e) {
297 if (e.getCause() instanceof EOFException) {
298 // Occurs on parallel mobile app access
299 logger.debug("EOF: {}", e.getMessage());
301 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
307 public void initialize() {
308 updateStatus(ThingStatus.UNKNOWN);
311 updateThingProperties();
316 public void dispose() {
318 stopRemainingTimer();
323 private void initConnector() {
324 if (connector == null) {
325 SomneoConfiguration config = getConfigAs(SomneoConfiguration.class);
326 HttpClient httpClient;
327 if (config.ignoreSSLErrors) {
328 logger.info("Using the insecure client for thing '{}'.", thing.getUID());
329 httpClient = httpClientProvider.getInsecureClient();
331 logger.info("Using the secure client for thing '{}'.", thing.getUID());
332 httpClient = httpClientProvider.getSecureClient();
335 connector = new SomneoHttpConnector(config, httpClient);
339 private void updateThingProperties() {
340 final SomneoHttpConnector connector = this.connector;
341 if (connector == null) {
345 Map<String, String> properties = editProperties();
346 properties.put(Thing.PROPERTY_VENDOR, PROPERTY_VENDOR_NAME);
349 final DeviceData deviceData = connector.fetchDeviceData();
350 String value = deviceData.getModelId();
352 properties.put(Thing.PROPERTY_MODEL_ID, value);
354 value = deviceData.getSerial();
356 properties.put(Thing.PROPERTY_SERIAL_NUMBER, value);
359 final WifiData wifiData = connector.fetchWifiData();
360 value = wifiData.getMacAddress();
362 properties.put(Thing.PROPERTY_MAC_ADDRESS, value);
365 final FirmwareData firmwareData = connector.fetchFirmwareData();
366 value = firmwareData.getVersion();
368 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, value);
371 updateProperties(properties);
372 } catch (InterruptedException e) {
373 logger.debug("Update properties interrupted");
374 Thread.currentThread().interrupt();
375 } catch (TimeoutException | ExecutionException e) {
376 if (e.getCause() instanceof EOFException) {
377 // Occurs on parallel mobile app access
378 logger.debug("EOF: {}", e.getMessage());
380 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
386 * Set up the connection to the receiver by starting to poll the HTTP API.
388 private void startPolling() {
389 final ScheduledFuture<?> pollingJob = this.pollingJob;
390 if (pollingJob != null && !pollingJob.isCancelled()) {
394 int refreshInterval = getConfigAs(SomneoConfiguration.class).refreshInterval;
395 logger.debug("Start polling job at interval {}s", refreshInterval);
396 this.pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, refreshInterval, TimeUnit.SECONDS);
399 private void stopPolling() {
400 final ScheduledFuture<?> pollingJob = this.pollingJob;
401 if (pollingJob == null || pollingJob.isCancelled()) {
405 pollingJob.cancel(true);
406 this.pollingJob = null;
407 logger.debug("HTTP polling stopped.");
410 private void poll() {
411 final SomneoHttpConnector connector = this.connector;
412 if (connector == null) {
417 final SensorData sensorData = connector.fetchSensorData();
418 updateState(CHANNEL_SENSOR_HUMIDITY, sensorData.getCurrentHumidity());
419 updateState(CHANNEL_SENSOR_ILLUMINANCE, sensorData.getCurrentIlluminance());
420 updateState(CHANNEL_SENSOR_NOISE, sensorData.getCurrentNoise());
421 updateState(CHANNEL_SENSOR_TEMPERATURE, sensorData.getCurrentTemperature());
423 final LightData lightData = connector.fetchLightData();
424 updateState(CHANNEL_LIGHT_MAIN, lightData.getMainLightState());
425 updateState(CHANNEL_LIGHT_NIGHT, lightData.getNightLightState());
426 lastLightBrightness = lightData.getMainLightLevel();
428 final SunsetData sunsetData = connector.fetchSunsetData();
429 updateState(CHANNEL_SUNSET_SWITCH, sunsetData.getSwitchState());
430 updateState(CHANNEL_SUNSET_LIGHT_INTENSITY, sunsetData.getLightIntensity());
431 updateState(CHANNEL_SUNSET_DURATION, sunsetData.getDurationInMin());
432 updateState(CHANNEL_SUNSET_COLOR_SCHEMA, sunsetData.getColorSchema());
433 updateState(CHANNEL_SUNSET_AMBIENT_NOISE, sunsetData.getAmbientNoise());
434 updateState(CHANNEL_SUNSET_VOLUME, sunsetData.getSoundVolume());
436 final RelaxData relaxData = connector.fetchRelaxData();
437 updateState(CHANNEL_RELAX_SWITCH, relaxData.getSwitchState());
438 updateState(CHANNEL_RELAX_BREATHING_RATE, relaxData.getBreathingRate());
439 updateState(CHANNEL_RELAX_DURATION, relaxData.getDurationInMin());
440 updateState(CHANNEL_RELAX_GUIDANCE_TYPE, relaxData.getGuidanceType());
441 updateState(CHANNEL_RELAX_LIGHT_INTENSITY, relaxData.getLightIntensity());
442 updateState(CHANNEL_RELAX_VOLUME, relaxData.getSoundVolume());
444 final AudioData audioData = connector.fetchAudioData();
445 updateState(CHANNEL_AUDIO_RADIO, audioData.getRadioState());
446 updateState(CHANNEL_AUDIO_AUX, audioData.getAuxState());
447 updateState(CHANNEL_AUDIO_VOLUME, audioData.getVolumeState());
448 updateState(CHANNEL_AUDIO_PRESET, audioData.getPresetState());
452 updateRemainingTimer();
454 updateStatus(ThingStatus.ONLINE);
455 } catch (InterruptedException e) {
456 logger.debug("Polling data interrupted");
457 Thread.currentThread().interrupt();
458 } catch (TimeoutException | ExecutionException e) {
459 if (e.getCause() instanceof EOFException) {
460 // Occurs on parallel mobile app access
461 logger.debug("EOF: {}", e.getMessage());
463 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
468 private void updateFrequency() throws TimeoutException, InterruptedException, ExecutionException {
469 final SomneoHttpConnector connector = this.connector;
470 if (connector == null) {
474 RadioData radioData = connector.getRadioData();
475 updateState(CHANNEL_AUDIO_FREQUENCY, radioData.getFrequency());
477 final PresetData presetData = connector.fetchPresetData();
478 final Channel presetChannel = getThing().getChannel(CHANNEL_AUDIO_PRESET);
479 if (presetChannel != null) {
480 provider.setStateOptions(presetChannel.getUID(), presetData.createPresetOptions());
484 private void updateRemainingTimer() throws TimeoutException, InterruptedException, ExecutionException {
485 final SomneoHttpConnector connector = this.connector;
486 if (connector == null) {
490 TimerData timerData = connector.fetchTimerData();
492 remainingTimeRelax = timerData.remainingTimeRelax();
493 remainingTimeSunset = timerData.remainingTimeSunset();
495 if (remainingTimeRelax > 0 || remainingTimeSunset > 0) {
496 startRemainingTimer();
498 State state = new QuantityType<>(0, Units.SECOND);
499 updateState(CHANNEL_RELAX_REMAINING_TIME, state);
500 updateState(CHANNEL_SUNSET_REMAINING_TIME, state);
504 private void startRemainingTimer() {
505 final ScheduledFuture<?> remainingTimerJob = this.remainingTimerJob;
506 if (remainingTimerJob != null && !remainingTimerJob.isCancelled()) {
510 logger.debug("Start remaining timer ticker job");
511 this.remainingTimerJob = scheduler.scheduleWithFixedDelay(this::remainingTimerTick, 0, 1, TimeUnit.SECONDS);
514 private void stopRemainingTimer() {
515 final ScheduledFuture<?> remainingTimerJob = this.remainingTimerJob;
516 if (remainingTimerJob == null || remainingTimerJob.isCancelled()) {
520 remainingTimerJob.cancel(true);
521 this.remainingTimerJob = null;
522 logger.debug("Remaining timer ticker stopped.");
525 private void remainingTimerTick() {
526 if (remainingTimeRelax > 0) {
527 remainingTimeRelax--;
529 State state = new QuantityType<>(remainingTimeRelax, Units.SECOND);
530 updateState(CHANNEL_RELAX_REMAINING_TIME, state);
533 if (remainingTimeSunset > 0) {
534 remainingTimeSunset--;
536 State state = new QuantityType<>(remainingTimeSunset, Units.SECOND);
537 updateState(CHANNEL_SUNSET_REMAINING_TIME, state);
540 if (remainingTimeRelax <= 0 && remainingTimeSunset <= 0) {
541 stopRemainingTimer();