]> git.basschouten.com Git - openhab-addons.git/blob
89339108f1be95180f44e936f15b4467097969b1
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.sensibo.internal.handler;
14
15 import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_CURRENT_HUMIDITY;
16 import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_CURRENT_TEMPERATURE;
17 import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_FAN_LEVEL;
18 import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_MASTER_SWITCH;
19 import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_MODE;
20 import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_SWING_MODE;
21 import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_TARGET_TEMPERATURE;
22 import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_TIMER;
23
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Locale;
30 import java.util.Map;
31 import java.util.Objects;
32 import java.util.Optional;
33 import java.util.stream.Collectors;
34
35 import javax.measure.IncommensurableException;
36 import javax.measure.UnconvertibleException;
37 import javax.measure.Unit;
38 import javax.measure.UnitConverter;
39 import javax.measure.quantity.Temperature;
40
41 import org.apache.commons.lang.StringUtils;
42 import org.apache.commons.lang.WordUtils;
43 import org.apache.commons.lang.builder.ToStringBuilder;
44 import org.apache.commons.lang.builder.ToStringStyle;
45 import org.eclipse.jdt.annotation.NonNullByDefault;
46 import org.eclipse.jdt.annotation.Nullable;
47 import org.openhab.binding.sensibo.internal.CallbackChannelsTypeProvider;
48 import org.openhab.binding.sensibo.internal.SensiboBindingConstants;
49 import org.openhab.binding.sensibo.internal.config.SensiboSkyConfiguration;
50 import org.openhab.binding.sensibo.internal.dto.poddetails.TemperatureDTO;
51 import org.openhab.binding.sensibo.internal.model.SensiboModel;
52 import org.openhab.binding.sensibo.internal.model.SensiboSky;
53 import org.openhab.core.library.types.DecimalType;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.library.types.QuantityType;
56 import org.openhab.core.library.types.StringType;
57 import org.openhab.core.library.unit.SIUnits;
58 import org.openhab.core.thing.Channel;
59 import org.openhab.core.thing.ChannelUID;
60 import org.openhab.core.thing.Thing;
61 import org.openhab.core.thing.ThingStatus;
62 import org.openhab.core.thing.ThingStatusDetail;
63 import org.openhab.core.thing.binding.ThingHandlerService;
64 import org.openhab.core.thing.binding.builder.ChannelBuilder;
65 import org.openhab.core.thing.type.ChannelType;
66 import org.openhab.core.thing.type.ChannelTypeBuilder;
67 import org.openhab.core.thing.type.ChannelTypeProvider;
68 import org.openhab.core.thing.type.ChannelTypeUID;
69 import org.openhab.core.thing.type.StateChannelTypeBuilder;
70 import org.openhab.core.types.Command;
71 import org.openhab.core.types.RefreshType;
72 import org.openhab.core.types.StateDescriptionFragmentBuilder;
73 import org.openhab.core.types.StateOption;
74 import org.openhab.core.types.UnDefType;
75 import org.slf4j.Logger;
76 import org.slf4j.LoggerFactory;
77
78 import tec.uom.se.unit.Units;
79
80 /**
81  * The {@link SensiboSkyHandler} is responsible for handling commands, which are
82  * sent to one of the channels.
83  *
84  * @author Arne Seime - Initial contribution
85  */
86 @NonNullByDefault
87 public class SensiboSkyHandler extends SensiboBaseThingHandler implements ChannelTypeProvider {
88     public static final String SWING_PROPERTY = "swing";
89     public static final String MASTER_SWITCH_PROPERTY = "on";
90     public static final String FAN_LEVEL_PROPERTY = "fanLevel";
91     public static final String MODE_PROPERTY = "mode";
92     public static final String TARGET_TEMPERATURE_PROPERTY = "targetTemperature";
93     public static final String SWING_MODE_LABEL = "Swing Mode";
94     public static final String FAN_LEVEL_LABEL = "Fan Level";
95     public static final String MODE_LABEL = "Mode";
96     public static final String TARGET_TEMPERATURE_LABEL = "Target Temperature";
97     private static final String ITEM_TYPE_STRING = "String";
98     private static final String ITEM_TYPE_NUMBER_TEMPERATURE = "Number:Temperature";
99     private final Logger logger = LoggerFactory.getLogger(SensiboSkyHandler.class);
100     private final Map<ChannelTypeUID, ChannelType> generatedChannelTypes = new HashMap<>();
101     private Optional<SensiboSkyConfiguration> config = Optional.empty();
102
103     public SensiboSkyHandler(final Thing thing) {
104         super(thing);
105     }
106
107     private static String beautify(final String camelCaseWording) {
108         final StringBuilder b = new StringBuilder();
109         for (final String s : StringUtils.splitByCharacterTypeCamelCase(camelCaseWording)) {
110             b.append(" ");
111             b.append(s);
112         }
113         final StringBuilder bs = new StringBuilder();
114         for (final String t : StringUtils.splitByWholeSeparator(b.toString(), " _")) {
115             bs.append(" ");
116             bs.append(t);
117         }
118
119         return WordUtils.capitalizeFully(bs.toString()).trim();
120     }
121
122     private String getMacAddress() {
123         if (config.isPresent()) {
124             return config.get().macAddress;
125         }
126         throw new IllegalArgumentException("No configuration present");
127     }
128
129     @Override
130     public void handleCommand(final ChannelUID channelUID, final Command command) {
131         handleCommand(channelUID, command, getSensiboModel());
132     }
133
134     /*
135      * Package private in order to be reachable from unit test
136      */
137     void updateAcState(SensiboSky sensiboSky, String property, Object value) {
138         StateChange stateChange = checkStateChangeValid(sensiboSky, property, value);
139         if (stateChange.valid) {
140             getAccountHandler().ifPresent(
141                     handler -> handler.updateSensiboSkyAcState(getMacAddress(), property, stateChange.value, this));
142         } else {
143             logger.info("Update command not sent; invalid state change for SensiboSky AC state: {}",
144                     stateChange.validationMessage);
145         }
146     }
147
148     private void updateTimer(@Nullable Integer secondsFromNowUntilSwitchOff) {
149         getAccountHandler()
150                 .ifPresent(handler -> handler.updateSensiboSkyTimer(getMacAddress(), secondsFromNowUntilSwitchOff));
151     }
152
153     @Override
154     protected void handleCommand(final ChannelUID channelUID, final Command command, final SensiboModel model) {
155         model.findSensiboSkyByMacAddress(getMacAddress()).ifPresent(sensiboSky -> {
156             if (sensiboSky.isAlive()) {
157                 if (getThing().getStatus() != ThingStatus.ONLINE) {
158                     addDynamicChannelsAndProperties(sensiboSky);
159                     updateStatus(ThingStatus.ONLINE); // In case it has been offline
160                 }
161                 switch (channelUID.getId()) {
162                     case CHANNEL_CURRENT_HUMIDITY:
163                         handleCurrentHumidityCommand(channelUID, command, sensiboSky);
164                         break;
165                     case CHANNEL_CURRENT_TEMPERATURE:
166                         handleCurrentTemperatureCommand(channelUID, command, sensiboSky);
167                         break;
168                     case CHANNEL_MASTER_SWITCH:
169                         handleMasterSwitchCommand(channelUID, command, sensiboSky);
170                         break;
171                     case CHANNEL_TARGET_TEMPERATURE:
172                         handleTargetTemperatureCommand(channelUID, command, sensiboSky);
173                         break;
174                     case CHANNEL_MODE:
175                         handleModeCommand(channelUID, command, sensiboSky);
176                         break;
177                     case CHANNEL_SWING_MODE:
178                         handleSwingCommand(channelUID, command, sensiboSky);
179                         break;
180                     case CHANNEL_FAN_LEVEL:
181                         handleFanLevelCommand(channelUID, command, sensiboSky);
182                         break;
183                     case CHANNEL_TIMER:
184                         handleTimerCommand(channelUID, command, sensiboSky);
185                         break;
186                     default:
187                         logger.debug("Received command on unknown channel {}, ignoring", channelUID.getId());
188                 }
189             } else {
190                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
191                         "Unreachable by Sensibo servers");
192             }
193         });
194     }
195
196     private void handleTimerCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
197         if (command instanceof RefreshType) {
198             if (sensiboSky.getTimer().isPresent() && sensiboSky.getTimer().get().secondsRemaining > 0) {
199                 updateState(channelUID, new DecimalType(sensiboSky.getTimer().get().secondsRemaining));
200             } else {
201                 updateState(channelUID, UnDefType.UNDEF);
202             }
203         } else if (command instanceof DecimalType) {
204             final DecimalType newValue = (DecimalType) command;
205             updateTimer(newValue.intValue());
206         } else {
207             updateTimer(null);
208         }
209     }
210
211     private void handleFanLevelCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
212         if (command instanceof RefreshType) {
213             if (sensiboSky.getAcState().isPresent() && sensiboSky.getAcState().get().getFanLevel() != null) {
214                 updateState(channelUID, new StringType(sensiboSky.getAcState().get().getFanLevel()));
215             } else {
216                 updateState(channelUID, UnDefType.UNDEF);
217             }
218         } else if (command instanceof StringType) {
219             final StringType newValue = (StringType) command;
220             updateAcState(sensiboSky, FAN_LEVEL_PROPERTY, newValue.toString());
221         }
222     }
223
224     private void handleSwingCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
225         if (command instanceof RefreshType && sensiboSky.getAcState().isPresent()) {
226             if (sensiboSky.getAcState().isPresent() && sensiboSky.getAcState().get().getSwing() != null) {
227                 updateState(channelUID, new StringType(sensiboSky.getAcState().get().getSwing()));
228             } else {
229                 updateState(channelUID, UnDefType.UNDEF);
230             }
231         } else if (command instanceof StringType) {
232             final StringType newValue = (StringType) command;
233             updateAcState(sensiboSky, SWING_PROPERTY, newValue.toString());
234         }
235     }
236
237     private void handleModeCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
238         if (command instanceof RefreshType) {
239             if (sensiboSky.getAcState().isPresent()) {
240                 updateState(channelUID, new StringType(sensiboSky.getAcState().get().getMode()));
241             } else {
242                 updateState(channelUID, UnDefType.UNDEF);
243             }
244         } else if (command instanceof StringType) {
245             final StringType newValue = (StringType) command;
246             updateAcState(sensiboSky, MODE_PROPERTY, newValue.toString());
247             addDynamicChannelsAndProperties(sensiboSky);
248         }
249     }
250
251     private void handleTargetTemperatureCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
252         if (command instanceof RefreshType) {
253             sensiboSky.getAcState().ifPresent(acState -> {
254                 @Nullable
255                 Integer targetTemperature = acState.getTargetTemperature();
256                 if (targetTemperature != null) {
257                     updateState(channelUID, new QuantityType<>(targetTemperature, sensiboSky.getTemperatureUnit()));
258                 } else {
259                     updateState(channelUID, UnDefType.UNDEF);
260                 }
261             });
262             if (!sensiboSky.getAcState().isPresent()) {
263                 updateState(channelUID, UnDefType.UNDEF);
264             }
265         } else if (command instanceof QuantityType<?>) {
266             QuantityType<?> newValue = (QuantityType<?>) command;
267             if (!Objects.equals(sensiboSky.getTemperatureUnit(), newValue.getUnit())) {
268                 // If quantity is given in celsius when fahrenheit is used or opposite
269                 try {
270                     UnitConverter temperatureConverter = newValue.getUnit()
271                             .getConverterToAny(sensiboSky.getTemperatureUnit());
272                     // No decimals supported
273                     long convertedValue = (long) temperatureConverter.convert(newValue.longValue());
274                     updateAcState(sensiboSky, TARGET_TEMPERATURE_PROPERTY, new DecimalType(convertedValue));
275                 } catch (UnconvertibleException | IncommensurableException e) {
276                     logger.info("Could not convert {} to {}: {}", newValue, sensiboSky.getTemperatureUnit(),
277                             e.getMessage());
278                 }
279             } else {
280                 updateAcState(sensiboSky, TARGET_TEMPERATURE_PROPERTY, new DecimalType(newValue.intValue()));
281             }
282         } else if (command instanceof DecimalType) {
283             updateAcState(sensiboSky, TARGET_TEMPERATURE_PROPERTY, command);
284         }
285     }
286
287     private void handleMasterSwitchCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
288         if (command instanceof RefreshType) {
289             sensiboSky.getAcState().ifPresent(e -> updateState(channelUID, OnOffType.from(e.isOn())));
290         } else if (command instanceof OnOffType) {
291             updateAcState(sensiboSky, MASTER_SWITCH_PROPERTY, command == OnOffType.ON);
292         }
293     }
294
295     private void handleCurrentTemperatureCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
296         if (command instanceof RefreshType) {
297             updateState(channelUID, new QuantityType<>(sensiboSky.getTemperature(), SIUnits.CELSIUS));
298         }
299     }
300
301     private void handleCurrentHumidityCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
302         if (command instanceof RefreshType) {
303             updateState(channelUID, new QuantityType<>(sensiboSky.getHumidity(), Units.PERCENT));
304         }
305     }
306
307     @Override
308     public Collection<Class<? extends ThingHandlerService>> getServices() {
309         return Collections.singleton(CallbackChannelsTypeProvider.class);
310     }
311
312     @Override
313     public void initialize() {
314         config = Optional.ofNullable(getConfigAs(SensiboSkyConfiguration.class));
315         logger.debug("Initializing SensiboSky using config {}", config);
316         getSensiboModel().findSensiboSkyByMacAddress(getMacAddress()).ifPresent(pod -> {
317
318             if (pod.isAlive()) {
319                 addDynamicChannelsAndProperties(pod);
320                 updateStatus(ThingStatus.ONLINE);
321             } else {
322                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
323                         "Unreachable by Sensibo servers");
324             }
325         });
326     }
327
328     private boolean isDynamicChannel(final ChannelTypeUID uid) {
329         return SensiboBindingConstants.DYNAMIC_CHANNEL_TYPES.stream().anyMatch(e -> uid.getId().startsWith(e));
330     }
331
332     private void addDynamicChannelsAndProperties(final SensiboSky sensiboSky) {
333         logger.debug("Updating dynamic channels for {}", sensiboSky.getId());
334         final List<Channel> newChannels = new ArrayList<>();
335         for (final Channel channel : getThing().getChannels()) {
336             final ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
337             if (channelTypeUID != null && !isDynamicChannel(channelTypeUID)) {
338                 newChannels.add(channel);
339             }
340         }
341
342         newChannels.addAll(createDynamicChannels(sensiboSky));
343         Map<String, String> properties = sensiboSky.getThingProperties();
344         updateThing(editThing().withChannels(newChannels).withProperties(properties).build());
345     }
346
347     public List<Channel> createDynamicChannels(final SensiboSky sensiboSky) {
348         final List<Channel> newChannels = new ArrayList<>();
349         generatedChannelTypes.clear();
350
351         sensiboSky.getCurrentModeCapabilities().ifPresent(capabilities -> {
352             // Not all modes have swing and fan level
353             final ChannelTypeUID swingModeChannelType = addChannelType(SensiboBindingConstants.CHANNEL_TYPE_SWING_MODE,
354                     SWING_MODE_LABEL, ITEM_TYPE_STRING, capabilities.swingModes, null, null);
355             newChannels
356                     .add(ChannelBuilder
357                             .create(new ChannelUID(getThing().getUID(), SensiboBindingConstants.CHANNEL_SWING_MODE),
358                                     ITEM_TYPE_STRING)
359                             .withLabel(SWING_MODE_LABEL).withType(swingModeChannelType).build());
360
361             final ChannelTypeUID fanLevelChannelType = addChannelType(SensiboBindingConstants.CHANNEL_TYPE_FAN_LEVEL,
362                     FAN_LEVEL_LABEL, ITEM_TYPE_STRING, capabilities.fanLevels, null, null);
363             newChannels.add(ChannelBuilder
364                     .create(new ChannelUID(getThing().getUID(), SensiboBindingConstants.CHANNEL_FAN_LEVEL),
365                             ITEM_TYPE_STRING)
366                     .withLabel(FAN_LEVEL_LABEL).withType(fanLevelChannelType).build());
367         });
368
369         final ChannelTypeUID modeChannelType = addChannelType(SensiboBindingConstants.CHANNEL_TYPE_MODE, MODE_LABEL,
370                 ITEM_TYPE_STRING, sensiboSky.getRemoteCapabilities().keySet(), null, null);
371         newChannels.add(ChannelBuilder
372                 .create(new ChannelUID(getThing().getUID(), SensiboBindingConstants.CHANNEL_MODE), ITEM_TYPE_STRING)
373                 .withLabel(MODE_LABEL).withType(modeChannelType).build());
374
375         final ChannelTypeUID targetTemperatureChannelType = addChannelType(
376                 SensiboBindingConstants.CHANNEL_TYPE_TARGET_TEMPERATURE, TARGET_TEMPERATURE_LABEL,
377                 ITEM_TYPE_NUMBER_TEMPERATURE, sensiboSky.getTargetTemperatures(), "%d %unit%", "TargetTemperature");
378         newChannels.add(ChannelBuilder
379                 .create(new ChannelUID(getThing().getUID(), SensiboBindingConstants.CHANNEL_TARGET_TEMPERATURE),
380                         ITEM_TYPE_NUMBER_TEMPERATURE)
381                 .withLabel(TARGET_TEMPERATURE_LABEL).withType(targetTemperatureChannelType).build());
382
383         return newChannels;
384     }
385
386     private ChannelTypeUID addChannelType(final String channelTypePrefix, final String label, final String itemType,
387             final Collection<?> options, @Nullable final String pattern, @Nullable final String tag) {
388         final ChannelTypeUID channelTypeUID = new ChannelTypeUID(SensiboBindingConstants.BINDING_ID,
389                 channelTypePrefix + getThing().getUID().getId());
390         final List<StateOption> stateOptions = options.stream()
391                 .map(e -> new StateOption(e.toString(), e instanceof String ? beautify((String) e) : e.toString()))
392                 .collect(Collectors.toList());
393
394         StateDescriptionFragmentBuilder stateDescription = StateDescriptionFragmentBuilder.create().withReadOnly(false)
395                 .withOptions(stateOptions);
396         if (pattern != null) {
397             stateDescription = stateDescription.withPattern(pattern);
398         }
399         final StateChannelTypeBuilder builder = ChannelTypeBuilder.state(channelTypeUID, label, itemType)
400                 .withStateDescription(stateDescription.build().toStateDescription());
401         if (tag != null) {
402             builder.withTag(tag);
403         }
404         final ChannelType channelType = builder.build();
405
406         generatedChannelTypes.put(channelTypeUID, channelType);
407
408         return channelTypeUID;
409     }
410
411     @Override
412     public Collection<ChannelType> getChannelTypes(@Nullable final Locale locale) {
413         return generatedChannelTypes.values();
414     }
415
416     @Override
417     public @Nullable ChannelType getChannelType(final ChannelTypeUID channelTypeUID, @Nullable final Locale locale) {
418         return generatedChannelTypes.get(channelTypeUID);
419     }
420
421     /*
422      * Package private in order to be reachable from unit test
423      */
424     StateChange checkStateChangeValid(SensiboSky sensiboSky, String property, Object newPropertyValue) {
425         StateChange stateChange = new StateChange(newPropertyValue);
426
427         sensiboSky.getCurrentModeCapabilities().ifPresent(currentModeCapabilities -> {
428             switch (property) {
429                 case TARGET_TEMPERATURE_PROPERTY:
430                     Unit<Temperature> temperatureUnit = sensiboSky.getTemperatureUnit();
431                     TemperatureDTO validTemperatures = currentModeCapabilities.temperatures
432                             .get(temperatureUnit == SIUnits.CELSIUS ? "C" : "F");
433                     DecimalType rawValue = (DecimalType) newPropertyValue;
434                     stateChange.updateValue(rawValue.intValue());
435                     if (!validTemperatures.validValues.contains(rawValue.intValue())) {
436                         stateChange.addError(String.format(
437                                 "Cannot change targetTemperature to '%d', valid targetTemperatures are one of %s",
438                                 rawValue.intValue(), ToStringBuilder.reflectionToString(
439                                         validTemperatures.validValues.toArray(), ToStringStyle.SIMPLE_STYLE)));
440                     }
441                     break;
442                 case MODE_PROPERTY:
443                     if (!sensiboSky.getRemoteCapabilities().containsKey(newPropertyValue)) {
444                         stateChange.addError(
445                                 String.format("Cannot change mode to %s, valid modes are %s", newPropertyValue,
446                                         ToStringBuilder.reflectionToString(
447                                                 sensiboSky.getRemoteCapabilities().keySet().toArray(),
448                                                 ToStringStyle.SIMPLE_STYLE)));
449                     }
450                     break;
451                 case FAN_LEVEL_PROPERTY:
452                     if (!currentModeCapabilities.fanLevels.contains(newPropertyValue)) {
453                         stateChange.addError(String.format("Cannot change fanLevel to %s, valid fanLevels are %s",
454                                 newPropertyValue, ToStringBuilder.reflectionToString(
455                                         currentModeCapabilities.fanLevels.toArray(), ToStringStyle.SIMPLE_STYLE)));
456                     }
457                     break;
458                 case MASTER_SWITCH_PROPERTY:
459                     // Always allowed
460                     break;
461                 case SWING_PROPERTY:
462                     if (!currentModeCapabilities.swingModes.contains(newPropertyValue)) {
463                         stateChange.addError(String.format("Cannot change swing to %s, valid swings are %s",
464                                 newPropertyValue, ToStringBuilder.reflectionToString(
465                                         currentModeCapabilities.swingModes.toArray(), ToStringStyle.SIMPLE_STYLE)));
466                     }
467                     break;
468                 default:
469                     stateChange.addError(String.format("No such ac state property %s", property));
470             }
471             logger.debug("State change request {}", stateChange);
472         });
473         return stateChange;
474     }
475
476     @NonNullByDefault
477     public class StateChange {
478         Object value;
479
480         boolean valid = true;
481         @Nullable
482         String validationMessage;
483
484         public StateChange(Object value) {
485             this.value = value;
486         }
487
488         public void updateValue(Object updatedValue) {
489             value = updatedValue;
490         }
491
492         public void addError(String validationMessage) {
493             valid = false;
494             this.validationMessage = validationMessage;
495         }
496
497         @Override
498         public String toString() {
499             return "StateChange [valid=" + valid + ", validationMessage=" + validationMessage + ", value=" + value
500                     + ", value Class=" + value.getClass() + "]";
501         }
502     }
503 }