2 * Copyright (c) 2010-2020 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.PowerMeterModel;
36 import org.openhab.binding.avmfritz.internal.dto.SwitchModel;
37 import org.openhab.binding.avmfritz.internal.dto.TemperatureModel;
38 import org.openhab.binding.avmfritz.internal.hardware.FritzAhaStatusListener;
39 import org.openhab.binding.avmfritz.internal.hardware.FritzAhaWebInterface;
40 import org.openhab.core.config.core.Configuration;
41 import org.openhab.core.library.types.DateTimeType;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.IncreaseDecreaseType;
44 import org.openhab.core.library.types.OnOffType;
45 import org.openhab.core.library.types.OpenClosedType;
46 import org.openhab.core.library.types.QuantityType;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.library.unit.SIUnits;
49 import org.openhab.core.library.unit.SmartHomeUnits;
50 import org.openhab.core.thing.Bridge;
51 import org.openhab.core.thing.Channel;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.thing.ThingUID;
58 import org.openhab.core.thing.binding.BaseThingHandler;
59 import org.openhab.core.thing.binding.BridgeHandler;
60 import org.openhab.core.thing.binding.ThingHandlerCallback;
61 import org.openhab.core.thing.type.ChannelTypeUID;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.openhab.core.types.State;
65 import org.openhab.core.types.UnDefType;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
70 * Abstract handler for a FRITZ! thing. Handles commands, which are sent to one of the channels.
72 * @author Robert Bausdorf - Initial contribution
73 * @author Christoph Weitkamp - Added support for AVM FRITZ!DECT 300 and Comet DECT
74 * @author Christoph Weitkamp - Added support for groups
77 public abstract class AVMFritzBaseThingHandler extends BaseThingHandler implements FritzAhaStatusListener {
79 private final Logger logger = LoggerFactory.getLogger(AVMFritzBaseThingHandler.class);
82 * keeps track of the current state for handling of increase/decrease
84 private @Nullable AVMFritzBaseModel state;
85 private @NonNullByDefault({}) AVMFritzDeviceConfiguration config;
90 * @param thing Thing object representing a FRITZ! device
92 public AVMFritzBaseThingHandler(Thing thing) {
97 public void initialize() {
98 config = getConfigAs(AVMFritzDeviceConfiguration.class);
100 String newIdentifier = config.ain;
101 if (newIdentifier == null || newIdentifier.trim().isEmpty()) {
102 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
103 "The 'ain' parameter must be configured.");
105 updateStatus(ThingStatus.UNKNOWN);
110 public void onDeviceAdded(AVMFritzBaseModel device) {
115 public void onDeviceUpdated(ThingUID thingUID, AVMFritzBaseModel device) {
116 if (thing.getUID().equals(thingUID)) {
117 logger.debug("Update thing '{}' with device model: {}", thingUID, device);
118 if (device.getPresent() == 1) {
119 updateStatus(ThingStatus.ONLINE);
121 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device not present");
125 updateProperties(device, editProperties());
127 if (device.isPowermeter()) {
128 updatePowermeter(device.getPowermeter());
130 if (device.isSwitchableOutlet()) {
131 updateSwitchableOutlet(device.getSwitch());
133 if (device.isHeatingThermostat()) {
134 updateHeatingThermostat(device.getHkr());
136 if (device instanceof DeviceModel) {
137 DeviceModel deviceModel = (DeviceModel) device;
138 if (deviceModel.isTempSensor()) {
139 updateTemperatureSensor(deviceModel.getTemperature());
141 if (deviceModel.isHANFUNAlarmSensor()) {
142 updateHANFUNAlarmSensor(deviceModel.getAlert());
148 private void updateHANFUNAlarmSensor(@Nullable AlertModel alertModel) {
149 if (alertModel != null) {
150 updateThingChannelState(CHANNEL_CONTACT_STATE,
151 AlertModel.ON.equals(alertModel.getState()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
155 private void updateTemperatureSensor(@Nullable TemperatureModel temperatureModel) {
156 if (temperatureModel != null) {
157 updateThingChannelState(CHANNEL_TEMPERATURE,
158 new QuantityType<>(temperatureModel.getCelsius(), SIUnits.CELSIUS));
159 updateThingChannelConfiguration(CHANNEL_TEMPERATURE, CONFIG_CHANNEL_TEMP_OFFSET,
160 temperatureModel.getOffset());
164 private void updateHeatingThermostat(@Nullable HeatingModel heatingModel) {
165 if (heatingModel != null) {
166 updateThingChannelState(CHANNEL_MODE, new StringType(heatingModel.getMode()));
167 updateThingChannelState(CHANNEL_LOCKED,
168 BigDecimal.ZERO.equals(heatingModel.getLock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
169 updateThingChannelState(CHANNEL_DEVICE_LOCKED,
170 BigDecimal.ZERO.equals(heatingModel.getDevicelock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
171 updateThingChannelState(CHANNEL_ACTUALTEMP,
172 new QuantityType<>(toCelsius(heatingModel.getTist()), SIUnits.CELSIUS));
173 updateThingChannelState(CHANNEL_SETTEMP,
174 new QuantityType<>(toCelsius(heatingModel.getTsoll()), SIUnits.CELSIUS));
175 updateThingChannelState(CHANNEL_ECOTEMP,
176 new QuantityType<>(toCelsius(heatingModel.getAbsenk()), SIUnits.CELSIUS));
177 updateThingChannelState(CHANNEL_COMFORTTEMP,
178 new QuantityType<>(toCelsius(heatingModel.getKomfort()), SIUnits.CELSIUS));
179 updateThingChannelState(CHANNEL_RADIATOR_MODE, new StringType(heatingModel.getRadiatorMode()));
180 NextChangeModel nextChange = heatingModel.getNextchange();
181 if (nextChange != null) {
182 int endPeriod = nextChange.getEndperiod();
183 updateThingChannelState(CHANNEL_NEXT_CHANGE, endPeriod == 0 ? UnDefType.UNDEF
185 ZonedDateTime.ofInstant(Instant.ofEpochSecond(endPeriod), ZoneId.systemDefault())));
186 BigDecimal nextTemperature = nextChange.getTchange();
187 updateThingChannelState(CHANNEL_NEXTTEMP, TEMP_FRITZ_UNDEFINED.equals(nextTemperature) ? UnDefType.UNDEF
188 : new QuantityType<>(toCelsius(nextTemperature), SIUnits.CELSIUS));
190 updateBattery(heatingModel);
194 protected void updateBattery(BatteryModel batteryModel) {
195 BigDecimal batteryLevel = batteryModel.getBattery();
196 updateThingChannelState(CHANNEL_BATTERY,
197 batteryLevel == null ? UnDefType.UNDEF : new DecimalType(batteryLevel));
198 BigDecimal lowBattery = batteryModel.getBatterylow();
199 if (lowBattery == null) {
200 updateThingChannelState(CHANNEL_BATTERY_LOW, UnDefType.UNDEF);
202 updateThingChannelState(CHANNEL_BATTERY_LOW,
203 BatteryModel.BATTERY_ON.equals(lowBattery) ? OnOffType.ON : OnOffType.OFF);
207 private void updateSwitchableOutlet(@Nullable SwitchModel switchModel) {
208 if (switchModel != null) {
209 updateThingChannelState(CHANNEL_MODE, new StringType(switchModel.getMode()));
210 updateThingChannelState(CHANNEL_LOCKED,
211 BigDecimal.ZERO.equals(switchModel.getLock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
212 updateThingChannelState(CHANNEL_DEVICE_LOCKED,
213 BigDecimal.ZERO.equals(switchModel.getDevicelock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
214 BigDecimal state = switchModel.getState();
216 updateThingChannelState(CHANNEL_OUTLET, UnDefType.UNDEF);
218 updateThingChannelState(CHANNEL_OUTLET, SwitchModel.ON.equals(state) ? OnOffType.ON : OnOffType.OFF);
223 private void updatePowermeter(@Nullable PowerMeterModel powerMeterModel) {
224 if (powerMeterModel != null) {
225 updateThingChannelState(CHANNEL_ENERGY,
226 new QuantityType<>(powerMeterModel.getEnergy(), SmartHomeUnits.WATT_HOUR));
227 updateThingChannelState(CHANNEL_POWER, new QuantityType<>(powerMeterModel.getPower(), SmartHomeUnits.WATT));
228 updateThingChannelState(CHANNEL_VOLTAGE,
229 new QuantityType<>(powerMeterModel.getVoltage(), SmartHomeUnits.VOLT));
234 * Updates thing properties.
236 * @param device the {@link AVMFritzBaseModel}
237 * @param editProperties map of existing properties
239 protected void updateProperties(AVMFritzBaseModel device, Map<String, String> editProperties) {
240 editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion());
241 updateProperties(editProperties);
245 * Updates thing channels and creates dynamic channels if missing.
247 * @param channelId ID of the channel to be updated.
248 * @param state State to be set.
250 protected void updateThingChannelState(String channelId, State state) {
251 Channel channel = thing.getChannel(channelId);
252 if (channel != null) {
253 updateState(channel.getUID(), state);
255 logger.debug("Channel '{}' in thing '{}' does not exist, recreating thing.", channelId, thing.getUID());
256 createChannel(channelId);
261 * Creates new channels for the thing.
263 * @param channelId ID of the channel to be created.
265 private void createChannel(String channelId) {
266 ThingHandlerCallback callback = getCallback();
267 if (callback != null) {
268 ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
269 ChannelTypeUID channelTypeUID = CHANNEL_BATTERY.equals(channelId)
270 ? DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_BATTERY_LEVEL.getUID()
271 : new ChannelTypeUID(BINDING_ID, channelId);
272 Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).build();
273 updateThing(editThing().withoutChannel(channelUID).withChannel(channel).build());
278 * Updates thing channel configurations.
280 * @param channelId ID of the channel which configuration to be updated.
281 * @param configId ID of the configuration to be updated.
282 * @param value Value to be set.
284 private void updateThingChannelConfiguration(String channelId, String configId, Object value) {
285 Channel channel = thing.getChannel(channelId);
286 if (channel != null) {
287 Configuration editConfig = channel.getConfiguration();
288 editConfig.put(configId, value);
293 public void onDeviceGone(ThingUID thingUID) {
294 if (thing.getUID().equals(thingUID)) {
295 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Device not present in response");
300 public void handleCommand(ChannelUID channelUID, Command command) {
301 String channelId = channelUID.getIdWithoutGroup();
302 logger.debug("Handle command '{}' for channel {}", command, channelId);
303 if (command == RefreshType.REFRESH) {
304 handleRefreshCommand();
307 FritzAhaWebInterface fritzBox = getWebInterface();
308 if (fritzBox == null) {
309 logger.debug("Cannot handle command '{}' because connection is missing", command);
312 String ain = getIdentifier();
314 logger.debug("Cannot handle command '{}' because AIN is missing", command);
320 case CHANNEL_DEVICE_LOCKED:
321 case CHANNEL_TEMPERATURE:
324 case CHANNEL_VOLTAGE:
325 case CHANNEL_ACTUALTEMP:
326 case CHANNEL_ECOTEMP:
327 case CHANNEL_COMFORTTEMP:
328 case CHANNEL_NEXT_CHANGE:
329 case CHANNEL_NEXTTEMP:
330 case CHANNEL_BATTERY:
331 case CHANNEL_BATTERY_LOW:
332 case CHANNEL_CONTACT_STATE:
333 case CHANNEL_LAST_CHANGE:
334 logger.debug("Channel {} is a read-only channel and cannot handle command '{}'", channelId, command);
337 if (command instanceof OnOffType) {
338 fritzBox.setSwitch(ain, OnOffType.ON.equals(command));
340 state.getSwitch().setState(OnOffType.ON.equals(command) ? SwitchModel.ON : SwitchModel.OFF);
344 case CHANNEL_SETTEMP:
345 BigDecimal temperature = null;
346 if (command instanceof DecimalType) {
347 temperature = normalizeCelsius(((DecimalType) command).toBigDecimal());
348 } else if (command instanceof QuantityType) {
349 temperature = normalizeCelsius(
350 ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS).toBigDecimal());
351 } else if (command instanceof IncreaseDecreaseType) {
352 temperature = state.getHkr().getTsoll();
353 if (IncreaseDecreaseType.INCREASE.equals(command)) {
354 temperature.add(BigDecimal.ONE);
356 temperature.subtract(BigDecimal.ONE);
358 } else if (command instanceof OnOffType) {
359 temperature = OnOffType.ON.equals(command) ? TEMP_FRITZ_ON : TEMP_FRITZ_OFF;
361 if (temperature != null) {
362 fritzBox.setSetTemp(ain, fromCelsius(temperature));
363 HeatingModel heatingModel = state.getHkr();
364 heatingModel.setTsoll(temperature);
365 updateState(CHANNEL_RADIATOR_MODE, new StringType(heatingModel.getRadiatorMode()));
368 case CHANNEL_RADIATOR_MODE:
369 BigDecimal targetTemperature = null;
370 if (command instanceof StringType) {
371 switch (command.toString()) {
373 targetTemperature = TEMP_FRITZ_ON;
376 targetTemperature = TEMP_FRITZ_OFF;
379 targetTemperature = state.getHkr().getKomfort();
382 targetTemperature = state.getHkr().getAbsenk();
385 targetTemperature = TEMP_FRITZ_MAX;
388 case MODE_WINDOW_OPEN:
389 logger.debug("Command '{}' is a read-only command for channel {}.", command, channelId);
392 if (targetTemperature != null) {
393 fritzBox.setSetTemp(ain, targetTemperature);
394 state.getHkr().setTsoll(targetTemperature);
395 updateState(CHANNEL_SETTEMP, new QuantityType<>(toCelsius(targetTemperature), SIUnits.CELSIUS));
400 logger.debug("Received unknown channel {}", channelId);
406 * Handles a command for a given action.
411 protected void handleAction(String action, long duration) {
412 FritzAhaWebInterface fritzBox = getWebInterface();
413 if (fritzBox == null) {
414 logger.debug("Cannot handle action '{}' because connection is missing", action);
417 String ain = getIdentifier();
419 logger.debug("Cannot handle action '{}' because AIN is missing", action);
422 if (duration < 0 || 86400 < duration) {
423 throw new IllegalArgumentException("Duration must not be less than zero or greater than 86400");
427 fritzBox.setBoostMode(ain,
428 duration > 0 ? ZonedDateTime.now().plusSeconds(duration).toEpochSecond() : 0);
430 case MODE_WINDOW_OPEN:
431 fritzBox.setWindowOpenMode(ain,
432 duration > 0 ? ZonedDateTime.now().plusSeconds(duration).toEpochSecond() : 0);
435 logger.debug("Received unknown action '{}'", action);
441 * Provides the web interface object.
443 * @return The web interface object
445 private @Nullable FritzAhaWebInterface getWebInterface() {
446 Bridge bridge = getBridge();
447 if (bridge != null) {
448 BridgeHandler handler = bridge.getHandler();
449 if (handler instanceof AVMFritzBaseBridgeHandler) {
450 return ((AVMFritzBaseBridgeHandler) handler).getWebInterface();
457 * Handles a refresh command.
459 private void handleRefreshCommand() {
460 Bridge bridge = getBridge();
461 if (bridge != null) {
462 BridgeHandler handler = bridge.getHandler();
463 if (handler instanceof AVMFritzBaseBridgeHandler) {
464 ((AVMFritzBaseBridgeHandler) handler).handleRefreshCommand();
474 public @Nullable String getIdentifier() {