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