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