]> git.basschouten.com Git - openhab-addons.git/blob
c8b9c783051aa2e0d205abe428c34e427e8f8c81
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.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 AVMFritzBaseModel currentDevice = new DeviceModel();
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             currentDevice = 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                     if (deviceModel.isHANFUNBlinds()) {
156                         updateHANFUNBlindsAlarmSensor(deviceModel.getAlert());
157                     } else {
158                         updateHANFUNAlarmSensor(deviceModel.getAlert());
159                     }
160                 }
161                 if (deviceModel.isHANFUNBlinds()) {
162                     updateLevelControl(deviceModel.getLevelControlModel());
163                 } else if (deviceModel.isColorLight()) {
164                     updateColorLight(deviceModel.getColorControlModel(), deviceModel.getLevelControlModel());
165                 } else if (deviceModel.isDimmableLight() && !deviceModel.isHANFUNBlinds()) {
166                     updateDimmableLight(deviceModel.getLevelControlModel());
167                 } else if (deviceModel.isHANFUNUnit() && deviceModel.isHANFUNOnOff()) {
168                     updateSimpleOnOffUnit(deviceModel.getSimpleOnOffUnit());
169                 }
170             }
171         }
172     }
173
174     private void updateHANFUNAlarmSensor(@Nullable AlertModel alertModel) {
175         if (alertModel != null) {
176             updateThingChannelState(CHANNEL_CONTACT_STATE,
177                     AlertModel.ON.equals(alertModel.getState()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
178         }
179     }
180
181     private void updateHANFUNBlindsAlarmSensor(@Nullable AlertModel alertModel) {
182         if (alertModel != null) {
183             updateThingChannelState(CHANNEL_OBSTRUCTION_ALARM,
184                     OnOffType.from(alertModel.hasObstructionAlarmOccurred()));
185             updateThingChannelState(CHANNEL_TEMPERATURE_ALARM, OnOffType.from(alertModel.hasTemperaturAlarmOccurred()));
186             if (alertModel.hasUnknownAlarmOccurred()) {
187                 logger.warn("Unknown blinds alarm {}", alertModel.getState());
188             }
189         }
190     }
191
192     protected void updateTemperatureSensor(@Nullable TemperatureModel temperatureModel) {
193         if (temperatureModel != null) {
194             updateThingChannelState(CHANNEL_TEMPERATURE,
195                     new QuantityType<>(temperatureModel.getCelsius(), SIUnits.CELSIUS));
196             updateThingChannelConfiguration(CHANNEL_TEMPERATURE, CONFIG_CHANNEL_TEMP_OFFSET,
197                     temperatureModel.getOffset());
198         }
199     }
200
201     protected void updateHumiditySensor(@Nullable HumidityModel humidityModel) {
202         if (humidityModel != null) {
203             updateThingChannelState(CHANNEL_HUMIDITY,
204                     new QuantityType<>(humidityModel.getRelativeHumidity(), Units.PERCENT));
205         }
206     }
207
208     protected void updateLevelControl(@Nullable LevelControlModel levelControlModel) {
209         if (levelControlModel != null) {
210             updateThingChannelState(CHANNEL_ROLLERSHUTTER, new PercentType(levelControlModel.getLevelPercentage()));
211         }
212     }
213
214     private void updateDimmableLight(@Nullable LevelControlModel levelControlModel) {
215         if (levelControlModel != null) {
216             updateThingChannelState(CHANNEL_BRIGHTNESS, new PercentType(levelControlModel.getLevelPercentage()));
217         }
218     }
219
220     private void updateColorLight(@Nullable ColorControlModel colorControlModel,
221             @Nullable LevelControlModel levelControlModel) {
222         if (colorControlModel != null && levelControlModel != null) {
223             DecimalType hue = new DecimalType(colorControlModel.hue);
224             PercentType saturation = ColorControlModel.toPercent(colorControlModel.saturation);
225             PercentType brightness = new PercentType(levelControlModel.getLevelPercentage());
226             updateThingChannelState(CHANNEL_COLOR, new HSBType(hue, saturation, brightness));
227         }
228     }
229
230     private void updateHeatingThermostat(@Nullable HeatingModel heatingModel) {
231         if (heatingModel != null) {
232             updateThingChannelState(CHANNEL_MODE, new StringType(heatingModel.getMode()));
233             updateThingChannelState(CHANNEL_LOCKED,
234                     BigDecimal.ZERO.equals(heatingModel.getLock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
235             updateThingChannelState(CHANNEL_DEVICE_LOCKED,
236                     BigDecimal.ZERO.equals(heatingModel.getDevicelock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
237             updateThingChannelState(CHANNEL_ACTUALTEMP,
238                     new QuantityType<>(toCelsius(heatingModel.getTist()), SIUnits.CELSIUS));
239             updateThingChannelState(CHANNEL_SETTEMP,
240                     new QuantityType<>(toCelsius(heatingModel.getTsoll()), SIUnits.CELSIUS));
241             updateThingChannelState(CHANNEL_ECOTEMP,
242                     new QuantityType<>(toCelsius(heatingModel.getAbsenk()), SIUnits.CELSIUS));
243             updateThingChannelState(CHANNEL_COMFORTTEMP,
244                     new QuantityType<>(toCelsius(heatingModel.getKomfort()), SIUnits.CELSIUS));
245             updateThingChannelState(CHANNEL_RADIATOR_MODE, new StringType(heatingModel.getRadiatorMode()));
246             NextChangeModel nextChange = heatingModel.getNextchange();
247             if (nextChange != null) {
248                 int endPeriod = nextChange.getEndperiod();
249                 updateThingChannelState(CHANNEL_NEXT_CHANGE, endPeriod == 0 ? UnDefType.UNDEF
250                         : new DateTimeType(
251                                 ZonedDateTime.ofInstant(Instant.ofEpochSecond(endPeriod), ZoneId.systemDefault())));
252                 BigDecimal nextTemperature = nextChange.getTchange();
253                 updateThingChannelState(CHANNEL_NEXTTEMP, TEMP_FRITZ_UNDEFINED.equals(nextTemperature) ? UnDefType.UNDEF
254                         : new QuantityType<>(toCelsius(nextTemperature), SIUnits.CELSIUS));
255             }
256             updateBattery(heatingModel);
257         }
258     }
259
260     protected void updateBattery(BatteryModel batteryModel) {
261         BigDecimal batteryLevel = batteryModel.getBattery();
262         updateThingChannelState(CHANNEL_BATTERY,
263                 batteryLevel == null ? UnDefType.UNDEF : new DecimalType(batteryLevel));
264         BigDecimal lowBattery = batteryModel.getBatterylow();
265         if (lowBattery == null) {
266             updateThingChannelState(CHANNEL_BATTERY_LOW, UnDefType.UNDEF);
267         } else {
268             updateThingChannelState(CHANNEL_BATTERY_LOW, OnOffType.from(BatteryModel.BATTERY_ON.equals(lowBattery)));
269         }
270     }
271
272     private void updateSimpleOnOffUnit(@Nullable SimpleOnOffModel simpleOnOffUnit) {
273         if (simpleOnOffUnit != null) {
274             updateThingChannelState(CHANNEL_ON_OFF, OnOffType.from(simpleOnOffUnit.state));
275         }
276     }
277
278     private void updateSwitchableOutlet(@Nullable SwitchModel switchModel) {
279         if (switchModel != null) {
280             updateThingChannelState(CHANNEL_MODE, new StringType(switchModel.getMode()));
281             updateThingChannelState(CHANNEL_LOCKED,
282                     BigDecimal.ZERO.equals(switchModel.getLock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
283             updateThingChannelState(CHANNEL_DEVICE_LOCKED,
284                     BigDecimal.ZERO.equals(switchModel.getDevicelock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
285             BigDecimal state = switchModel.getState();
286             if (state == null) {
287                 updateThingChannelState(CHANNEL_OUTLET, UnDefType.UNDEF);
288             } else {
289                 updateThingChannelState(CHANNEL_OUTLET, OnOffType.from(SwitchModel.ON.equals(state)));
290             }
291         }
292     }
293
294     private void updatePowermeter(@Nullable PowerMeterModel powerMeterModel) {
295         if (powerMeterModel != null) {
296             updateThingChannelState(CHANNEL_ENERGY, new QuantityType<>(powerMeterModel.getEnergy(), Units.WATT_HOUR));
297             updateThingChannelState(CHANNEL_POWER, new QuantityType<>(powerMeterModel.getPower(), Units.WATT));
298             updateThingChannelState(CHANNEL_VOLTAGE, new QuantityType<>(powerMeterModel.getVoltage(), Units.VOLT));
299         }
300     }
301
302     /**
303      * Updates thing properties.
304      *
305      * @param device the {@link AVMFritzBaseModel}
306      * @param editProperties map of existing properties
307      */
308     protected void updateProperties(AVMFritzBaseModel device, Map<String, String> editProperties) {
309         editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion());
310         updateProperties(editProperties);
311     }
312
313     /**
314      * Updates thing channels and creates dynamic channels if missing.
315      *
316      * @param channelId ID of the channel to be updated.
317      * @param state State to be set.
318      */
319     protected void updateThingChannelState(String channelId, State state) {
320         Channel channel = thing.getChannel(channelId);
321         if (channel != null) {
322             updateState(channel.getUID(), state);
323         } else {
324             logger.debug("Channel '{}' in thing '{}' does not exist, recreating thing.", channelId, thing.getUID());
325             createChannel(channelId);
326         }
327     }
328
329     /**
330      * Creates a {@link ChannelTypeUID} from the given channel id.
331      *
332      * @param channelId ID of the channel type UID to be created.
333      * @return the channel type UID
334      */
335     private ChannelTypeUID createChannelTypeUID(String channelId) {
336         int pos = channelId.indexOf(ChannelUID.CHANNEL_GROUP_SEPARATOR);
337         String id = pos > -1 ? channelId.substring(pos + 1) : channelId;
338         final ChannelTypeUID channelTypeUID;
339         switch (id) {
340             case CHANNEL_BATTERY:
341                 channelTypeUID = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_BATTERY_LEVEL.getUID();
342                 break;
343             case CHANNEL_VOLTAGE:
344                 channelTypeUID = DefaultSystemChannelTypeProvider.SYSTEM_ELECTRIC_VOLTAGE.getUID();
345                 break;
346             default:
347                 channelTypeUID = new ChannelTypeUID(BINDING_ID, id);
348                 break;
349         }
350         return channelTypeUID;
351     }
352
353     /**
354      * Creates new channels for the thing.
355      *
356      * @param channelId ID of the channel to be created.
357      */
358     private void createChannel(String channelId) {
359         ThingHandlerCallback callback = getCallback();
360         if (callback != null) {
361             final ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
362             final ChannelTypeUID channelTypeUID = createChannelTypeUID(channelId);
363             final Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).build();
364             updateThing(editThing().withoutChannel(channelUID).withChannel(channel).build());
365         }
366     }
367
368     /**
369      * Updates thing channel configurations.
370      *
371      * @param channelId ID of the channel which configuration to be updated.
372      * @param configId ID of the configuration to be updated.
373      * @param value Value to be set.
374      */
375     protected void updateThingChannelConfiguration(String channelId, String configId, Object value) {
376         Channel channel = thing.getChannel(channelId);
377         if (channel != null) {
378             Configuration editConfig = channel.getConfiguration();
379             editConfig.put(configId, value);
380         }
381     }
382
383     @Override
384     public void onDeviceGone(ThingUID thingUID) {
385         if (thing.getUID().equals(thingUID)) {
386             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Device not present in response");
387         }
388     }
389
390     @Override
391     public void handleCommand(ChannelUID channelUID, Command command) {
392         String channelId = channelUID.getIdWithoutGroup();
393         logger.debug("Handle command '{}' for channel {}", command, channelId);
394         if (command == RefreshType.REFRESH) {
395             handleRefreshCommand();
396             return;
397         }
398         FritzAhaWebInterface fritzBox = getWebInterface();
399         if (fritzBox == null) {
400             logger.debug("Cannot handle command '{}' because connection is missing", command);
401             return;
402         }
403         String ain = getIdentifier();
404         if (ain == null) {
405             logger.debug("Cannot handle command '{}' because AIN is missing", command);
406             return;
407         }
408         switch (channelId) {
409             case CHANNEL_MODE:
410             case CHANNEL_LOCKED:
411             case CHANNEL_DEVICE_LOCKED:
412             case CHANNEL_TEMPERATURE:
413             case CHANNEL_HUMIDITY:
414             case CHANNEL_ENERGY:
415             case CHANNEL_POWER:
416             case CHANNEL_VOLTAGE:
417             case CHANNEL_ACTUALTEMP:
418             case CHANNEL_ECOTEMP:
419             case CHANNEL_COMFORTTEMP:
420             case CHANNEL_NEXT_CHANGE:
421             case CHANNEL_NEXTTEMP:
422             case CHANNEL_BATTERY:
423             case CHANNEL_BATTERY_LOW:
424             case CHANNEL_CONTACT_STATE:
425             case CHANNEL_LAST_CHANGE:
426             case CHANNEL_OBSTRUCTION_ALARM:
427             case CHANNEL_TEMPERATURE_ALARM:
428                 logger.debug("Channel {} is a read-only channel and cannot handle command '{}'", channelId, command);
429                 break;
430             case CHANNEL_OUTLET:
431             case CHANNEL_ON_OFF:
432                 if (command instanceof OnOffType) {
433                     fritzBox.setSwitch(ain, OnOffType.ON.equals(command));
434                 }
435                 break;
436             case CHANNEL_COLOR:
437             case CHANNEL_BRIGHTNESS:
438                 BigDecimal brightness = null;
439                 if (command instanceof HSBType) {
440                     HSBType hsbType = (HSBType) command;
441                     brightness = hsbType.getBrightness().toBigDecimal();
442                     fritzBox.setUnmappedHueAndSaturation(ain, hsbType.getHue().intValue(),
443                             ColorControlModel.fromPercent(hsbType.getSaturation()), 0);
444                 } else if (command instanceof PercentType) {
445                     brightness = ((PercentType) command).toBigDecimal();
446                 } else if (command instanceof OnOffType) {
447                     fritzBox.setSwitch(ain, OnOffType.ON.equals(command));
448                 } else if (command instanceof IncreaseDecreaseType) {
449                     brightness = ((DeviceModel) currentDevice).getLevelControlModel().getLevelPercentage();
450                     if (IncreaseDecreaseType.INCREASE.equals(command)) {
451                         brightness.add(BigDecimal.TEN);
452                     } else {
453                         brightness.subtract(BigDecimal.TEN);
454                     }
455                 }
456                 if (brightness != null) {
457                     fritzBox.setLevelPercentage(ain, brightness);
458                 }
459                 break;
460             case CHANNEL_SETTEMP:
461                 BigDecimal temperature = null;
462                 if (command instanceof DecimalType) {
463                     temperature = normalizeCelsius(((DecimalType) command).toBigDecimal());
464                 } else if (command instanceof QuantityType) {
465                     @SuppressWarnings("unchecked")
466                     QuantityType<Temperature> convertedCommand = ((QuantityType<Temperature>) command)
467                             .toUnit(SIUnits.CELSIUS);
468                     if (convertedCommand != null) {
469                         temperature = normalizeCelsius(convertedCommand.toBigDecimal());
470                     } else {
471                         logger.warn("Unable to convert unit from '{}' to '{}'. Skipping command.",
472                                 ((QuantityType<?>) command).getUnit(), SIUnits.CELSIUS);
473                     }
474                 } else if (command instanceof IncreaseDecreaseType) {
475                     temperature = currentDevice.getHkr().getTsoll();
476                     if (IncreaseDecreaseType.INCREASE.equals(command)) {
477                         temperature.add(BigDecimal.ONE);
478                     } else {
479                         temperature.subtract(BigDecimal.ONE);
480                     }
481                 } else if (command instanceof OnOffType) {
482                     temperature = OnOffType.ON.equals(command) ? TEMP_FRITZ_ON : TEMP_FRITZ_OFF;
483                 }
484                 if (temperature != null) {
485                     fritzBox.setSetTemp(ain, fromCelsius(temperature));
486                     HeatingModel heatingModel = currentDevice.getHkr();
487                     heatingModel.setTsoll(temperature);
488                     updateState(CHANNEL_RADIATOR_MODE, new StringType(heatingModel.getRadiatorMode()));
489                 }
490                 break;
491             case CHANNEL_RADIATOR_MODE:
492                 BigDecimal targetTemperature = null;
493                 if (command instanceof StringType) {
494                     switch (command.toString()) {
495                         case MODE_ON:
496                             targetTemperature = TEMP_FRITZ_ON;
497                             break;
498                         case MODE_OFF:
499                             targetTemperature = TEMP_FRITZ_OFF;
500                             break;
501                         case MODE_COMFORT:
502                             targetTemperature = currentDevice.getHkr().getKomfort();
503                             break;
504                         case MODE_ECO:
505                             targetTemperature = currentDevice.getHkr().getAbsenk();
506                             break;
507                         case MODE_BOOST:
508                             targetTemperature = TEMP_FRITZ_MAX;
509                             break;
510                         case MODE_UNKNOWN:
511                         case MODE_WINDOW_OPEN:
512                             logger.debug("Command '{}' is a read-only command for channel {}.", command, channelId);
513                             break;
514                     }
515                     if (targetTemperature != null) {
516                         fritzBox.setSetTemp(ain, targetTemperature);
517                         currentDevice.getHkr().setTsoll(targetTemperature);
518                         updateState(CHANNEL_SETTEMP, new QuantityType<>(toCelsius(targetTemperature), SIUnits.CELSIUS));
519                     }
520                 }
521                 break;
522             case CHANNEL_ROLLERSHUTTER:
523                 if (command instanceof StopMoveType) {
524                     StopMoveType rollershutterCommand = (StopMoveType) command;
525                     if (StopMoveType.STOP.equals(rollershutterCommand)) {
526                         fritzBox.setBlind(ain, BlindCommand.STOP);
527                     } else {
528                         logger.debug("Received unknown rollershutter StopMove command MOVE");
529                     }
530                 } else if (command instanceof UpDownType) {
531                     UpDownType rollershutterCommand = (UpDownType) command;
532                     if (UpDownType.UP.equals(rollershutterCommand)) {
533                         fritzBox.setBlind(ain, BlindCommand.OPEN);
534                     } else {
535                         fritzBox.setBlind(ain, BlindCommand.CLOSE);
536                     }
537                 } else if (command instanceof PercentType) {
538                     BigDecimal levelPercentage = ((PercentType) command).toBigDecimal();
539                     fritzBox.setLevelPercentage(ain, levelPercentage);
540                 } else {
541                     logger.debug("Received unknown rollershutter command type '{}'", command.toString());
542                 }
543                 break;
544             default:
545                 logger.debug("Received unknown channel {}", channelId);
546                 break;
547         }
548     }
549
550     /**
551      * Handles a command for a given action.
552      *
553      * @param action
554      * @param duration
555      */
556     protected void handleAction(String action, long duration) {
557         FritzAhaWebInterface fritzBox = getWebInterface();
558         if (fritzBox == null) {
559             logger.debug("Cannot handle action '{}' because connection is missing", action);
560             return;
561         }
562         String ain = getIdentifier();
563         if (ain == null) {
564             logger.debug("Cannot handle action '{}' because AIN is missing", action);
565             return;
566         }
567         if (duration < 0 || 86400 < duration) {
568             throw new IllegalArgumentException("Duration must not be less than zero or greater than 86400");
569         }
570         switch (action) {
571             case MODE_BOOST:
572                 fritzBox.setBoostMode(ain,
573                         duration > 0 ? ZonedDateTime.now().plusSeconds(duration).toEpochSecond() : 0);
574                 break;
575             case MODE_WINDOW_OPEN:
576                 fritzBox.setWindowOpenMode(ain,
577                         duration > 0 ? ZonedDateTime.now().plusSeconds(duration).toEpochSecond() : 0);
578                 break;
579             default:
580                 logger.debug("Received unknown action '{}'", action);
581                 break;
582         }
583     }
584
585     /**
586      * Provides the web interface object.
587      *
588      * @return The web interface object
589      */
590     private @Nullable FritzAhaWebInterface getWebInterface() {
591         Bridge bridge = getBridge();
592         if (bridge != null) {
593             BridgeHandler handler = bridge.getHandler();
594             if (handler instanceof AVMFritzBaseBridgeHandler) {
595                 return ((AVMFritzBaseBridgeHandler) handler).getWebInterface();
596             }
597         }
598         return null;
599     }
600
601     /**
602      * Handles a refresh command.
603      */
604     private void handleRefreshCommand() {
605         Bridge bridge = getBridge();
606         if (bridge != null) {
607             BridgeHandler handler = bridge.getHandler();
608             if (handler instanceof AVMFritzBaseBridgeHandler) {
609                 ((AVMFritzBaseBridgeHandler) handler).handleRefreshCommand();
610             }
611         }
612     }
613
614     /**
615      * Returns the AIN.
616      *
617      * @return the AIN
618      */
619     public @Nullable String getIdentifier() {
620         return identifier;
621     }
622 }