]> git.basschouten.com Git - openhab-addons.git/blob
b3a1ed174c197195cec7d6e210bbe0f76ee617d4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.plugwiseha.internal.handler;
14
15 import static org.openhab.binding.plugwiseha.internal.PlugwiseHABindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.*;
17 import static org.openhab.core.thing.ThingStatus.*;
18 import static org.openhab.core.thing.ThingStatusDetail.BRIDGE_OFFLINE;
19 import static org.openhab.core.thing.ThingStatusDetail.COMMUNICATION_ERROR;
20 import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
21
22 import java.util.List;
23 import java.util.Map;
24
25 import javax.measure.Unit;
26 import javax.measure.quantity.Dimensionless;
27 import javax.measure.quantity.Power;
28 import javax.measure.quantity.Pressure;
29 import javax.measure.quantity.Temperature;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.plugwiseha.internal.PlugwiseHABindingConstants;
34 import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAException;
35 import org.openhab.binding.plugwiseha.internal.api.model.PlugwiseHAController;
36 import org.openhab.binding.plugwiseha.internal.api.model.dto.Appliance;
37 import org.openhab.binding.plugwiseha.internal.config.PlugwiseHAThingConfig;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.unit.ImperialUnits;
41 import org.openhab.core.library.unit.SIUnits;
42 import org.openhab.core.library.unit.Units;
43 import org.openhab.core.thing.Channel;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingTypeUID;
47 import org.openhab.core.thing.binding.builder.ChannelBuilder;
48 import org.openhab.core.thing.binding.builder.ThingBuilder;
49 import org.openhab.core.thing.type.ChannelKind;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.State;
52 import org.openhab.core.types.UnDefType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 /**
57  * The {@link PlugwiseHAApplianceHandler} class is responsible for handling
58  * commands and status updates for the Plugwise Home Automation appliances.
59  * Extends @{link PlugwiseHABaseHandler}
60  *
61  * @author Bas van Wetten - Initial contribution
62  * @author Leo Siepel - finish initial contribution
63  *
64  */
65 @NonNullByDefault
66 public class PlugwiseHAApplianceHandler extends PlugwiseHABaseHandler<Appliance, PlugwiseHAThingConfig> {
67
68     private @Nullable Appliance appliance;
69     private final Logger logger = LoggerFactory.getLogger(PlugwiseHAApplianceHandler.class);
70
71     // Constructor
72
73     public PlugwiseHAApplianceHandler(Thing thing) {
74         super(thing);
75     }
76
77     public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
78         return PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_VALVE.equals(thingTypeUID)
79                 || PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_PUMP.equals(thingTypeUID)
80                 || PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_BOILER.equals(thingTypeUID)
81                 || PlugwiseHABindingConstants.THING_TYPE_APPLIANCE_THERMOSTAT.equals(thingTypeUID);
82     }
83
84     // Overrides
85
86     @Override
87     protected synchronized void initialize(PlugwiseHAThingConfig config, PlugwiseHABridgeHandler bridgeHandler) {
88         if (thing.getStatus() == INITIALIZING) {
89             logger.debug("Initializing Plugwise Home Automation appliance handler with config = {}", config);
90             if (!config.isValid()) {
91                 updateStatus(OFFLINE, CONFIGURATION_ERROR,
92                         "Invalid configuration for Plugwise Home Automation appliance handler.");
93                 return;
94             }
95
96             try {
97                 PlugwiseHAController controller = bridgeHandler.getController();
98                 if (controller != null) {
99                     this.appliance = getEntity(controller, true);
100                     Appliance localAppliance = this.appliance;
101                     if (localAppliance != null) {
102                         if (localAppliance.isBatteryOperated()) {
103                             addBatteryChannels();
104                         }
105                         setApplianceProperties();
106                         updateStatus(ONLINE);
107                     } else {
108                         updateStatus(OFFLINE);
109                     }
110                 } else {
111                     updateStatus(OFFLINE, BRIDGE_OFFLINE);
112                 }
113             } catch (PlugwiseHAException e) {
114                 updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getMessage());
115             }
116         }
117     }
118
119     @Override
120     protected @Nullable Appliance getEntity(PlugwiseHAController controller, Boolean forceRefresh)
121             throws PlugwiseHAException {
122         PlugwiseHAThingConfig config = getPlugwiseThingConfig();
123         Appliance appliance = controller.getAppliance(config.getId(), forceRefresh);
124
125         return appliance;
126     }
127
128     @Override
129     protected void handleCommand(Appliance entity, ChannelUID channelUID, Command command) throws PlugwiseHAException {
130         String channelID = channelUID.getIdWithoutGroup();
131
132         PlugwiseHABridgeHandler bridge = this.getPlugwiseHABridge();
133         if (bridge == null) {
134             return;
135         }
136
137         PlugwiseHAController controller = bridge.getController();
138         if (controller == null) {
139             return;
140         }
141
142         switch (channelID) {
143             case APPLIANCE_LOCK_CHANNEL:
144                 if (command instanceof OnOffType) {
145                     try {
146                         if (command == OnOffType.ON) {
147                             controller.switchRelayLockOn(entity);
148                         } else {
149                             controller.switchRelayLockOff(entity);
150                         }
151                     } catch (PlugwiseHAException e) {
152                         logger.warn("Unable to switch relay lock {} for appliance '{}'", (State) command,
153                                 entity.getName());
154                     }
155                 }
156                 break;
157             case APPLIANCE_OFFSET_CHANNEL:
158                 if (command instanceof QuantityType) {
159                     Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
160                             ? SIUnits.CELSIUS
161                             : ImperialUnits.FAHRENHEIT;
162                     QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
163
164                     if (state != null) {
165                         try {
166                             controller.setOffsetTemperature(entity, state.doubleValue());
167                         } catch (PlugwiseHAException e) {
168                             logger.warn("Unable to update setpoint for zone '{}': {} -> {}", entity.getName(),
169                                     entity.getSetpointTemperature().orElse(null), state.doubleValue());
170                         }
171                     }
172                 }
173                 break;
174             case APPLIANCE_POWER_CHANNEL:
175                 if (command instanceof OnOffType) {
176                     try {
177                         if (command == OnOffType.ON) {
178                             controller.switchRelayOn(entity);
179                         } else {
180                             controller.switchRelayOff(entity);
181                         }
182                     } catch (PlugwiseHAException e) {
183                         logger.warn("Unable to switch relay {} for appliance '{}'", (State) command, entity.getName());
184                     }
185                 }
186                 break;
187             case APPLIANCE_SETPOINT_CHANNEL:
188                 if (command instanceof QuantityType) {
189                     Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
190                             .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
191                     QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
192
193                     if (state != null) {
194                         try {
195                             controller.setThermostat(entity, state.doubleValue());
196                         } catch (PlugwiseHAException e) {
197                             logger.warn("Unable to update setpoint for appliance '{}': {} -> {}", entity.getName(),
198                                     entity.getSetpointTemperature().orElse(null), state.doubleValue());
199                         }
200                     }
201                 }
202                 break;
203             default:
204                 logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
205         }
206     }
207
208     private State getDefaultState(String channelID) {
209         State state = UnDefType.NULL;
210         switch (channelID) {
211             case APPLIANCE_BATTERYLEVEL_CHANNEL:
212             case APPLIANCE_CHSTATE_CHANNEL:
213             case APPLIANCE_DHWSTATE_CHANNEL:
214             case APPLIANCE_COOLINGSTATE_CHANNEL:
215             case APPLIANCE_INTENDEDBOILERTEMP_CHANNEL:
216             case APPLIANCE_FLAMESTATE_CHANNEL:
217             case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
218             case APPLIANCE_MODULATIONLEVEL_CHANNEL:
219             case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
220             case APPLIANCE_DHWTEMPERATURE_CHANNEL:
221             case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
222             case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
223             case APPLIANCE_DHWSETPOINT_CHANNEL:
224             case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
225             case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
226             case APPLIANCE_OFFSET_CHANNEL:
227             case APPLIANCE_POWER_USAGE_CHANNEL:
228             case APPLIANCE_SETPOINT_CHANNEL:
229             case APPLIANCE_TEMPERATURE_CHANNEL:
230             case APPLIANCE_VALVEPOSITION_CHANNEL:
231             case APPLIANCE_WATERPRESSURE_CHANNEL:
232                 state = UnDefType.NULL;
233                 break;
234             case APPLIANCE_BATTERYLEVELLOW_CHANNEL:
235             case APPLIANCE_LOCK_CHANNEL:
236             case APPLIANCE_POWER_CHANNEL:
237                 state = UnDefType.UNDEF;
238                 break;
239         }
240         return state;
241     }
242
243     @Override
244     protected void refreshChannel(Appliance entity, ChannelUID channelUID) {
245         String channelID = channelUID.getIdWithoutGroup();
246         State state = getDefaultState(channelID);
247         PlugwiseHAThingConfig config = getPlugwiseThingConfig();
248
249         switch (channelID) {
250             case APPLIANCE_BATTERYLEVEL_CHANNEL: {
251                 Double batteryLevel = entity.getBatteryLevel().orElse(null);
252
253                 if (batteryLevel != null) {
254                     batteryLevel = batteryLevel * 100;
255                     state = new QuantityType<Dimensionless>(batteryLevel.intValue(), Units.PERCENT);
256                     if (batteryLevel <= config.getLowBatteryPercentage()) {
257                         updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.ON);
258                     } else {
259                         updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.OFF);
260                     }
261                 }
262                 break;
263             }
264             case APPLIANCE_BATTERYLEVELLOW_CHANNEL: {
265                 Double batteryLevel = entity.getBatteryLevel().orElse(null);
266
267                 if (batteryLevel != null) {
268                     batteryLevel *= 100;
269                     if (batteryLevel <= config.getLowBatteryPercentage()) {
270                         state = OnOffType.ON;
271                     } else {
272                         state = OnOffType.OFF;
273                     }
274                 }
275                 break;
276             }
277             case APPLIANCE_CHSTATE_CHANNEL:
278                 if (entity.getCHState().isPresent()) {
279                     state = OnOffType.from(entity.getCHState().get());
280                 }
281                 break;
282             case APPLIANCE_DHWSTATE_CHANNEL:
283                 if (entity.getDHWState().isPresent()) {
284                     state = OnOffType.from(entity.getDHWState().get());
285                 }
286                 break;
287             case APPLIANCE_LOCK_CHANNEL:
288                 Boolean relayLockState = entity.getRelayLockState().orElse(null);
289                 if (relayLockState != null) {
290                     state = OnOffType.from(relayLockState);
291                 }
292                 break;
293             case APPLIANCE_OFFSET_CHANNEL:
294                 if (entity.getOffsetTemperature().isPresent()) {
295                     Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
296                             ? SIUnits.CELSIUS
297                             : ImperialUnits.FAHRENHEIT;
298                     state = new QuantityType<Temperature>(entity.getOffsetTemperature().get(), unit);
299                 }
300                 break;
301             case APPLIANCE_POWER_CHANNEL:
302                 if (entity.getRelayState().isPresent()) {
303                     state = OnOffType.from(entity.getRelayState().get());
304                 }
305                 break;
306             case APPLIANCE_POWER_USAGE_CHANNEL:
307                 if (entity.getPowerUsage().isPresent()) {
308                     state = new QuantityType<Power>(entity.getPowerUsage().get(), Units.WATT);
309                 }
310                 break;
311             case APPLIANCE_SETPOINT_CHANNEL:
312                 if (entity.getSetpointTemperature().isPresent()) {
313                     Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
314                             .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
315                     state = new QuantityType<Temperature>(entity.getSetpointTemperature().get(), unit);
316                 }
317                 break;
318             case APPLIANCE_TEMPERATURE_CHANNEL:
319                 if (entity.getTemperature().isPresent()) {
320                     Unit<Temperature> unit = entity.getTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
321                             ? SIUnits.CELSIUS
322                             : ImperialUnits.FAHRENHEIT;
323                     state = new QuantityType<Temperature>(entity.getTemperature().get(), unit);
324                 }
325                 break;
326             case APPLIANCE_VALVEPOSITION_CHANNEL:
327                 if (entity.getValvePosition().isPresent()) {
328                     Double valvePosition = entity.getValvePosition().get() * 100;
329                     state = new QuantityType<Dimensionless>(valvePosition.intValue(), Units.PERCENT);
330                 }
331                 break;
332             case APPLIANCE_WATERPRESSURE_CHANNEL:
333                 if (entity.getWaterPressure().isPresent()) {
334                     Unit<Pressure> unit = HECTO(SIUnits.PASCAL);
335                     state = new QuantityType<Pressure>(entity.getWaterPressure().get(), unit);
336                 }
337                 break;
338             case APPLIANCE_COOLINGSTATE_CHANNEL:
339                 if (entity.getCoolingState().isPresent()) {
340                     state = OnOffType.from(entity.getCoolingState().get());
341                 }
342                 break;
343             case APPLIANCE_INTENDEDBOILERTEMP_CHANNEL:
344                 if (entity.getIntendedBoilerTemp().isPresent()) {
345                     Unit<Temperature> unit = entity.getIntendedBoilerTempUnit().orElse(UNIT_CELSIUS)
346                             .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
347                     state = new QuantityType<Temperature>(entity.getIntendedBoilerTemp().get(), unit);
348                 }
349                 break;
350             case APPLIANCE_FLAMESTATE_CHANNEL:
351                 if (entity.getFlameState().isPresent()) {
352                     state = OnOffType.from(entity.getFlameState().get());
353                 }
354                 break;
355             case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
356                 if (entity.getIntendedHeatingState().isPresent()) {
357                     state = OnOffType.from(entity.getIntendedHeatingState().get());
358                 }
359                 break;
360             case APPLIANCE_MODULATIONLEVEL_CHANNEL:
361                 if (entity.getModulationLevel().isPresent()) {
362                     Double modulationLevel = entity.getModulationLevel().get() * 100;
363                     state = new QuantityType<Dimensionless>(modulationLevel.intValue(), Units.PERCENT);
364                 }
365                 break;
366             case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
367                 if (entity.getOTAppFaultCode().isPresent()) {
368                     state = new QuantityType<Dimensionless>(entity.getOTAppFaultCode().get().intValue(), Units.PERCENT);
369                 }
370                 break;
371             case APPLIANCE_DHWTEMPERATURE_CHANNEL:
372                 if (entity.getDHWTemp().isPresent()) {
373                     Unit<Temperature> unit = entity.getDHWTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
374                             ? SIUnits.CELSIUS
375                             : ImperialUnits.FAHRENHEIT;
376                     state = new QuantityType<Temperature>(entity.getDHWTemp().get(), unit);
377                 }
378                 break;
379             case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
380                 if (entity.getOTOEMFaultcode().isPresent()) {
381                     state = new QuantityType<Dimensionless>(entity.getOTOEMFaultcode().get().intValue(), Units.PERCENT);
382                 }
383                 break;
384             case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
385                 if (entity.getBoilerTemp().isPresent()) {
386                     Unit<Temperature> unit = entity.getBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
387                             ? SIUnits.CELSIUS
388                             : ImperialUnits.FAHRENHEIT;
389                     state = new QuantityType<Temperature>(entity.getBoilerTemp().get(), unit);
390                 }
391                 break;
392             case APPLIANCE_DHWSETPOINT_CHANNEL:
393                 if (entity.getDHTSetpoint().isPresent()) {
394                     Unit<Temperature> unit = entity.getDHTSetpointUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
395                             ? SIUnits.CELSIUS
396                             : ImperialUnits.FAHRENHEIT;
397                     state = new QuantityType<Temperature>(entity.getDHTSetpoint().get(), unit);
398                 }
399                 break;
400             case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
401                 if (entity.getMaxBoilerTemp().isPresent()) {
402                     Unit<Temperature> unit = entity.getMaxBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
403                             ? SIUnits.CELSIUS
404                             : ImperialUnits.FAHRENHEIT;
405                     state = new QuantityType<Temperature>(entity.getMaxBoilerTemp().get(), unit);
406                 }
407                 break;
408             case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
409                 if (entity.getDHWComfortMode().isPresent()) {
410                     state = OnOffType.from(entity.getDHWComfortMode().get());
411                 }
412                 break;
413             default:
414                 break;
415         }
416
417         if (state != UnDefType.NULL) {
418             updateState(channelID, state);
419         }
420     }
421
422     protected synchronized void addBatteryChannels() {
423         logger.debug("Battery operated appliance: {} detected: adding 'Battery level' and 'Battery low level' channels",
424                 thing.getLabel());
425
426         ChannelUID channelUIDBatteryLevel = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVEL_CHANNEL);
427         ChannelUID channelUIDBatteryLevelLow = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVELLOW_CHANNEL);
428
429         boolean channelBatteryLevelExists = false;
430         boolean channelBatteryLowExists = false;
431
432         List<Channel> channels = getThing().getChannels();
433         for (Channel channel : channels) {
434             if (channel.getUID().equals(channelUIDBatteryLevel)) {
435                 channelBatteryLevelExists = true;
436             } else if (channel.getUID().equals(channelUIDBatteryLevelLow)) {
437                 channelBatteryLowExists = true;
438             }
439             if (channelBatteryLevelExists && channelBatteryLowExists) {
440                 break;
441             }
442         }
443
444         if (!channelBatteryLevelExists) {
445             ThingBuilder thingBuilder = editThing();
446
447             Channel channelBatteryLevel = ChannelBuilder.create(channelUIDBatteryLevel, "Number")
448                     .withType(CHANNEL_TYPE_BATTERYLEVEL).withKind(ChannelKind.STATE).withLabel("Battery Level")
449                     .withDescription("Represents the battery level as a percentage (0-100%)").build();
450
451             thingBuilder.withChannel(channelBatteryLevel);
452
453             updateThing(thingBuilder.build());
454         }
455
456         if (!channelBatteryLowExists) {
457             ThingBuilder thingBuilder = editThing();
458
459             Channel channelBatteryLow = ChannelBuilder.create(channelUIDBatteryLevelLow, "Switch")
460                     .withType(CHANNEL_TYPE_BATTERYLEVELLOW).withKind(ChannelKind.STATE).withLabel("Battery Low Level")
461                     .withDescription("Switches ON when battery level gets below threshold level").build();
462
463             thingBuilder.withChannel(channelBatteryLow);
464
465             updateThing(thingBuilder.build());
466         }
467     }
468
469     protected void setApplianceProperties() {
470         Map<String, String> properties = editProperties();
471         logger.debug("Setting thing properties to {}", thing.getLabel());
472         Appliance localAppliance = this.appliance;
473         if (localAppliance != null) {
474             properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_DESCRIPTION, localAppliance.getDescription());
475             properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_TYPE, localAppliance.getType());
476             properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_FUNCTIONALITIES,
477                     String.join(", ", localAppliance.getActuatorFunctionalities().keySet()));
478
479             if (localAppliance.isZigbeeDevice()) {
480                 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_TYPE,
481                         localAppliance.getZigbeeNode().getType());
482                 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_REACHABLE,
483                         localAppliance.getZigbeeNode().getReachable());
484                 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_POWERSOURCE,
485                         localAppliance.getZigbeeNode().getPowerSource());
486                 properties.put(Thing.PROPERTY_MAC_ADDRESS, localAppliance.getZigbeeNode().getMacAddress());
487             }
488
489             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, localAppliance.getModule().getFirmwareVersion());
490             properties.put(Thing.PROPERTY_HARDWARE_VERSION, localAppliance.getModule().getHardwareVersion());
491             properties.put(Thing.PROPERTY_VENDOR, localAppliance.getModule().getVendorName());
492             properties.put(Thing.PROPERTY_MODEL_ID, localAppliance.getModule().getVendorModel());
493         }
494         updateProperties(properties);
495     }
496 }