]> git.basschouten.com Git - openhab-addons.git/blob
7e024af6390b8e7b7b9d683973d96fb24a48dc57
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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);
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) throws PlugwiseHAException {
121         PlugwiseHAThingConfig config = getPlugwiseThingConfig();
122         Appliance appliance = controller.getAppliance(config.getId());
123
124         return appliance;
125     }
126
127     @Override
128     protected void handleCommand(Appliance entity, ChannelUID channelUID, Command command) throws PlugwiseHAException {
129         String channelID = channelUID.getIdWithoutGroup();
130
131         PlugwiseHABridgeHandler bridge = this.getPlugwiseHABridge();
132         if (bridge == null) {
133             return;
134         }
135
136         PlugwiseHAController controller = bridge.getController();
137         if (controller == null) {
138             return;
139         }
140
141         switch (channelID) {
142             case APPLIANCE_LOCK_CHANNEL:
143                 if (command instanceof OnOffType) {
144                     try {
145                         controller.setRelay(entity, (command == OnOffType.ON));
146                     } catch (PlugwiseHAException e) {
147                         logger.warn("Unable to switch relay lock {} for appliance '{}'", (State) command,
148                                 entity.getName());
149                     }
150                 }
151                 break;
152             case APPLIANCE_OFFSET_CHANNEL:
153                 if (command instanceof QuantityType) {
154                     Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
155                             ? SIUnits.CELSIUS
156                             : ImperialUnits.FAHRENHEIT;
157                     QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
158
159                     if (state != null) {
160                         try {
161                             controller.setOffsetTemperature(entity, state.doubleValue());
162                         } catch (PlugwiseHAException e) {
163                             logger.warn("Unable to update setpoint for zone '{}': {} -> {}", entity.getName(),
164                                     entity.getSetpointTemperature().orElse(null), state.doubleValue());
165                         }
166                     }
167                 }
168                 break;
169             case APPLIANCE_POWER_CHANNEL:
170                 if (command instanceof OnOffType) {
171                     try {
172                         controller.setRelay(entity, command == OnOffType.ON);
173                     } catch (PlugwiseHAException e) {
174                         logger.warn("Unable to switch relay {} for appliance '{}'", (State) command, entity.getName());
175                     }
176                 }
177                 break;
178             case APPLIANCE_SETPOINT_CHANNEL:
179                 if (command instanceof QuantityType) {
180                     Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
181                             .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
182                     QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
183
184                     if (state != null) {
185                         try {
186                             controller.setThermostat(entity, state.doubleValue());
187                         } catch (PlugwiseHAException e) {
188                             logger.warn("Unable to update setpoint for appliance '{}': {} -> {}", entity.getName(),
189                                     entity.getSetpointTemperature().orElse(null), state.doubleValue());
190                         }
191                     }
192                 }
193                 break;
194             default:
195                 logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
196         }
197     }
198
199     private State getDefaultState(String channelID) {
200         State state = UnDefType.NULL;
201         switch (channelID) {
202             case APPLIANCE_BATTERYLEVEL_CHANNEL:
203             case APPLIANCE_CHSTATE_CHANNEL:
204             case APPLIANCE_DHWSTATE_CHANNEL:
205             case APPLIANCE_COOLINGSTATE_CHANNEL:
206             case APPLIANCE_INTENDEDBOILERTEMP_CHANNEL:
207             case APPLIANCE_FLAMESTATE_CHANNEL:
208             case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
209             case APPLIANCE_MODULATIONLEVEL_CHANNEL:
210             case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
211             case APPLIANCE_DHWTEMPERATURE_CHANNEL:
212             case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
213             case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
214             case APPLIANCE_DHWSETPOINT_CHANNEL:
215             case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
216             case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
217             case APPLIANCE_OFFSET_CHANNEL:
218             case APPLIANCE_POWER_USAGE_CHANNEL:
219             case APPLIANCE_SETPOINT_CHANNEL:
220             case APPLIANCE_TEMPERATURE_CHANNEL:
221             case APPLIANCE_VALVEPOSITION_CHANNEL:
222             case APPLIANCE_WATERPRESSURE_CHANNEL:
223             case APPLIANCE_RETURNWATERTEMPERATURE_CHANNEL:
224                 state = UnDefType.NULL;
225                 break;
226             case APPLIANCE_BATTERYLEVELLOW_CHANNEL:
227             case APPLIANCE_LOCK_CHANNEL:
228             case APPLIANCE_POWER_CHANNEL:
229                 state = UnDefType.UNDEF;
230                 break;
231         }
232         return state;
233     }
234
235     @Override
236     protected void refreshChannel(Appliance entity, ChannelUID channelUID) {
237         String channelID = channelUID.getIdWithoutGroup();
238         State state = getDefaultState(channelID);
239         PlugwiseHAThingConfig config = getPlugwiseThingConfig();
240
241         switch (channelID) {
242             case APPLIANCE_BATTERYLEVEL_CHANNEL: {
243                 Double batteryLevel = entity.getBatteryLevel().orElse(null);
244
245                 if (batteryLevel != null) {
246                     batteryLevel = batteryLevel * 100;
247                     state = new QuantityType<Dimensionless>(batteryLevel.intValue(), Units.PERCENT);
248                     if (batteryLevel <= config.getLowBatteryPercentage()) {
249                         updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.ON);
250                     } else {
251                         updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.OFF);
252                     }
253                 }
254                 break;
255             }
256             case APPLIANCE_BATTERYLEVELLOW_CHANNEL: {
257                 Double batteryLevel = entity.getBatteryLevel().orElse(null);
258
259                 if (batteryLevel != null) {
260                     batteryLevel *= 100;
261                     if (batteryLevel <= config.getLowBatteryPercentage()) {
262                         state = OnOffType.ON;
263                     } else {
264                         state = OnOffType.OFF;
265                     }
266                 }
267                 break;
268             }
269             case APPLIANCE_CHSTATE_CHANNEL:
270                 if (entity.getCHState().isPresent()) {
271                     state = OnOffType.from(entity.getCHState().get());
272                 }
273                 break;
274             case APPLIANCE_DHWSTATE_CHANNEL:
275                 if (entity.getDHWState().isPresent()) {
276                     state = OnOffType.from(entity.getDHWState().get());
277                 }
278                 break;
279             case APPLIANCE_LOCK_CHANNEL:
280                 Boolean relayLockState = entity.getRelayLockState().orElse(null);
281                 if (relayLockState != null) {
282                     state = OnOffType.from(relayLockState);
283                 }
284                 break;
285             case APPLIANCE_OFFSET_CHANNEL:
286                 if (entity.getOffsetTemperature().isPresent()) {
287                     Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
288                             ? SIUnits.CELSIUS
289                             : ImperialUnits.FAHRENHEIT;
290                     state = new QuantityType<Temperature>(entity.getOffsetTemperature().get(), unit);
291                 }
292                 break;
293             case APPLIANCE_POWER_CHANNEL:
294                 if (entity.getRelayState().isPresent()) {
295                     state = OnOffType.from(entity.getRelayState().get());
296                 }
297                 break;
298             case APPLIANCE_POWER_USAGE_CHANNEL:
299                 if (entity.getPowerUsage().isPresent()) {
300                     state = new QuantityType<Power>(entity.getPowerUsage().get(), Units.WATT);
301                 }
302                 break;
303             case APPLIANCE_SETPOINT_CHANNEL:
304                 if (entity.getSetpointTemperature().isPresent()) {
305                     Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
306                             .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
307                     state = new QuantityType<Temperature>(entity.getSetpointTemperature().get(), unit);
308                 }
309                 break;
310             case APPLIANCE_TEMPERATURE_CHANNEL:
311                 if (entity.getTemperature().isPresent()) {
312                     Unit<Temperature> unit = entity.getTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
313                             ? SIUnits.CELSIUS
314                             : ImperialUnits.FAHRENHEIT;
315                     state = new QuantityType<Temperature>(entity.getTemperature().get(), unit);
316                 }
317                 break;
318             case APPLIANCE_VALVEPOSITION_CHANNEL:
319                 if (entity.getValvePosition().isPresent()) {
320                     Double valvePosition = entity.getValvePosition().get() * 100;
321                     state = new QuantityType<Dimensionless>(valvePosition.intValue(), Units.PERCENT);
322                 }
323                 break;
324             case APPLIANCE_WATERPRESSURE_CHANNEL:
325                 if (entity.getWaterPressure().isPresent()) {
326                     Unit<Pressure> unit = HECTO(SIUnits.PASCAL);
327                     state = new QuantityType<Pressure>(entity.getWaterPressure().get(), unit);
328                 }
329                 break;
330             case APPLIANCE_COOLINGSTATE_CHANNEL:
331                 if (entity.getCoolingState().isPresent()) {
332                     state = OnOffType.from(entity.getCoolingState().get());
333                 }
334                 break;
335             case APPLIANCE_INTENDEDBOILERTEMP_CHANNEL:
336                 if (entity.getIntendedBoilerTemp().isPresent()) {
337                     Unit<Temperature> unit = entity.getIntendedBoilerTempUnit().orElse(UNIT_CELSIUS)
338                             .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
339                     state = new QuantityType<Temperature>(entity.getIntendedBoilerTemp().get(), unit);
340                 }
341                 break;
342             case APPLIANCE_FLAMESTATE_CHANNEL:
343                 if (entity.getFlameState().isPresent()) {
344                     state = OnOffType.from(entity.getFlameState().get());
345                 }
346                 break;
347             case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
348                 if (entity.getIntendedHeatingState().isPresent()) {
349                     state = OnOffType.from(entity.getIntendedHeatingState().get());
350                 }
351                 break;
352             case APPLIANCE_MODULATIONLEVEL_CHANNEL:
353                 if (entity.getModulationLevel().isPresent()) {
354                     Double modulationLevel = entity.getModulationLevel().get() * 100;
355                     state = new QuantityType<Dimensionless>(modulationLevel.intValue(), Units.PERCENT);
356                 }
357                 break;
358             case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
359                 if (entity.getOTAppFaultCode().isPresent()) {
360                     state = new QuantityType<Dimensionless>(entity.getOTAppFaultCode().get().intValue(), Units.PERCENT);
361                 }
362                 break;
363             case APPLIANCE_RETURNWATERTEMPERATURE_CHANNEL:
364                 if (entity.getBoilerTemp().isPresent()) {
365                     Unit<Temperature> unit = entity.getReturnWaterTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
366                             ? SIUnits.CELSIUS
367                             : ImperialUnits.FAHRENHEIT;
368                     state = new QuantityType<Temperature>(entity.getReturnWaterTemp().get(), unit);
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 }