]> git.basschouten.com Git - openhab-addons.git/blob
b6759a1c454ee40f25e74cc45ff0618d7b1a5f67
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.avmfritz.internal.handler;
14
15 import static org.openhab.binding.avmfritz.internal.AVMFritzBindingConstants.*;
16 import static org.openhab.binding.avmfritz.internal.dto.HeatingModel.*;
17
18 import java.math.BigDecimal;
19 import java.time.Instant;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
22 import java.util.Map;
23
24 import javax.measure.quantity.Temperature;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.avmfritz.internal.config.AVMFritzDeviceConfiguration;
29 import org.openhab.binding.avmfritz.internal.dto.AVMFritzBaseModel;
30 import org.openhab.binding.avmfritz.internal.dto.AlertModel;
31 import org.openhab.binding.avmfritz.internal.dto.BatteryModel;
32 import org.openhab.binding.avmfritz.internal.dto.DeviceModel;
33 import org.openhab.binding.avmfritz.internal.dto.HeatingModel;
34 import org.openhab.binding.avmfritz.internal.dto.HeatingModel.NextChangeModel;
35 import org.openhab.binding.avmfritz.internal.dto.HumidityModel;
36 import org.openhab.binding.avmfritz.internal.dto.LevelcontrolModel;
37 import org.openhab.binding.avmfritz.internal.dto.PowerMeterModel;
38 import org.openhab.binding.avmfritz.internal.dto.SwitchModel;
39 import org.openhab.binding.avmfritz.internal.dto.TemperatureModel;
40 import org.openhab.binding.avmfritz.internal.hardware.FritzAhaStatusListener;
41 import org.openhab.binding.avmfritz.internal.hardware.FritzAhaWebInterface;
42 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetBlindTargetCallback.BlindCommand;
43 import org.openhab.core.config.core.Configuration;
44 import org.openhab.core.library.types.DateTimeType;
45 import org.openhab.core.library.types.DecimalType;
46 import org.openhab.core.library.types.IncreaseDecreaseType;
47 import org.openhab.core.library.types.OnOffType;
48 import org.openhab.core.library.types.OpenClosedType;
49 import org.openhab.core.library.types.PercentType;
50 import org.openhab.core.library.types.QuantityType;
51 import org.openhab.core.library.types.StopMoveType;
52 import org.openhab.core.library.types.StringType;
53 import org.openhab.core.library.types.UpDownType;
54 import org.openhab.core.library.unit.SIUnits;
55 import org.openhab.core.library.unit.Units;
56 import org.openhab.core.thing.Bridge;
57 import org.openhab.core.thing.Channel;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
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.ThingUID;
64 import org.openhab.core.thing.binding.BaseThingHandler;
65 import org.openhab.core.thing.binding.BridgeHandler;
66 import org.openhab.core.thing.binding.ThingHandlerCallback;
67 import org.openhab.core.thing.type.ChannelTypeUID;
68 import org.openhab.core.types.Command;
69 import org.openhab.core.types.RefreshType;
70 import org.openhab.core.types.State;
71 import org.openhab.core.types.UnDefType;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74
75 /**
76  * Abstract handler for a FRITZ! thing. Handles commands, which are sent to one of the channels.
77  *
78  * @author Robert Bausdorf - Initial contribution
79  * @author Christoph Weitkamp - Added support for AVM FRITZ!DECT 300 and Comet DECT
80  * @author Christoph Weitkamp - Added support for groups
81  * @author Ulrich Mertin - Added support for HAN-FUN blinds
82  */
83 @NonNullByDefault
84 public abstract class AVMFritzBaseThingHandler extends BaseThingHandler implements FritzAhaStatusListener {
85
86     private final Logger logger = LoggerFactory.getLogger(AVMFritzBaseThingHandler.class);
87
88     /**
89      * keeps track of the current state for handling of increase/decrease
90      */
91     private @Nullable AVMFritzBaseModel state;
92     private @Nullable String identifier;
93
94     /**
95      * Constructor
96      *
97      * @param thing Thing object representing a FRITZ! device
98      */
99     public AVMFritzBaseThingHandler(Thing thing) {
100         super(thing);
101     }
102
103     @Override
104     public void initialize() {
105         final AVMFritzDeviceConfiguration config = getConfigAs(AVMFritzDeviceConfiguration.class);
106         final String newIdentifier = config.ain;
107         if (newIdentifier == null || newIdentifier.isBlank()) {
108             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
109                     "The 'ain' parameter must be configured.");
110         } else {
111             this.identifier = newIdentifier;
112             updateStatus(ThingStatus.UNKNOWN);
113         }
114     }
115
116     @Override
117     public void onDeviceAdded(AVMFritzBaseModel device) {
118         // nothing to do
119     }
120
121     @Override
122     public void onDeviceUpdated(ThingUID thingUID, AVMFritzBaseModel device) {
123         if (thing.getUID().equals(thingUID)) {
124             logger.debug("Update thing '{}' with device model: {}", thingUID, device);
125             if (device.getPresent() == 1) {
126                 updateStatus(ThingStatus.ONLINE);
127             } else {
128                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device not present");
129             }
130             state = device;
131
132             updateProperties(device, editProperties());
133
134             if (device.isPowermeter()) {
135                 updatePowermeter(device.getPowermeter());
136             }
137             if (device.isSwitchableOutlet()) {
138                 updateSwitchableOutlet(device.getSwitch());
139             }
140             if (device.isHeatingThermostat()) {
141                 updateHeatingThermostat(device.getHkr());
142             }
143             if (device instanceof DeviceModel) {
144                 DeviceModel deviceModel = (DeviceModel) device;
145                 if (deviceModel.isTempSensor()) {
146                     updateTemperatureSensor(deviceModel.getTemperature());
147                 }
148                 if (deviceModel.isHumiditySensor()) {
149                     updateHumiditySensor(deviceModel.getHumidity());
150                 }
151                 if (deviceModel.isHANFUNAlarmSensor()) {
152                     updateHANFUNAlarmSensor(deviceModel.getAlert());
153                 }
154                 if (deviceModel.isHANFUNBlinds()) {
155                     updateLevelcontrol(deviceModel.getLevelcontrol());
156                 }
157             }
158         }
159     }
160
161     private void updateHANFUNAlarmSensor(@Nullable AlertModel alertModel) {
162         if (alertModel != null) {
163             updateThingChannelState(CHANNEL_CONTACT_STATE,
164                     AlertModel.ON.equals(alertModel.getState()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
165         }
166     }
167
168     protected void updateTemperatureSensor(@Nullable TemperatureModel temperatureModel) {
169         if (temperatureModel != null) {
170             updateThingChannelState(CHANNEL_TEMPERATURE,
171                     new QuantityType<>(temperatureModel.getCelsius(), SIUnits.CELSIUS));
172             updateThingChannelConfiguration(CHANNEL_TEMPERATURE, CONFIG_CHANNEL_TEMP_OFFSET,
173                     temperatureModel.getOffset());
174         }
175     }
176
177     protected void updateHumiditySensor(@Nullable HumidityModel humidityModel) {
178         if (humidityModel != null) {
179             updateThingChannelState(CHANNEL_HUMIDITY,
180                     new QuantityType<>(humidityModel.getRelativeHumidity(), Units.PERCENT));
181         }
182     }
183
184     protected void updateLevelcontrol(@Nullable LevelcontrolModel levelcontrolModel) {
185         if (levelcontrolModel != null) {
186             updateThingChannelState(CHANNEL_ROLLERSHUTTER, new PercentType(levelcontrolModel.getLevelPercentage()));
187         }
188     }
189
190     private void updateHeatingThermostat(@Nullable HeatingModel heatingModel) {
191         if (heatingModel != null) {
192             updateThingChannelState(CHANNEL_MODE, new StringType(heatingModel.getMode()));
193             updateThingChannelState(CHANNEL_LOCKED,
194                     BigDecimal.ZERO.equals(heatingModel.getLock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
195             updateThingChannelState(CHANNEL_DEVICE_LOCKED,
196                     BigDecimal.ZERO.equals(heatingModel.getDevicelock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
197             updateThingChannelState(CHANNEL_ACTUALTEMP,
198                     new QuantityType<>(toCelsius(heatingModel.getTist()), SIUnits.CELSIUS));
199             updateThingChannelState(CHANNEL_SETTEMP,
200                     new QuantityType<>(toCelsius(heatingModel.getTsoll()), SIUnits.CELSIUS));
201             updateThingChannelState(CHANNEL_ECOTEMP,
202                     new QuantityType<>(toCelsius(heatingModel.getAbsenk()), SIUnits.CELSIUS));
203             updateThingChannelState(CHANNEL_COMFORTTEMP,
204                     new QuantityType<>(toCelsius(heatingModel.getKomfort()), SIUnits.CELSIUS));
205             updateThingChannelState(CHANNEL_RADIATOR_MODE, new StringType(heatingModel.getRadiatorMode()));
206             NextChangeModel nextChange = heatingModel.getNextchange();
207             if (nextChange != null) {
208                 int endPeriod = nextChange.getEndperiod();
209                 updateThingChannelState(CHANNEL_NEXT_CHANGE, endPeriod == 0 ? UnDefType.UNDEF
210                         : new DateTimeType(
211                                 ZonedDateTime.ofInstant(Instant.ofEpochSecond(endPeriod), ZoneId.systemDefault())));
212                 BigDecimal nextTemperature = nextChange.getTchange();
213                 updateThingChannelState(CHANNEL_NEXTTEMP, TEMP_FRITZ_UNDEFINED.equals(nextTemperature) ? UnDefType.UNDEF
214                         : new QuantityType<>(toCelsius(nextTemperature), SIUnits.CELSIUS));
215             }
216             updateBattery(heatingModel);
217         }
218     }
219
220     protected void updateBattery(BatteryModel batteryModel) {
221         BigDecimal batteryLevel = batteryModel.getBattery();
222         updateThingChannelState(CHANNEL_BATTERY,
223                 batteryLevel == null ? UnDefType.UNDEF : new DecimalType(batteryLevel));
224         BigDecimal lowBattery = batteryModel.getBatterylow();
225         if (lowBattery == null) {
226             updateThingChannelState(CHANNEL_BATTERY_LOW, UnDefType.UNDEF);
227         } else {
228             updateThingChannelState(CHANNEL_BATTERY_LOW,
229                     BatteryModel.BATTERY_ON.equals(lowBattery) ? OnOffType.ON : OnOffType.OFF);
230         }
231     }
232
233     private void updateSwitchableOutlet(@Nullable SwitchModel switchModel) {
234         if (switchModel != null) {
235             updateThingChannelState(CHANNEL_MODE, new StringType(switchModel.getMode()));
236             updateThingChannelState(CHANNEL_LOCKED,
237                     BigDecimal.ZERO.equals(switchModel.getLock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
238             updateThingChannelState(CHANNEL_DEVICE_LOCKED,
239                     BigDecimal.ZERO.equals(switchModel.getDevicelock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
240             BigDecimal state = switchModel.getState();
241             if (state == null) {
242                 updateThingChannelState(CHANNEL_OUTLET, UnDefType.UNDEF);
243             } else {
244                 updateThingChannelState(CHANNEL_OUTLET, SwitchModel.ON.equals(state) ? OnOffType.ON : OnOffType.OFF);
245             }
246         }
247     }
248
249     private void updatePowermeter(@Nullable PowerMeterModel powerMeterModel) {
250         if (powerMeterModel != null) {
251             updateThingChannelState(CHANNEL_ENERGY, new QuantityType<>(powerMeterModel.getEnergy(), Units.WATT_HOUR));
252             updateThingChannelState(CHANNEL_POWER, new QuantityType<>(powerMeterModel.getPower(), Units.WATT));
253             updateThingChannelState(CHANNEL_VOLTAGE, new QuantityType<>(powerMeterModel.getVoltage(), Units.VOLT));
254         }
255     }
256
257     /**
258      * Updates thing properties.
259      *
260      * @param device the {@link AVMFritzBaseModel}
261      * @param editProperties map of existing properties
262      */
263     protected void updateProperties(AVMFritzBaseModel device, Map<String, String> editProperties) {
264         editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion());
265         updateProperties(editProperties);
266     }
267
268     /**
269      * Updates thing channels and creates dynamic channels if missing.
270      *
271      * @param channelId ID of the channel to be updated.
272      * @param state State to be set.
273      */
274     protected void updateThingChannelState(String channelId, State state) {
275         Channel channel = thing.getChannel(channelId);
276         if (channel != null) {
277             updateState(channel.getUID(), state);
278         } else {
279             logger.debug("Channel '{}' in thing '{}' does not exist, recreating thing.", channelId, thing.getUID());
280             createChannel(channelId);
281         }
282     }
283
284     /**
285      * Creates a {@link ChannelTypeUID} from the given channel id.
286      *
287      * @param channelId ID of the channel type UID to be created.
288      * @return the channel type UID
289      */
290     private ChannelTypeUID createChannelTypeUID(String channelId) {
291         int pos = channelId.indexOf(ChannelUID.CHANNEL_GROUP_SEPARATOR);
292         String id = pos > -1 ? channelId.substring(pos + 1) : channelId;
293         return CHANNEL_BATTERY.equals(id) ? DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_BATTERY_LEVEL.getUID()
294                 : new ChannelTypeUID(BINDING_ID, id);
295     }
296
297     /**
298      * Creates new channels for the thing.
299      *
300      * @param channelId ID of the channel to be created.
301      */
302     private void createChannel(String channelId) {
303         ThingHandlerCallback callback = getCallback();
304         if (callback != null) {
305             ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
306             ChannelTypeUID channelTypeUID = createChannelTypeUID(channelId);
307             Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).build();
308             updateThing(editThing().withoutChannel(channelUID).withChannel(channel).build());
309         }
310     }
311
312     /**
313      * Updates thing channel configurations.
314      *
315      * @param channelId ID of the channel which configuration to be updated.
316      * @param configId ID of the configuration to be updated.
317      * @param value Value to be set.
318      */
319     protected void updateThingChannelConfiguration(String channelId, String configId, Object value) {
320         Channel channel = thing.getChannel(channelId);
321         if (channel != null) {
322             Configuration editConfig = channel.getConfiguration();
323             editConfig.put(configId, value);
324         }
325     }
326
327     @Override
328     public void onDeviceGone(ThingUID thingUID) {
329         if (thing.getUID().equals(thingUID)) {
330             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Device not present in response");
331         }
332     }
333
334     @Override
335     public void handleCommand(ChannelUID channelUID, Command command) {
336         String channelId = channelUID.getIdWithoutGroup();
337         logger.debug("Handle command '{}' for channel {}", command, channelId);
338         if (command == RefreshType.REFRESH) {
339             handleRefreshCommand();
340             return;
341         }
342         FritzAhaWebInterface fritzBox = getWebInterface();
343         if (fritzBox == null) {
344             logger.debug("Cannot handle command '{}' because connection is missing", command);
345             return;
346         }
347         String ain = getIdentifier();
348         if (ain == null) {
349             logger.debug("Cannot handle command '{}' because AIN is missing", command);
350             return;
351         }
352         switch (channelId) {
353             case CHANNEL_MODE:
354             case CHANNEL_LOCKED:
355             case CHANNEL_DEVICE_LOCKED:
356             case CHANNEL_TEMPERATURE:
357             case CHANNEL_HUMIDITY:
358             case CHANNEL_ENERGY:
359             case CHANNEL_POWER:
360             case CHANNEL_VOLTAGE:
361             case CHANNEL_ACTUALTEMP:
362             case CHANNEL_ECOTEMP:
363             case CHANNEL_COMFORTTEMP:
364             case CHANNEL_NEXT_CHANGE:
365             case CHANNEL_NEXTTEMP:
366             case CHANNEL_BATTERY:
367             case CHANNEL_BATTERY_LOW:
368             case CHANNEL_CONTACT_STATE:
369             case CHANNEL_LAST_CHANGE:
370                 logger.debug("Channel {} is a read-only channel and cannot handle command '{}'", channelId, command);
371                 break;
372             case CHANNEL_OUTLET:
373                 if (command instanceof OnOffType) {
374                     fritzBox.setSwitch(ain, OnOffType.ON.equals(command));
375                     if (state != null) {
376                         state.getSwitch().setState(OnOffType.ON.equals(command) ? SwitchModel.ON : SwitchModel.OFF);
377                     }
378                 }
379                 break;
380             case CHANNEL_SETTEMP:
381                 BigDecimal temperature = null;
382                 if (command instanceof DecimalType) {
383                     temperature = normalizeCelsius(((DecimalType) command).toBigDecimal());
384                 } else if (command instanceof QuantityType) {
385                     @SuppressWarnings("unchecked")
386                     QuantityType<Temperature> convertedCommand = ((QuantityType<Temperature>) command)
387                             .toUnit(SIUnits.CELSIUS);
388                     if (convertedCommand != null) {
389                         temperature = normalizeCelsius(convertedCommand.toBigDecimal());
390                     } else {
391                         logger.warn("Unable to convert unit from '{}' to '{}'. Skipping command.",
392                                 ((QuantityType<?>) command).getUnit(), SIUnits.CELSIUS);
393                     }
394                 } else if (command instanceof IncreaseDecreaseType) {
395                     temperature = state.getHkr().getTsoll();
396                     if (IncreaseDecreaseType.INCREASE.equals(command)) {
397                         temperature.add(BigDecimal.ONE);
398                     } else {
399                         temperature.subtract(BigDecimal.ONE);
400                     }
401                 } else if (command instanceof OnOffType) {
402                     temperature = OnOffType.ON.equals(command) ? TEMP_FRITZ_ON : TEMP_FRITZ_OFF;
403                 }
404                 if (temperature != null) {
405                     fritzBox.setSetTemp(ain, fromCelsius(temperature));
406                     HeatingModel heatingModel = state.getHkr();
407                     heatingModel.setTsoll(temperature);
408                     updateState(CHANNEL_RADIATOR_MODE, new StringType(heatingModel.getRadiatorMode()));
409                 }
410                 break;
411             case CHANNEL_RADIATOR_MODE:
412                 BigDecimal targetTemperature = null;
413                 if (command instanceof StringType) {
414                     switch (command.toString()) {
415                         case MODE_ON:
416                             targetTemperature = TEMP_FRITZ_ON;
417                             break;
418                         case MODE_OFF:
419                             targetTemperature = TEMP_FRITZ_OFF;
420                             break;
421                         case MODE_COMFORT:
422                             targetTemperature = state.getHkr().getKomfort();
423                             break;
424                         case MODE_ECO:
425                             targetTemperature = state.getHkr().getAbsenk();
426                             break;
427                         case MODE_BOOST:
428                             targetTemperature = TEMP_FRITZ_MAX;
429                             break;
430                         case MODE_UNKNOWN:
431                         case MODE_WINDOW_OPEN:
432                             logger.debug("Command '{}' is a read-only command for channel {}.", command, channelId);
433                             break;
434                     }
435                     if (targetTemperature != null) {
436                         fritzBox.setSetTemp(ain, targetTemperature);
437                         state.getHkr().setTsoll(targetTemperature);
438                         updateState(CHANNEL_SETTEMP, new QuantityType<>(toCelsius(targetTemperature), SIUnits.CELSIUS));
439                     }
440                 }
441                 break;
442             case CHANNEL_ROLLERSHUTTER:
443                 if (command instanceof StopMoveType) {
444                     StopMoveType rollershutterCommand = (StopMoveType) command;
445                     if (StopMoveType.STOP.equals(rollershutterCommand)) {
446                         fritzBox.setBlind(ain, BlindCommand.STOP);
447                     } else {
448                         logger.debug("Received unknown rollershutter StopMove command MOVE");
449                     }
450                 } else if (command instanceof UpDownType) {
451                     UpDownType rollershutterCommand = (UpDownType) command;
452                     if (UpDownType.UP.equals(rollershutterCommand)) {
453                         fritzBox.setBlind(ain, BlindCommand.OPEN);
454                     } else {
455                         fritzBox.setBlind(ain, BlindCommand.CLOSE);
456                     }
457                 } else if (command instanceof PercentType) {
458                     PercentType rollershutterCommand = (PercentType) command;
459                     BigDecimal levelpercentage = rollershutterCommand.toBigDecimal();
460                     fritzBox.setLevelpercentage(ain, levelpercentage);
461                 } else {
462                     logger.debug("Received unknown rollershutter command type '{}'", command.toString());
463                 }
464                 break;
465             default:
466                 logger.debug("Received unknown channel {}", channelId);
467                 break;
468         }
469     }
470
471     /**
472      * Handles a command for a given action.
473      *
474      * @param action
475      * @param duration
476      */
477     protected void handleAction(String action, long duration) {
478         FritzAhaWebInterface fritzBox = getWebInterface();
479         if (fritzBox == null) {
480             logger.debug("Cannot handle action '{}' because connection is missing", action);
481             return;
482         }
483         String ain = getIdentifier();
484         if (ain == null) {
485             logger.debug("Cannot handle action '{}' because AIN is missing", action);
486             return;
487         }
488         if (duration < 0 || 86400 < duration) {
489             throw new IllegalArgumentException("Duration must not be less than zero or greater than 86400");
490         }
491         switch (action) {
492             case MODE_BOOST:
493                 fritzBox.setBoostMode(ain,
494                         duration > 0 ? ZonedDateTime.now().plusSeconds(duration).toEpochSecond() : 0);
495                 break;
496             case MODE_WINDOW_OPEN:
497                 fritzBox.setWindowOpenMode(ain,
498                         duration > 0 ? ZonedDateTime.now().plusSeconds(duration).toEpochSecond() : 0);
499                 break;
500             default:
501                 logger.debug("Received unknown action '{}'", action);
502                 break;
503         }
504     }
505
506     /**
507      * Provides the web interface object.
508      *
509      * @return The web interface object
510      */
511     private @Nullable FritzAhaWebInterface getWebInterface() {
512         Bridge bridge = getBridge();
513         if (bridge != null) {
514             BridgeHandler handler = bridge.getHandler();
515             if (handler instanceof AVMFritzBaseBridgeHandler) {
516                 return ((AVMFritzBaseBridgeHandler) handler).getWebInterface();
517             }
518         }
519         return null;
520     }
521
522     /**
523      * Handles a refresh command.
524      */
525     private void handleRefreshCommand() {
526         Bridge bridge = getBridge();
527         if (bridge != null) {
528             BridgeHandler handler = bridge.getHandler();
529             if (handler instanceof AVMFritzBaseBridgeHandler) {
530                 ((AVMFritzBaseBridgeHandler) handler).handleRefreshCommand();
531             }
532         }
533     }
534
535     /**
536      * Returns the AIN.
537      *
538      * @return the AIN
539      */
540     public @Nullable String getIdentifier() {
541         return identifier;
542     }
543 }