]> git.basschouten.com Git - openhab-addons.git/blob
0773ff462749c4b892ba2925e7728ffa546474da
[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);
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                         if (command == OnOffType.ON) {
146                             controller.switchRelayLockOn(entity);
147                         } else {
148                             controller.switchRelayLockOff(entity);
149                         }
150                     } catch (PlugwiseHAException e) {
151                         logger.warn("Unable to switch relay lock {} for appliance '{}'", (State) command,
152                                 entity.getName());
153                     }
154                 }
155                 break;
156             case APPLIANCE_OFFSET_CHANNEL:
157                 if (command instanceof QuantityType) {
158                     Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
159                             ? SIUnits.CELSIUS
160                             : ImperialUnits.FAHRENHEIT;
161                     QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
162
163                     if (state != null) {
164                         try {
165                             controller.setOffsetTemperature(entity, state.doubleValue());
166                         } catch (PlugwiseHAException e) {
167                             logger.warn("Unable to update setpoint for zone '{}': {} -> {}", entity.getName(),
168                                     entity.getSetpointTemperature().orElse(null), state.doubleValue());
169                         }
170                     }
171                 }
172                 break;
173             case APPLIANCE_POWER_CHANNEL:
174                 if (command instanceof OnOffType) {
175                     try {
176                         if (command == OnOffType.ON) {
177                             controller.switchRelayOn(entity);
178                         } else {
179                             controller.switchRelayOff(entity);
180                         }
181                     } catch (PlugwiseHAException e) {
182                         logger.warn("Unable to switch relay {} for appliance '{}'", (State) command, entity.getName());
183                     }
184                 }
185                 break;
186             case APPLIANCE_SETPOINT_CHANNEL:
187                 if (command instanceof QuantityType) {
188                     Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
189                             .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
190                     QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
191
192                     if (state != null) {
193                         try {
194                             controller.setThermostat(entity, state.doubleValue());
195                         } catch (PlugwiseHAException e) {
196                             logger.warn("Unable to update setpoint for appliance '{}': {} -> {}", entity.getName(),
197                                     entity.getSetpointTemperature().orElse(null), state.doubleValue());
198                         }
199                     }
200                 }
201                 break;
202             default:
203                 logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
204         }
205     }
206
207     private State getDefaultState(String channelID) {
208         State state = UnDefType.NULL;
209         switch (channelID) {
210             case APPLIANCE_BATTERYLEVEL_CHANNEL:
211             case APPLIANCE_CHSTATE_CHANNEL:
212             case APPLIANCE_DHWSTATE_CHANNEL:
213             case APPLIANCE_COOLINGSTATE_CHANNEL:
214             case APPLIANCE_INTENDEDBOILERTEMP_CHANNEL:
215             case APPLIANCE_FLAMESTATE_CHANNEL:
216             case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
217             case APPLIANCE_MODULATIONLEVEL_CHANNEL:
218             case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
219             case APPLIANCE_DHWTEMPERATURE_CHANNEL:
220             case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
221             case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
222             case APPLIANCE_DHWSETPOINT_CHANNEL:
223             case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
224             case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
225             case APPLIANCE_OFFSET_CHANNEL:
226             case APPLIANCE_POWER_USAGE_CHANNEL:
227             case APPLIANCE_SETPOINT_CHANNEL:
228             case APPLIANCE_TEMPERATURE_CHANNEL:
229             case APPLIANCE_VALVEPOSITION_CHANNEL:
230             case APPLIANCE_WATERPRESSURE_CHANNEL:
231                 state = UnDefType.NULL;
232                 break;
233             case APPLIANCE_BATTERYLEVELLOW_CHANNEL:
234             case APPLIANCE_LOCK_CHANNEL:
235             case APPLIANCE_POWER_CHANNEL:
236                 state = UnDefType.UNDEF;
237                 break;
238         }
239         return state;
240     }
241
242     @Override
243     protected void refreshChannel(Appliance entity, ChannelUID channelUID) {
244         String channelID = channelUID.getIdWithoutGroup();
245         State state = getDefaultState(channelID);
246         PlugwiseHAThingConfig config = getPlugwiseThingConfig();
247
248         switch (channelID) {
249             case APPLIANCE_BATTERYLEVEL_CHANNEL: {
250                 Double batteryLevel = entity.getBatteryLevel().orElse(null);
251
252                 if (batteryLevel != null) {
253                     batteryLevel = batteryLevel * 100;
254                     state = new QuantityType<Dimensionless>(batteryLevel.intValue(), Units.PERCENT);
255                     if (batteryLevel <= config.getLowBatteryPercentage()) {
256                         updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.ON);
257                     } else {
258                         updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.OFF);
259                     }
260                 }
261                 break;
262             }
263             case APPLIANCE_BATTERYLEVELLOW_CHANNEL: {
264                 Double batteryLevel = entity.getBatteryLevel().orElse(null);
265
266                 if (batteryLevel != null) {
267                     batteryLevel *= 100;
268                     if (batteryLevel <= config.getLowBatteryPercentage()) {
269                         state = OnOffType.ON;
270                     } else {
271                         state = OnOffType.OFF;
272                     }
273                 }
274                 break;
275             }
276             case APPLIANCE_CHSTATE_CHANNEL:
277                 if (entity.getCHState().isPresent()) {
278                     state = OnOffType.from(entity.getCHState().get());
279                 }
280                 break;
281             case APPLIANCE_DHWSTATE_CHANNEL:
282                 if (entity.getDHWState().isPresent()) {
283                     state = OnOffType.from(entity.getDHWState().get());
284                 }
285                 break;
286             case APPLIANCE_LOCK_CHANNEL:
287                 Boolean relayLockState = entity.getRelayLockState().orElse(null);
288                 if (relayLockState != null) {
289                     state = OnOffType.from(relayLockState);
290                 }
291                 break;
292             case APPLIANCE_OFFSET_CHANNEL:
293                 if (entity.getOffsetTemperature().isPresent()) {
294                     Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
295                             ? SIUnits.CELSIUS
296                             : ImperialUnits.FAHRENHEIT;
297                     state = new QuantityType<Temperature>(entity.getOffsetTemperature().get(), unit);
298                 }
299                 break;
300             case APPLIANCE_POWER_CHANNEL:
301                 if (entity.getRelayState().isPresent()) {
302                     state = OnOffType.from(entity.getRelayState().get());
303                 }
304                 break;
305             case APPLIANCE_POWER_USAGE_CHANNEL:
306                 if (entity.getPowerUsage().isPresent()) {
307                     state = new QuantityType<Power>(entity.getPowerUsage().get(), Units.WATT);
308                 }
309                 break;
310             case APPLIANCE_SETPOINT_CHANNEL:
311                 if (entity.getSetpointTemperature().isPresent()) {
312                     Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
313                             .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
314                     state = new QuantityType<Temperature>(entity.getSetpointTemperature().get(), unit);
315                 }
316                 break;
317             case APPLIANCE_TEMPERATURE_CHANNEL:
318                 if (entity.getTemperature().isPresent()) {
319                     Unit<Temperature> unit = entity.getTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
320                             ? SIUnits.CELSIUS
321                             : ImperialUnits.FAHRENHEIT;
322                     state = new QuantityType<Temperature>(entity.getTemperature().get(), unit);
323                 }
324                 break;
325             case APPLIANCE_VALVEPOSITION_CHANNEL:
326                 if (entity.getValvePosition().isPresent()) {
327                     Double valvePosition = entity.getValvePosition().get() * 100;
328                     state = new QuantityType<Dimensionless>(valvePosition.intValue(), Units.PERCENT);
329                 }
330                 break;
331             case APPLIANCE_WATERPRESSURE_CHANNEL:
332                 if (entity.getWaterPressure().isPresent()) {
333                     Unit<Pressure> unit = HECTO(SIUnits.PASCAL);
334                     state = new QuantityType<Pressure>(entity.getWaterPressure().get(), unit);
335                 }
336                 break;
337             case APPLIANCE_COOLINGSTATE_CHANNEL:
338                 if (entity.getCoolingState().isPresent()) {
339                     state = OnOffType.from(entity.getCoolingState().get());
340                 }
341                 break;
342             case APPLIANCE_INTENDEDBOILERTEMP_CHANNEL:
343                 if (entity.getIntendedBoilerTemp().isPresent()) {
344                     Unit<Temperature> unit = entity.getIntendedBoilerTempUnit().orElse(UNIT_CELSIUS)
345                             .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
346                     state = new QuantityType<Temperature>(entity.getIntendedBoilerTemp().get(), unit);
347                 }
348                 break;
349             case APPLIANCE_FLAMESTATE_CHANNEL:
350                 if (entity.getFlameState().isPresent()) {
351                     state = OnOffType.from(entity.getFlameState().get());
352                 }
353                 break;
354             case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
355                 if (entity.getIntendedHeatingState().isPresent()) {
356                     state = OnOffType.from(entity.getIntendedHeatingState().get());
357                 }
358                 break;
359             case APPLIANCE_MODULATIONLEVEL_CHANNEL:
360                 if (entity.getModulationLevel().isPresent()) {
361                     Double modulationLevel = entity.getModulationLevel().get() * 100;
362                     state = new QuantityType<Dimensionless>(modulationLevel.intValue(), Units.PERCENT);
363                 }
364                 break;
365             case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
366                 if (entity.getOTAppFaultCode().isPresent()) {
367                     state = new QuantityType<Dimensionless>(entity.getOTAppFaultCode().get().intValue(), Units.PERCENT);
368                 }
369                 break;
370             case APPLIANCE_DHWTEMPERATURE_CHANNEL:
371                 if (entity.getDHWTemp().isPresent()) {
372                     Unit<Temperature> unit = entity.getDHWTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
373                             ? SIUnits.CELSIUS
374                             : ImperialUnits.FAHRENHEIT;
375                     state = new QuantityType<Temperature>(entity.getDHWTemp().get(), unit);
376                 }
377                 break;
378             case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
379                 if (entity.getOTOEMFaultcode().isPresent()) {
380                     state = new QuantityType<Dimensionless>(entity.getOTOEMFaultcode().get().intValue(), Units.PERCENT);
381                 }
382                 break;
383             case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
384                 if (entity.getBoilerTemp().isPresent()) {
385                     Unit<Temperature> unit = entity.getBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
386                             ? SIUnits.CELSIUS
387                             : ImperialUnits.FAHRENHEIT;
388                     state = new QuantityType<Temperature>(entity.getBoilerTemp().get(), unit);
389                 }
390                 break;
391             case APPLIANCE_DHWSETPOINT_CHANNEL:
392                 if (entity.getDHTSetpoint().isPresent()) {
393                     Unit<Temperature> unit = entity.getDHTSetpointUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
394                             ? SIUnits.CELSIUS
395                             : ImperialUnits.FAHRENHEIT;
396                     state = new QuantityType<Temperature>(entity.getDHTSetpoint().get(), unit);
397                 }
398                 break;
399             case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
400                 if (entity.getMaxBoilerTemp().isPresent()) {
401                     Unit<Temperature> unit = entity.getMaxBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
402                             ? SIUnits.CELSIUS
403                             : ImperialUnits.FAHRENHEIT;
404                     state = new QuantityType<Temperature>(entity.getMaxBoilerTemp().get(), unit);
405                 }
406                 break;
407             case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
408                 if (entity.getDHWComfortMode().isPresent()) {
409                     state = OnOffType.from(entity.getDHWComfortMode().get());
410                 }
411                 break;
412             default:
413                 break;
414         }
415
416         if (state != UnDefType.NULL) {
417             updateState(channelID, state);
418         }
419     }
420
421     protected synchronized void addBatteryChannels() {
422         logger.debug("Battery operated appliance: {} detected: adding 'Battery level' and 'Battery low level' channels",
423                 thing.getLabel());
424
425         ChannelUID channelUIDBatteryLevel = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVEL_CHANNEL);
426         ChannelUID channelUIDBatteryLevelLow = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVELLOW_CHANNEL);
427
428         boolean channelBatteryLevelExists = false;
429         boolean channelBatteryLowExists = false;
430
431         List<Channel> channels = getThing().getChannels();
432         for (Channel channel : channels) {
433             if (channel.getUID().equals(channelUIDBatteryLevel)) {
434                 channelBatteryLevelExists = true;
435             } else if (channel.getUID().equals(channelUIDBatteryLevelLow)) {
436                 channelBatteryLowExists = true;
437             }
438             if (channelBatteryLevelExists && channelBatteryLowExists) {
439                 break;
440             }
441         }
442
443         if (!channelBatteryLevelExists) {
444             ThingBuilder thingBuilder = editThing();
445
446             Channel channelBatteryLevel = ChannelBuilder.create(channelUIDBatteryLevel, "Number")
447                     .withType(CHANNEL_TYPE_BATTERYLEVEL).withKind(ChannelKind.STATE).withLabel("Battery Level")
448                     .withDescription("Represents the battery level as a percentage (0-100%)").build();
449
450             thingBuilder.withChannel(channelBatteryLevel);
451
452             updateThing(thingBuilder.build());
453         }
454
455         if (!channelBatteryLowExists) {
456             ThingBuilder thingBuilder = editThing();
457
458             Channel channelBatteryLow = ChannelBuilder.create(channelUIDBatteryLevelLow, "Switch")
459                     .withType(CHANNEL_TYPE_BATTERYLEVELLOW).withKind(ChannelKind.STATE).withLabel("Battery Low Level")
460                     .withDescription("Switches ON when battery level gets below threshold level").build();
461
462             thingBuilder.withChannel(channelBatteryLow);
463
464             updateThing(thingBuilder.build());
465         }
466     }
467
468     protected void setApplianceProperties() {
469         Map<String, String> properties = editProperties();
470         logger.debug("Setting thing properties to {}", thing.getLabel());
471         Appliance localAppliance = this.appliance;
472         if (localAppliance != null) {
473             properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_DESCRIPTION, localAppliance.getDescription());
474             properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_TYPE, localAppliance.getType());
475             properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_FUNCTIONALITIES,
476                     String.join(", ", localAppliance.getActuatorFunctionalities().keySet()));
477
478             if (localAppliance.isZigbeeDevice()) {
479                 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_TYPE,
480                         localAppliance.getZigbeeNode().getType());
481                 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_REACHABLE,
482                         localAppliance.getZigbeeNode().getReachable());
483                 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_POWERSOURCE,
484                         localAppliance.getZigbeeNode().getPowerSource());
485                 properties.put(Thing.PROPERTY_MAC_ADDRESS, localAppliance.getZigbeeNode().getMacAddress());
486             }
487
488             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, localAppliance.getModule().getFirmwareVersion());
489             properties.put(Thing.PROPERTY_HARDWARE_VERSION, localAppliance.getModule().getHardwareVersion());
490             properties.put(Thing.PROPERTY_VENDOR, localAppliance.getModule().getVendorName());
491             properties.put(Thing.PROPERTY_MODEL_ID, localAppliance.getModule().getVendorModel());
492         }
493         updateProperties(properties);
494     }
495 }