2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.plugwiseha.internal.handler;
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;
22 import java.util.List;
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;
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;
57 * The {@link PlugwiseHAApplianceHandler} class is responsible for handling
58 * commands and status updates for the Plugwise Home Automation appliances.
59 * Extends @{link PlugwiseHABaseHandler}
61 * @author Bas van Wetten - Initial contribution
62 * @author Leo Siepel - finish initial contribution
66 public class PlugwiseHAApplianceHandler extends PlugwiseHABaseHandler<Appliance, PlugwiseHAThingConfig> {
68 private @Nullable Appliance appliance;
69 private final Logger logger = LoggerFactory.getLogger(PlugwiseHAApplianceHandler.class);
73 public PlugwiseHAApplianceHandler(Thing thing) {
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);
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.");
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();
105 setApplianceProperties();
106 updateStatus(ONLINE);
108 updateStatus(OFFLINE);
111 updateStatus(OFFLINE, BRIDGE_OFFLINE);
113 } catch (PlugwiseHAException e) {
114 updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getMessage());
120 protected @Nullable Appliance getEntity(PlugwiseHAController controller) throws PlugwiseHAException {
121 PlugwiseHAThingConfig config = getPlugwiseThingConfig();
122 Appliance appliance = controller.getAppliance(config.getId());
128 protected void handleCommand(Appliance entity, ChannelUID channelUID, Command command) throws PlugwiseHAException {
129 String channelID = channelUID.getIdWithoutGroup();
131 PlugwiseHABridgeHandler bridge = this.getPlugwiseHABridge();
132 if (bridge == null) {
136 PlugwiseHAController controller = bridge.getController();
137 if (controller == null) {
142 case APPLIANCE_LOCK_CHANNEL:
143 if (command instanceof OnOffType) {
145 controller.setRelay(entity, (command == OnOffType.ON));
146 } catch (PlugwiseHAException e) {
147 logger.warn("Unable to switch relay lock {} for appliance '{}'", (State) command,
152 case APPLIANCE_OFFSET_CHANNEL:
153 if (command instanceof QuantityType) {
154 Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
156 : ImperialUnits.FAHRENHEIT;
157 QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
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());
169 case APPLIANCE_POWER_CHANNEL:
170 if (command instanceof OnOffType) {
172 controller.setRelay(entity, command == OnOffType.ON);
173 } catch (PlugwiseHAException e) {
174 logger.warn("Unable to switch relay {} for appliance '{}'", (State) command, entity.getName());
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);
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());
195 logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
199 private State getDefaultState(String channelID) {
200 State state = UnDefType.NULL;
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;
226 case APPLIANCE_BATTERYLEVELLOW_CHANNEL:
227 case APPLIANCE_LOCK_CHANNEL:
228 case APPLIANCE_POWER_CHANNEL:
229 state = UnDefType.UNDEF;
236 protected void refreshChannel(Appliance entity, ChannelUID channelUID) {
237 String channelID = channelUID.getIdWithoutGroup();
238 State state = getDefaultState(channelID);
239 PlugwiseHAThingConfig config = getPlugwiseThingConfig();
242 case APPLIANCE_BATTERYLEVEL_CHANNEL: {
243 Double batteryLevel = entity.getBatteryLevel().orElse(null);
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);
251 updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.OFF);
256 case APPLIANCE_BATTERYLEVELLOW_CHANNEL: {
257 Double batteryLevel = entity.getBatteryLevel().orElse(null);
259 if (batteryLevel != null) {
261 if (batteryLevel <= config.getLowBatteryPercentage()) {
262 state = OnOffType.ON;
264 state = OnOffType.OFF;
269 case APPLIANCE_CHSTATE_CHANNEL:
270 if (entity.getCHState().isPresent()) {
271 state = OnOffType.from(entity.getCHState().get());
274 case APPLIANCE_DHWSTATE_CHANNEL:
275 if (entity.getDHWState().isPresent()) {
276 state = OnOffType.from(entity.getDHWState().get());
279 case APPLIANCE_LOCK_CHANNEL:
280 Boolean relayLockState = entity.getRelayLockState().orElse(null);
281 if (relayLockState != null) {
282 state = OnOffType.from(relayLockState);
285 case APPLIANCE_OFFSET_CHANNEL:
286 if (entity.getOffsetTemperature().isPresent()) {
287 Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
289 : ImperialUnits.FAHRENHEIT;
290 state = new QuantityType<Temperature>(entity.getOffsetTemperature().get(), unit);
293 case APPLIANCE_POWER_CHANNEL:
294 if (entity.getRelayState().isPresent()) {
295 state = OnOffType.from(entity.getRelayState().get());
298 case APPLIANCE_POWER_USAGE_CHANNEL:
299 if (entity.getPowerUsage().isPresent()) {
300 state = new QuantityType<Power>(entity.getPowerUsage().get(), Units.WATT);
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);
310 case APPLIANCE_TEMPERATURE_CHANNEL:
311 if (entity.getTemperature().isPresent()) {
312 Unit<Temperature> unit = entity.getTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
314 : ImperialUnits.FAHRENHEIT;
315 state = new QuantityType<Temperature>(entity.getTemperature().get(), unit);
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);
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);
330 case APPLIANCE_COOLINGSTATE_CHANNEL:
331 if (entity.getCoolingState().isPresent()) {
332 state = OnOffType.from(entity.getCoolingState().get());
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);
342 case APPLIANCE_FLAMESTATE_CHANNEL:
343 if (entity.getFlameState().isPresent()) {
344 state = OnOffType.from(entity.getFlameState().get());
347 case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
348 if (entity.getIntendedHeatingState().isPresent()) {
349 state = OnOffType.from(entity.getIntendedHeatingState().get());
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);
358 case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
359 if (entity.getOTAppFaultCode().isPresent()) {
360 state = new QuantityType<Dimensionless>(entity.getOTAppFaultCode().get().intValue(), Units.PERCENT);
363 case APPLIANCE_RETURNWATERTEMPERATURE_CHANNEL:
364 if (entity.getBoilerTemp().isPresent()) {
365 Unit<Temperature> unit = entity.getReturnWaterTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
367 : ImperialUnits.FAHRENHEIT;
368 state = new QuantityType<Temperature>(entity.getReturnWaterTemp().get(), unit);
371 case APPLIANCE_DHWTEMPERATURE_CHANNEL:
372 if (entity.getDHWTemp().isPresent()) {
373 Unit<Temperature> unit = entity.getDHWTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
375 : ImperialUnits.FAHRENHEIT;
376 state = new QuantityType<Temperature>(entity.getDHWTemp().get(), unit);
379 case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
380 if (entity.getOTOEMFaultcode().isPresent()) {
381 state = new QuantityType<Dimensionless>(entity.getOTOEMFaultcode().get().intValue(), Units.PERCENT);
384 case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
385 if (entity.getBoilerTemp().isPresent()) {
386 Unit<Temperature> unit = entity.getBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
388 : ImperialUnits.FAHRENHEIT;
389 state = new QuantityType<Temperature>(entity.getBoilerTemp().get(), unit);
392 case APPLIANCE_DHWSETPOINT_CHANNEL:
393 if (entity.getDHTSetpoint().isPresent()) {
394 Unit<Temperature> unit = entity.getDHTSetpointUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
396 : ImperialUnits.FAHRENHEIT;
397 state = new QuantityType<Temperature>(entity.getDHTSetpoint().get(), unit);
400 case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
401 if (entity.getMaxBoilerTemp().isPresent()) {
402 Unit<Temperature> unit = entity.getMaxBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
404 : ImperialUnits.FAHRENHEIT;
405 state = new QuantityType<Temperature>(entity.getMaxBoilerTemp().get(), unit);
408 case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
409 if (entity.getDHWComfortMode().isPresent()) {
410 state = OnOffType.from(entity.getDHWComfortMode().get());
417 if (state != UnDefType.NULL) {
418 updateState(channelID, state);
422 protected synchronized void addBatteryChannels() {
423 logger.debug("Battery operated appliance: {} detected: adding 'Battery level' and 'Battery low level' channels",
426 ChannelUID channelUIDBatteryLevel = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVEL_CHANNEL);
427 ChannelUID channelUIDBatteryLevelLow = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVELLOW_CHANNEL);
429 boolean channelBatteryLevelExists = false;
430 boolean channelBatteryLowExists = false;
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;
439 if (channelBatteryLevelExists && channelBatteryLowExists) {
444 if (!channelBatteryLevelExists) {
445 ThingBuilder thingBuilder = editThing();
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();
451 thingBuilder.withChannel(channelBatteryLevel);
453 updateThing(thingBuilder.build());
456 if (!channelBatteryLowExists) {
457 ThingBuilder thingBuilder = editThing();
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();
463 thingBuilder.withChannel(channelBatteryLow);
465 updateThing(thingBuilder.build());
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()));
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());
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());
494 updateProperties(properties);