]> git.basschouten.com Git - openhab-addons.git/blob
e8c6bcdf24e4aa12f54e7123bb75abba43530ecd
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.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;
68
69 /**
70  * Abstract handler for a FRITZ! thing. Handles commands, which are sent to one of the channels.
71  *
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
75  */
76 @NonNullByDefault
77 public abstract class AVMFritzBaseThingHandler extends BaseThingHandler implements FritzAhaStatusListener {
78
79     private final Logger logger = LoggerFactory.getLogger(AVMFritzBaseThingHandler.class);
80
81     /**
82      * keeps track of the current state for handling of increase/decrease
83      */
84     private @Nullable AVMFritzBaseModel state;
85     private @NonNullByDefault({}) AVMFritzDeviceConfiguration config;
86
87     /**
88      * Constructor
89      *
90      * @param thing Thing object representing a FRITZ! device
91      */
92     public AVMFritzBaseThingHandler(Thing thing) {
93         super(thing);
94     }
95
96     @Override
97     public void initialize() {
98         config = getConfigAs(AVMFritzDeviceConfiguration.class);
99
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.");
104         } else {
105             updateStatus(ThingStatus.UNKNOWN);
106         }
107     }
108
109     @Override
110     public void onDeviceAdded(AVMFritzBaseModel device) {
111         // nothing to do
112     }
113
114     @Override
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);
120             } else {
121                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device not present");
122             }
123             state = device;
124
125             updateProperties(device, editProperties());
126
127             if (device.isPowermeter()) {
128                 updatePowermeter(device.getPowermeter());
129             }
130             if (device.isSwitchableOutlet()) {
131                 updateSwitchableOutlet(device.getSwitch());
132             }
133             if (device.isHeatingThermostat()) {
134                 updateHeatingThermostat(device.getHkr());
135             }
136             if (device instanceof DeviceModel) {
137                 DeviceModel deviceModel = (DeviceModel) device;
138                 if (deviceModel.isTempSensor()) {
139                     updateTemperatureSensor(deviceModel.getTemperature());
140                 }
141                 if (deviceModel.isHANFUNAlarmSensor()) {
142                     updateHANFUNAlarmSensor(deviceModel.getAlert());
143                 }
144             }
145         }
146     }
147
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);
152         }
153     }
154
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());
161         }
162     }
163
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
184                         : new DateTimeType(
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));
189             }
190             updateBattery(heatingModel);
191         }
192     }
193
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);
201         } else {
202             updateThingChannelState(CHANNEL_BATTERY_LOW,
203                     BatteryModel.BATTERY_ON.equals(lowBattery) ? OnOffType.ON : OnOffType.OFF);
204         }
205     }
206
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();
215             if (state == null) {
216                 updateThingChannelState(CHANNEL_OUTLET, UnDefType.UNDEF);
217             } else {
218                 updateThingChannelState(CHANNEL_OUTLET, SwitchModel.ON.equals(state) ? OnOffType.ON : OnOffType.OFF);
219             }
220         }
221     }
222
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));
230         }
231     }
232
233     /**
234      * Updates thing properties.
235      *
236      * @param device the {@link AVMFritzBaseModel}
237      * @param editProperties map of existing properties
238      */
239     protected void updateProperties(AVMFritzBaseModel device, Map<String, String> editProperties) {
240         editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion());
241         updateProperties(editProperties);
242     }
243
244     /**
245      * Updates thing channels and creates dynamic channels if missing.
246      *
247      * @param channelId ID of the channel to be updated.
248      * @param state State to be set.
249      */
250     protected void updateThingChannelState(String channelId, State state) {
251         Channel channel = thing.getChannel(channelId);
252         if (channel != null) {
253             updateState(channel.getUID(), state);
254         } else {
255             logger.debug("Channel '{}' in thing '{}' does not exist, recreating thing.", channelId, thing.getUID());
256             createChannel(channelId);
257         }
258     }
259
260     /**
261      * Creates new channels for the thing.
262      *
263      * @param channelId ID of the channel to be created.
264      */
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());
274         }
275     }
276
277     /**
278      * Updates thing channel configurations.
279      *
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.
283      */
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);
289         }
290     }
291
292     @Override
293     public void onDeviceGone(ThingUID thingUID) {
294         if (thing.getUID().equals(thingUID)) {
295             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Device not present in response");
296         }
297     }
298
299     @Override
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();
305             return;
306         }
307         FritzAhaWebInterface fritzBox = getWebInterface();
308         if (fritzBox == null) {
309             logger.debug("Cannot handle command '{}' because connection is missing", command);
310             return;
311         }
312         String ain = getIdentifier();
313         if (ain == null) {
314             logger.debug("Cannot handle command '{}' because AIN is missing", command);
315             return;
316         }
317         switch (channelId) {
318             case CHANNEL_MODE:
319             case CHANNEL_LOCKED:
320             case CHANNEL_DEVICE_LOCKED:
321             case CHANNEL_TEMPERATURE:
322             case CHANNEL_ENERGY:
323             case CHANNEL_POWER:
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);
335                 break;
336             case CHANNEL_OUTLET:
337                 if (command instanceof OnOffType) {
338                     fritzBox.setSwitch(ain, OnOffType.ON.equals(command));
339                     if (state != null) {
340                         state.getSwitch().setState(OnOffType.ON.equals(command) ? SwitchModel.ON : SwitchModel.OFF);
341                     }
342                 }
343                 break;
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);
355                     } else {
356                         temperature.subtract(BigDecimal.ONE);
357                     }
358                 } else if (command instanceof OnOffType) {
359                     temperature = OnOffType.ON.equals(command) ? TEMP_FRITZ_ON : TEMP_FRITZ_OFF;
360                 }
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()));
366                 }
367                 break;
368             case CHANNEL_RADIATOR_MODE:
369                 BigDecimal targetTemperature = null;
370                 if (command instanceof StringType) {
371                     switch (command.toString()) {
372                         case MODE_ON:
373                             targetTemperature = TEMP_FRITZ_ON;
374                             break;
375                         case MODE_OFF:
376                             targetTemperature = TEMP_FRITZ_OFF;
377                             break;
378                         case MODE_COMFORT:
379                             targetTemperature = state.getHkr().getKomfort();
380                             break;
381                         case MODE_ECO:
382                             targetTemperature = state.getHkr().getAbsenk();
383                             break;
384                         case MODE_BOOST:
385                             targetTemperature = TEMP_FRITZ_MAX;
386                             break;
387                         case MODE_UNKNOWN:
388                         case MODE_WINDOW_OPEN:
389                             logger.debug("Command '{}' is a read-only command for channel {}.", command, channelId);
390                             break;
391                     }
392                     if (targetTemperature != null) {
393                         fritzBox.setSetTemp(ain, targetTemperature);
394                         state.getHkr().setTsoll(targetTemperature);
395                         updateState(CHANNEL_SETTEMP, new QuantityType<>(toCelsius(targetTemperature), SIUnits.CELSIUS));
396                     }
397                 }
398                 break;
399             default:
400                 logger.debug("Received unknown channel {}", channelId);
401                 break;
402         }
403     }
404
405     /**
406      * Handles a command for a given action.
407      *
408      * @param action
409      * @param duration
410      */
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);
415             return;
416         }
417         String ain = getIdentifier();
418         if (ain == null) {
419             logger.debug("Cannot handle action '{}' because AIN is missing", action);
420             return;
421         }
422         if (duration < 0 || 86400 < duration) {
423             throw new IllegalArgumentException("Duration must not be less than zero or greater than 86400");
424         }
425         switch (action) {
426             case MODE_BOOST:
427                 fritzBox.setBoostMode(ain,
428                         duration > 0 ? ZonedDateTime.now().plusSeconds(duration).toEpochSecond() : 0);
429                 break;
430             case MODE_WINDOW_OPEN:
431                 fritzBox.setWindowOpenMode(ain,
432                         duration > 0 ? ZonedDateTime.now().plusSeconds(duration).toEpochSecond() : 0);
433                 break;
434             default:
435                 logger.debug("Received unknown action '{}'", action);
436                 break;
437         }
438     }
439
440     /**
441      * Provides the web interface object.
442      *
443      * @return The web interface object
444      */
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();
451             }
452         }
453         return null;
454     }
455
456     /**
457      * Handles a refresh command.
458      */
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();
465             }
466         }
467     }
468
469     /**
470      * Returns the AIN.
471      *
472      * @return the AIN
473      */
474     public @Nullable String getIdentifier() {
475         return config.ain;
476     }
477 }