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