]> git.basschouten.com Git - openhab-addons.git/blob
1530ea2d4f7dd1163537c2aab5c83c9e6c42649f
[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.concurrent.ExecutionException;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.TimeoutException;
23
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;
57
58 /**
59  * The {@link SomneoHandler} is responsible for handling commands, which are
60  * sent to one of the channels.
61  *
62  * @author Michael Myrcik - Initial contribution
63  */
64 @NonNullByDefault
65 public class SomneoHandler extends BaseThingHandler {
66
67     private final Logger logger = LoggerFactory.getLogger(SomneoHandler.class);
68
69     private final HttpClientProvider httpClientProvider;
70
71     private final SomneoPresetStateDescriptionProvider provider;
72
73     /**
74      * Job to poll data from the device.
75      */
76     private @Nullable ScheduledFuture<?> pollingJob;
77
78     /**
79      * Job to count down the remaining program time.
80      */
81     private @Nullable ScheduledFuture<?> remainingTimerJob;
82
83     private @Nullable SomneoHttpConnector connector;
84
85     /**
86      * Cache the last brightness level in order to know the correct level when the
87      * ON command is given.
88      */
89     private volatile int lastLightBrightness;
90
91     private volatile int remainingTimeRelax;
92
93     private volatile int remainingTimeSunset;
94
95     public SomneoHandler(Thing thing, HttpClientProvider httpClientProvider,
96             SomneoPresetStateDescriptionProvider provider) {
97         super(thing);
98         this.httpClientProvider = httpClientProvider;
99         this.provider = provider;
100     }
101
102     @Override
103     public void handleCommand(ChannelUID channelUID, Command command) {
104         String channelId = channelUID.getId();
105         logger.debug("Handle command '{}' for channel {}", command, channelId);
106
107         if (command instanceof RefreshType) {
108             this.poll();
109             return;
110         }
111
112         final SomneoHttpConnector connector = this.connector;
113         if (connector == null) {
114             return;
115         }
116
117         try {
118             switch (channelId) {
119                 case CHANNEL_AUDIO_AUX:
120                     if (command instanceof OnOffType) {
121                         boolean isOn = OnOffType.ON.equals(command);
122                         connector.switchAux(isOn);
123
124                         if (isOn) {
125                             updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PAUSE);
126                             updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
127                             updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
128                         }
129                     }
130                     break;
131                 case CHANNEL_AUDIO_PRESET:
132                     if (command instanceof StringType) {
133                         connector.setRadioChannel(command.toFullString());
134
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);
139
140                         updateFrequency();
141                     }
142                     break;
143                 case CHANNEL_AUDIO_RADIO:
144                     if (command instanceof PlayPauseType) {
145                         boolean isPlaying = PlayPauseType.PLAY.equals(command);
146                         connector.switchRadio(isPlaying);
147
148                         if (isPlaying) {
149                             updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
150                             updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
151                             updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
152                         }
153                     } else if (command instanceof NextPreviousType && NextPreviousType.NEXT.equals(command)) {
154                         connector.radioSeekUp();
155
156                         updateFrequency();
157                     } else if (command instanceof NextPreviousType && NextPreviousType.PREVIOUS.equals(command)) {
158                         connector.radioSeekDown();
159
160                         updateFrequency();
161                     }
162                     break;
163                 case CHANNEL_AUDIO_VOLUME:
164                     if (command instanceof PercentType) {
165                         connector.setAudioVolume(Integer.parseInt(command.toFullString()));
166                     }
167                     break;
168                 case CHANNEL_LIGHT_MAIN:
169                     if (command instanceof OnOffType) {
170                         boolean isOn = OnOffType.ON.equals(command);
171                         connector.switchMainLight(isOn);
172
173                         if (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);
178                         }
179                     }
180                     if (command instanceof PercentType) {
181                         int level = Integer.parseInt(command.toFullString());
182
183                         if (level > 0) {
184                             connector.setMainLightDimmer(level);
185                             lastLightBrightness = level;
186
187                             updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
188                             updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
189                             updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
190                         } else {
191                             connector.switchMainLight(false);
192                         }
193                     }
194                     break;
195                 case CHANNEL_LIGHT_NIGHT:
196                     if (command instanceof OnOffType) {
197                         boolean isOn = OnOffType.ON.equals(command);
198                         connector.switchNightLight(isOn);
199
200                         if (isOn) {
201                             updateState(CHANNEL_LIGHT_MAIN, OnOffType.OFF);
202                             updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
203                             updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
204                         }
205                     }
206                     break;
207                 case CHANNEL_RELAX_BREATHING_RATE:
208                     if (command instanceof DecimalType) {
209                         connector.setRelaxBreathingRate(Integer.parseInt(command.toFullString()));
210                     }
211                     break;
212                 case CHANNEL_RELAX_DURATION:
213                     if (command instanceof DecimalType) {
214                         connector.setRelaxDuration(Integer.parseInt(command.toFullString()));
215                     }
216                     break;
217                 case CHANNEL_RELAX_GUIDANCE_TYPE:
218                     if (command instanceof DecimalType) {
219                         connector.setRelaxGuidanceType(Integer.parseInt(command.toFullString()));
220                     }
221                     break;
222                 case CHANNEL_RELAX_LIGHT_INTENSITY:
223                     if (command instanceof PercentType) {
224                         connector.setRelaxLightIntensity(Integer.parseInt(command.toFullString()));
225                     }
226                     break;
227                 case CHANNEL_RELAX_SWITCH:
228                     if (command instanceof OnOffType) {
229                         boolean isOn = OnOffType.ON.equals(command);
230                         connector.switchRelaxProgram(isOn);
231
232                         updateRemainingTimer();
233
234                         if (isOn) {
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);
240                         }
241                     }
242                     break;
243                 case CHANNEL_RELAX_VOLUME:
244                     if (command instanceof PercentType) {
245                         connector.setRelaxVolume(Integer.parseInt(command.toFullString()));
246                     }
247                     break;
248                 case CHANNEL_SUNSET_AMBIENT_NOISE:
249                     if (command instanceof StringType) {
250                         connector.setSunsetAmbientNoise(command.toFullString());
251                     }
252                     break;
253                 case CHANNEL_SUNSET_COLOR_SCHEMA:
254                     if (command instanceof DecimalType) {
255                         connector.setSunsetColorSchema(Integer.parseInt(command.toFullString()));
256                     }
257                     break;
258                 case CHANNEL_SUNSET_DURATION:
259                     if (command instanceof DecimalType) {
260                         connector.setSunsetDuration(Integer.parseInt(command.toFullString()));
261                     }
262                     break;
263                 case CHANNEL_SUNSET_LIGHT_INTENSITY:
264                     if (command instanceof PercentType) {
265                         connector.setSunsetLightIntensity(Integer.parseInt(command.toFullString()));
266                     }
267                     break;
268                 case CHANNEL_SUNSET_SWITCH:
269                     if (command instanceof OnOffType) {
270                         boolean isOn = OnOffType.ON.equals(command);
271                         connector.switchSunsetProgram(isOn);
272
273                         updateRemainingTimer();
274
275                         if (isOn) {
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);
281                         }
282                     }
283                     break;
284                 case CHANNEL_SUNSET_VOLUME:
285                     if (command instanceof PercentType) {
286                         connector.setSunsetVolume(Integer.parseInt(command.toFullString()));
287                     }
288                     break;
289                 default:
290                     logger.warn("Received unknown channel {}", channelId);
291                     break;
292             }
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());
300             } else {
301                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
302             }
303         }
304     }
305
306     @Override
307     public void initialize() {
308         updateStatus(ThingStatus.UNKNOWN);
309
310         initConnector();
311         updateThingProperties();
312         startPolling();
313     }
314
315     @Override
316     public void dispose() {
317         stopPolling();
318         stopRemainingTimer();
319
320         super.dispose();
321     }
322
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();
330             } else {
331                 logger.info("Using the secure client for thing '{}'.", thing.getUID());
332                 httpClient = httpClientProvider.getSecureClient();
333             }
334
335             connector = new SomneoHttpConnector(config, httpClient);
336         }
337     }
338
339     private void updateThingProperties() {
340         final SomneoHttpConnector connector = this.connector;
341         if (connector == null) {
342             return;
343         }
344
345         Map<String, String> properties = editProperties();
346         properties.put(Thing.PROPERTY_VENDOR, PROPERTY_VENDOR_NAME);
347
348         try {
349             final DeviceData deviceData = connector.fetchDeviceData();
350             String value = deviceData.getModelId();
351             if (value != null) {
352                 properties.put(Thing.PROPERTY_MODEL_ID, value);
353             }
354             value = deviceData.getSerial();
355             if (value != null) {
356                 properties.put(Thing.PROPERTY_SERIAL_NUMBER, value);
357             }
358
359             final WifiData wifiData = connector.fetchWifiData();
360             value = wifiData.getMacAddress();
361             if (value != null) {
362                 properties.put(Thing.PROPERTY_MAC_ADDRESS, value);
363             }
364
365             final FirmwareData firmwareData = connector.fetchFirmwareData();
366             value = firmwareData.getVersion();
367             if (value != null) {
368                 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, value);
369             }
370
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());
379             } else {
380                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
381             }
382         }
383     }
384
385     /**
386      * Set up the connection to the receiver by starting to poll the HTTP API.
387      */
388     private void startPolling() {
389         final ScheduledFuture<?> pollingJob = this.pollingJob;
390         if (pollingJob != null && !pollingJob.isCancelled()) {
391             return;
392         }
393
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);
397     }
398
399     private void stopPolling() {
400         final ScheduledFuture<?> pollingJob = this.pollingJob;
401         if (pollingJob == null || pollingJob.isCancelled()) {
402             return;
403         }
404
405         pollingJob.cancel(true);
406         this.pollingJob = null;
407         logger.debug("HTTP polling stopped.");
408     }
409
410     private void poll() {
411         final SomneoHttpConnector connector = this.connector;
412         if (connector == null) {
413             return;
414         }
415
416         try {
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());
422
423             final LightData lightData = connector.fetchLightData();
424             updateState(CHANNEL_LIGHT_MAIN, lightData.getMainLightState());
425             updateState(CHANNEL_LIGHT_NIGHT, lightData.getNightLightState());
426             lastLightBrightness = lightData.getMainLightLevel();
427
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());
435
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());
443
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());
449
450             updateFrequency();
451
452             updateRemainingTimer();
453
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());
462             } else {
463                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
464             }
465         }
466     }
467
468     private void updateFrequency() throws TimeoutException, InterruptedException, ExecutionException {
469         final SomneoHttpConnector connector = this.connector;
470         if (connector == null) {
471             return;
472         }
473
474         RadioData radioData = connector.getRadioData();
475         updateState(CHANNEL_AUDIO_FREQUENCY, radioData.getFrequency());
476
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());
481         }
482     }
483
484     private void updateRemainingTimer() throws TimeoutException, InterruptedException, ExecutionException {
485         final SomneoHttpConnector connector = this.connector;
486         if (connector == null) {
487             return;
488         }
489
490         TimerData timerData = connector.fetchTimerData();
491
492         remainingTimeRelax = timerData.remainingTimeRelax();
493         remainingTimeSunset = timerData.remainingTimeSunset();
494
495         if (remainingTimeRelax > 0 || remainingTimeSunset > 0) {
496             startRemainingTimer();
497         } else {
498             State state = new QuantityType<>(0, Units.SECOND);
499             updateState(CHANNEL_RELAX_REMAINING_TIME, state);
500             updateState(CHANNEL_SUNSET_REMAINING_TIME, state);
501         }
502     }
503
504     private void startRemainingTimer() {
505         final ScheduledFuture<?> remainingTimerJob = this.remainingTimerJob;
506         if (remainingTimerJob != null && !remainingTimerJob.isCancelled()) {
507             return;
508         }
509
510         logger.debug("Start remaining timer ticker job");
511         this.remainingTimerJob = scheduler.scheduleWithFixedDelay(this::remainingTimerTick, 0, 1, TimeUnit.SECONDS);
512     }
513
514     private void stopRemainingTimer() {
515         final ScheduledFuture<?> remainingTimerJob = this.remainingTimerJob;
516         if (remainingTimerJob == null || remainingTimerJob.isCancelled()) {
517             return;
518         }
519
520         remainingTimerJob.cancel(true);
521         this.remainingTimerJob = null;
522         logger.debug("Remaining timer ticker stopped.");
523     }
524
525     private void remainingTimerTick() {
526         if (remainingTimeRelax > 0) {
527             remainingTimeRelax--;
528
529             State state = new QuantityType<>(remainingTimeRelax, Units.SECOND);
530             updateState(CHANNEL_RELAX_REMAINING_TIME, state);
531         }
532
533         if (remainingTimeSunset > 0) {
534             remainingTimeSunset--;
535
536             State state = new QuantityType<>(remainingTimeSunset, Units.SECOND);
537             updateState(CHANNEL_SUNSET_REMAINING_TIME, state);
538         }
539
540         if (remainingTimeRelax <= 0 && remainingTimeSunset <= 0) {
541             stopRemainingTimer();
542         }
543     }
544 }