2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.avmfritz.internal.handler;
15 import static org.openhab.binding.avmfritz.internal.AVMFritzBindingConstants.*;
16 import static org.openhab.binding.avmfritz.internal.dto.HeatingModel.*;
18 import java.math.BigDecimal;
19 import java.time.Instant;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
24 import javax.measure.quantity.Temperature;
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;
76 * Abstract handler for a FRITZ! thing. Handles commands, which are sent to one of the channels.
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
84 public abstract class AVMFritzBaseThingHandler extends BaseThingHandler implements FritzAhaStatusListener {
86 private final Logger logger = LoggerFactory.getLogger(AVMFritzBaseThingHandler.class);
89 * keeps track of the current state for handling of increase/decrease
91 private @Nullable AVMFritzBaseModel state;
92 private @Nullable String identifier;
97 * @param thing Thing object representing a FRITZ! device
99 public AVMFritzBaseThingHandler(Thing thing) {
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.");
111 this.identifier = newIdentifier;
112 updateStatus(ThingStatus.UNKNOWN);
117 public void onDeviceAdded(AVMFritzBaseModel device) {
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);
128 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device not present");
132 updateProperties(device, editProperties());
134 if (device.isPowermeter()) {
135 updatePowermeter(device.getPowermeter());
137 if (device.isSwitchableOutlet()) {
138 updateSwitchableOutlet(device.getSwitch());
140 if (device.isHeatingThermostat()) {
141 updateHeatingThermostat(device.getHkr());
143 if (device instanceof DeviceModel) {
144 DeviceModel deviceModel = (DeviceModel) device;
145 if (deviceModel.isTempSensor()) {
146 updateTemperatureSensor(deviceModel.getTemperature());
148 if (deviceModel.isHumiditySensor()) {
149 updateHumiditySensor(deviceModel.getHumidity());
151 if (deviceModel.isHANFUNAlarmSensor()) {
152 updateHANFUNAlarmSensor(deviceModel.getAlert());
154 if (deviceModel.isHANFUNBlinds()) {
155 updateLevelcontrol(deviceModel.getLevelcontrol());
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);
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());
177 protected void updateHumiditySensor(@Nullable HumidityModel humidityModel) {
178 if (humidityModel != null) {
179 updateThingChannelState(CHANNEL_HUMIDITY,
180 new QuantityType<>(humidityModel.getRelativeHumidity(), Units.PERCENT));
184 protected void updateLevelcontrol(@Nullable LevelcontrolModel levelcontrolModel) {
185 if (levelcontrolModel != null) {
186 updateThingChannelState(CHANNEL_ROLLERSHUTTER, new PercentType(levelcontrolModel.getLevelPercentage()));
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
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));
216 updateBattery(heatingModel);
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);
228 updateThingChannelState(CHANNEL_BATTERY_LOW,
229 BatteryModel.BATTERY_ON.equals(lowBattery) ? OnOffType.ON : OnOffType.OFF);
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();
242 updateThingChannelState(CHANNEL_OUTLET, UnDefType.UNDEF);
244 updateThingChannelState(CHANNEL_OUTLET, SwitchModel.ON.equals(state) ? OnOffType.ON : OnOffType.OFF);
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));
258 * Updates thing properties.
260 * @param device the {@link AVMFritzBaseModel}
261 * @param editProperties map of existing properties
263 protected void updateProperties(AVMFritzBaseModel device, Map<String, String> editProperties) {
264 editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion());
265 updateProperties(editProperties);
269 * Updates thing channels and creates dynamic channels if missing.
271 * @param channelId ID of the channel to be updated.
272 * @param state State to be set.
274 protected void updateThingChannelState(String channelId, State state) {
275 Channel channel = thing.getChannel(channelId);
276 if (channel != null) {
277 updateState(channel.getUID(), state);
279 logger.debug("Channel '{}' in thing '{}' does not exist, recreating thing.", channelId, thing.getUID());
280 createChannel(channelId);
285 * Creates a {@link ChannelTypeUID} from the given channel id.
287 * @param channelId ID of the channel type UID to be created.
288 * @return the channel type UID
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);
298 * Creates new channels for the thing.
300 * @param channelId ID of the channel to be created.
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());
313 * Updates thing channel configurations.
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.
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);
328 public void onDeviceGone(ThingUID thingUID) {
329 if (thing.getUID().equals(thingUID)) {
330 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Device not present in response");
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();
342 FritzAhaWebInterface fritzBox = getWebInterface();
343 if (fritzBox == null) {
344 logger.debug("Cannot handle command '{}' because connection is missing", command);
347 String ain = getIdentifier();
349 logger.debug("Cannot handle command '{}' because AIN is missing", command);
355 case CHANNEL_DEVICE_LOCKED:
356 case CHANNEL_TEMPERATURE:
357 case CHANNEL_HUMIDITY:
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);
373 if (command instanceof OnOffType) {
374 fritzBox.setSwitch(ain, OnOffType.ON.equals(command));
376 state.getSwitch().setState(OnOffType.ON.equals(command) ? SwitchModel.ON : SwitchModel.OFF);
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());
391 logger.warn("Unable to convert unit from '{}' to '{}'. Skipping command.",
392 ((QuantityType<?>) command).getUnit(), SIUnits.CELSIUS);
394 } else if (command instanceof IncreaseDecreaseType) {
395 temperature = state.getHkr().getTsoll();
396 if (IncreaseDecreaseType.INCREASE.equals(command)) {
397 temperature.add(BigDecimal.ONE);
399 temperature.subtract(BigDecimal.ONE);
401 } else if (command instanceof OnOffType) {
402 temperature = OnOffType.ON.equals(command) ? TEMP_FRITZ_ON : TEMP_FRITZ_OFF;
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()));
411 case CHANNEL_RADIATOR_MODE:
412 BigDecimal targetTemperature = null;
413 if (command instanceof StringType) {
414 switch (command.toString()) {
416 targetTemperature = TEMP_FRITZ_ON;
419 targetTemperature = TEMP_FRITZ_OFF;
422 targetTemperature = state.getHkr().getKomfort();
425 targetTemperature = state.getHkr().getAbsenk();
428 targetTemperature = TEMP_FRITZ_MAX;
431 case MODE_WINDOW_OPEN:
432 logger.debug("Command '{}' is a read-only command for channel {}.", command, channelId);
435 if (targetTemperature != null) {
436 fritzBox.setSetTemp(ain, targetTemperature);
437 state.getHkr().setTsoll(targetTemperature);
438 updateState(CHANNEL_SETTEMP, new QuantityType<>(toCelsius(targetTemperature), SIUnits.CELSIUS));
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);
448 logger.debug("Received unknown rollershutter StopMove command MOVE");
450 } else if (command instanceof UpDownType) {
451 UpDownType rollershutterCommand = (UpDownType) command;
452 if (UpDownType.UP.equals(rollershutterCommand)) {
453 fritzBox.setBlind(ain, BlindCommand.OPEN);
455 fritzBox.setBlind(ain, BlindCommand.CLOSE);
457 } else if (command instanceof PercentType) {
458 PercentType rollershutterCommand = (PercentType) command;
459 BigDecimal levelpercentage = rollershutterCommand.toBigDecimal();
460 fritzBox.setLevelpercentage(ain, levelpercentage);
462 logger.debug("Received unknown rollershutter command type '{}'", command.toString());
466 logger.debug("Received unknown channel {}", channelId);
472 * Handles a command for a given action.
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);
483 String ain = getIdentifier();
485 logger.debug("Cannot handle action '{}' because AIN is missing", action);
488 if (duration < 0 || 86400 < duration) {
489 throw new IllegalArgumentException("Duration must not be less than zero or greater than 86400");
493 fritzBox.setBoostMode(ain,
494 duration > 0 ? ZonedDateTime.now().plusSeconds(duration).toEpochSecond() : 0);
496 case MODE_WINDOW_OPEN:
497 fritzBox.setWindowOpenMode(ain,
498 duration > 0 ? ZonedDateTime.now().plusSeconds(duration).toEpochSecond() : 0);
501 logger.debug("Received unknown action '{}'", action);
507 * Provides the web interface object.
509 * @return The web interface object
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();
523 * Handles a refresh command.
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();
540 public @Nullable String getIdentifier() {