2 * Copyright (c) 2010-2023 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 return controller.getAppliance(config.getId());
126 protected void handleCommand(Appliance entity, ChannelUID channelUID, Command command) throws PlugwiseHAException {
127 String channelID = channelUID.getIdWithoutGroup();
129 PlugwiseHABridgeHandler bridge = this.getPlugwiseHABridge();
130 if (bridge == null) {
134 PlugwiseHAController controller = bridge.getController();
135 if (controller == null) {
140 case APPLIANCE_LOCK_CHANNEL:
141 if (command instanceof OnOffType onOffCommand) {
143 controller.setRelay(entity, (command == OnOffType.ON));
144 } catch (PlugwiseHAException e) {
145 logger.warn("Unable to switch relay lock {} for appliance '{}'", onOffCommand,
150 case APPLIANCE_OFFSET_CHANNEL:
151 if (command instanceof QuantityType quantityCommand) {
152 Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
154 : ImperialUnits.FAHRENHEIT;
155 QuantityType<?> state = quantityCommand.toUnit(unit);
159 controller.setOffsetTemperature(entity, state.doubleValue());
160 } catch (PlugwiseHAException e) {
161 logger.warn("Unable to update setpoint for zone '{}': {} -> {}", entity.getName(),
162 entity.getSetpointTemperature().orElse(null), state.doubleValue());
167 case APPLIANCE_POWER_CHANNEL:
168 if (command instanceof OnOffType onOffCommand) {
170 controller.setRelay(entity, command == OnOffType.ON);
171 } catch (PlugwiseHAException e) {
172 logger.warn("Unable to switch relay {} for appliance '{}'", onOffCommand, entity.getName());
176 case APPLIANCE_SETPOINT_CHANNEL:
177 if (command instanceof QuantityType quantityCommand) {
178 Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
179 .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
180 QuantityType<?> state = quantityCommand.toUnit(unit);
184 controller.setThermostat(entity, state.doubleValue());
185 } catch (PlugwiseHAException e) {
186 logger.warn("Unable to update setpoint for appliance '{}': {} -> {}", entity.getName(),
187 entity.getSetpointTemperature().orElse(null), state.doubleValue());
193 logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
197 private State getDefaultState(String channelID) {
198 State state = UnDefType.NULL;
200 case APPLIANCE_BATTERYLEVEL_CHANNEL:
201 case APPLIANCE_CHSTATE_CHANNEL:
202 case APPLIANCE_DHWSTATE_CHANNEL:
203 case APPLIANCE_COOLINGSTATE_CHANNEL:
204 case APPLIANCE_INTENDEDBOILERTEMP_CHANNEL:
205 case APPLIANCE_FLAMESTATE_CHANNEL:
206 case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
207 case APPLIANCE_MODULATIONLEVEL_CHANNEL:
208 case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
209 case APPLIANCE_DHWTEMPERATURE_CHANNEL:
210 case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
211 case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
212 case APPLIANCE_DHWSETPOINT_CHANNEL:
213 case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
214 case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
215 case APPLIANCE_OFFSET_CHANNEL:
216 case APPLIANCE_POWER_USAGE_CHANNEL:
217 case APPLIANCE_SETPOINT_CHANNEL:
218 case APPLIANCE_TEMPERATURE_CHANNEL:
219 case APPLIANCE_VALVEPOSITION_CHANNEL:
220 case APPLIANCE_WATERPRESSURE_CHANNEL:
221 case APPLIANCE_RETURNWATERTEMPERATURE_CHANNEL:
222 state = UnDefType.NULL;
224 case APPLIANCE_BATTERYLEVELLOW_CHANNEL:
225 case APPLIANCE_LOCK_CHANNEL:
226 case APPLIANCE_POWER_CHANNEL:
227 state = UnDefType.UNDEF;
234 protected void refreshChannel(Appliance entity, ChannelUID channelUID) {
235 String channelID = channelUID.getIdWithoutGroup();
236 State state = getDefaultState(channelID);
237 PlugwiseHAThingConfig config = getPlugwiseThingConfig();
240 case APPLIANCE_BATTERYLEVEL_CHANNEL: {
241 Double batteryLevel = entity.getBatteryLevel().orElse(null);
243 if (batteryLevel != null) {
244 batteryLevel = batteryLevel * 100;
245 state = new QuantityType<Dimensionless>(batteryLevel.intValue(), Units.PERCENT);
246 if (batteryLevel <= config.getLowBatteryPercentage()) {
247 updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.ON);
249 updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.OFF);
254 case APPLIANCE_BATTERYLEVELLOW_CHANNEL: {
255 Double batteryLevel = entity.getBatteryLevel().orElse(null);
257 if (batteryLevel != null) {
259 if (batteryLevel <= config.getLowBatteryPercentage()) {
260 state = OnOffType.ON;
262 state = OnOffType.OFF;
267 case APPLIANCE_CHSTATE_CHANNEL:
268 if (entity.getCHState().isPresent()) {
269 state = OnOffType.from(entity.getCHState().get());
272 case APPLIANCE_DHWSTATE_CHANNEL:
273 if (entity.getDHWState().isPresent()) {
274 state = OnOffType.from(entity.getDHWState().get());
277 case APPLIANCE_LOCK_CHANNEL:
278 Boolean relayLockState = entity.getRelayLockState().orElse(null);
279 if (relayLockState != null) {
280 state = OnOffType.from(relayLockState);
283 case APPLIANCE_OFFSET_CHANNEL:
284 if (entity.getOffsetTemperature().isPresent()) {
285 Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
287 : ImperialUnits.FAHRENHEIT;
288 state = new QuantityType<Temperature>(entity.getOffsetTemperature().get(), unit);
291 case APPLIANCE_POWER_CHANNEL:
292 if (entity.getRelayState().isPresent()) {
293 state = OnOffType.from(entity.getRelayState().get());
296 case APPLIANCE_POWER_USAGE_CHANNEL:
297 if (entity.getPowerUsage().isPresent()) {
298 state = new QuantityType<Power>(entity.getPowerUsage().get(), Units.WATT);
301 case APPLIANCE_SETPOINT_CHANNEL:
302 if (entity.getSetpointTemperature().isPresent()) {
303 Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
304 .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
305 state = new QuantityType<Temperature>(entity.getSetpointTemperature().get(), unit);
308 case APPLIANCE_TEMPERATURE_CHANNEL:
309 if (entity.getTemperature().isPresent()) {
310 Unit<Temperature> unit = entity.getTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
312 : ImperialUnits.FAHRENHEIT;
313 state = new QuantityType<Temperature>(entity.getTemperature().get(), unit);
316 case APPLIANCE_VALVEPOSITION_CHANNEL:
317 if (entity.getValvePosition().isPresent()) {
318 Double valvePosition = entity.getValvePosition().get() * 100;
319 state = new QuantityType<Dimensionless>(valvePosition.intValue(), Units.PERCENT);
322 case APPLIANCE_WATERPRESSURE_CHANNEL:
323 if (entity.getWaterPressure().isPresent()) {
324 Unit<Pressure> unit = HECTO(SIUnits.PASCAL);
325 state = new QuantityType<Pressure>(entity.getWaterPressure().get(), unit);
328 case APPLIANCE_COOLINGSTATE_CHANNEL:
329 if (entity.getCoolingState().isPresent()) {
330 state = OnOffType.from(entity.getCoolingState().get());
333 case APPLIANCE_INTENDEDBOILERTEMP_CHANNEL:
334 if (entity.getIntendedBoilerTemp().isPresent()) {
335 Unit<Temperature> unit = entity.getIntendedBoilerTempUnit().orElse(UNIT_CELSIUS)
336 .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
337 state = new QuantityType<Temperature>(entity.getIntendedBoilerTemp().get(), unit);
340 case APPLIANCE_FLAMESTATE_CHANNEL:
341 if (entity.getFlameState().isPresent()) {
342 state = OnOffType.from(entity.getFlameState().get());
345 case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
346 if (entity.getIntendedHeatingState().isPresent()) {
347 state = OnOffType.from(entity.getIntendedHeatingState().get());
350 case APPLIANCE_MODULATIONLEVEL_CHANNEL:
351 if (entity.getModulationLevel().isPresent()) {
352 Double modulationLevel = entity.getModulationLevel().get() * 100;
353 state = new QuantityType<Dimensionless>(modulationLevel.intValue(), Units.PERCENT);
356 case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
357 if (entity.getOTAppFaultCode().isPresent()) {
358 state = new QuantityType<Dimensionless>(entity.getOTAppFaultCode().get().intValue(), Units.PERCENT);
361 case APPLIANCE_RETURNWATERTEMPERATURE_CHANNEL:
362 if (entity.getBoilerTemp().isPresent()) {
363 Unit<Temperature> unit = entity.getReturnWaterTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
365 : ImperialUnits.FAHRENHEIT;
366 state = new QuantityType<Temperature>(entity.getReturnWaterTemp().get(), unit);
369 case APPLIANCE_DHWTEMPERATURE_CHANNEL:
370 if (entity.getDHWTemp().isPresent()) {
371 Unit<Temperature> unit = entity.getDHWTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
373 : ImperialUnits.FAHRENHEIT;
374 state = new QuantityType<Temperature>(entity.getDHWTemp().get(), unit);
377 case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
378 if (entity.getOTOEMFaultcode().isPresent()) {
379 state = new QuantityType<Dimensionless>(entity.getOTOEMFaultcode().get().intValue(), Units.PERCENT);
382 case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
383 if (entity.getBoilerTemp().isPresent()) {
384 Unit<Temperature> unit = entity.getBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
386 : ImperialUnits.FAHRENHEIT;
387 state = new QuantityType<Temperature>(entity.getBoilerTemp().get(), unit);
390 case APPLIANCE_DHWSETPOINT_CHANNEL:
391 if (entity.getDHTSetpoint().isPresent()) {
392 Unit<Temperature> unit = entity.getDHTSetpointUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
394 : ImperialUnits.FAHRENHEIT;
395 state = new QuantityType<Temperature>(entity.getDHTSetpoint().get(), unit);
398 case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
399 if (entity.getMaxBoilerTemp().isPresent()) {
400 Unit<Temperature> unit = entity.getMaxBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
402 : ImperialUnits.FAHRENHEIT;
403 state = new QuantityType<Temperature>(entity.getMaxBoilerTemp().get(), unit);
406 case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
407 if (entity.getDHWComfortMode().isPresent()) {
408 state = OnOffType.from(entity.getDHWComfortMode().get());
415 if (state != UnDefType.NULL) {
416 updateState(channelID, state);
420 protected synchronized void addBatteryChannels() {
421 logger.debug("Battery operated appliance: {} detected: adding 'Battery level' and 'Battery low level' channels",
424 ChannelUID channelUIDBatteryLevel = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVEL_CHANNEL);
425 ChannelUID channelUIDBatteryLevelLow = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVELLOW_CHANNEL);
427 boolean channelBatteryLevelExists = false;
428 boolean channelBatteryLowExists = false;
430 List<Channel> channels = getThing().getChannels();
431 for (Channel channel : channels) {
432 if (channel.getUID().equals(channelUIDBatteryLevel)) {
433 channelBatteryLevelExists = true;
434 } else if (channel.getUID().equals(channelUIDBatteryLevelLow)) {
435 channelBatteryLowExists = true;
437 if (channelBatteryLevelExists && channelBatteryLowExists) {
442 if (!channelBatteryLevelExists) {
443 ThingBuilder thingBuilder = editThing();
445 Channel channelBatteryLevel = ChannelBuilder.create(channelUIDBatteryLevel, "Number")
446 .withType(CHANNEL_TYPE_BATTERYLEVEL).withKind(ChannelKind.STATE).withLabel("Battery Level")
447 .withDescription("Represents the battery level as a percentage (0-100%)").build();
449 thingBuilder.withChannel(channelBatteryLevel);
451 updateThing(thingBuilder.build());
454 if (!channelBatteryLowExists) {
455 ThingBuilder thingBuilder = editThing();
457 Channel channelBatteryLow = ChannelBuilder.create(channelUIDBatteryLevelLow, "Switch")
458 .withType(CHANNEL_TYPE_BATTERYLEVELLOW).withKind(ChannelKind.STATE).withLabel("Battery Low Level")
459 .withDescription("Switches ON when battery level gets below threshold level").build();
461 thingBuilder.withChannel(channelBatteryLow);
463 updateThing(thingBuilder.build());
467 protected void setApplianceProperties() {
468 Map<String, String> properties = editProperties();
469 logger.debug("Setting thing properties to {}", thing.getLabel());
470 Appliance localAppliance = this.appliance;
471 if (localAppliance != null) {
472 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_DESCRIPTION, localAppliance.getDescription());
473 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_TYPE, localAppliance.getType());
474 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_FUNCTIONALITIES,
475 String.join(", ", localAppliance.getActuatorFunctionalities().keySet()));
477 if (localAppliance.isZigbeeDevice()) {
478 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_TYPE,
479 localAppliance.getZigbeeNode().getType());
480 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_REACHABLE,
481 localAppliance.getZigbeeNode().getReachable());
482 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_POWERSOURCE,
483 localAppliance.getZigbeeNode().getPowerSource());
484 properties.put(Thing.PROPERTY_MAC_ADDRESS, localAppliance.getZigbeeNode().getMacAddress());
487 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, localAppliance.getModule().getFirmwareVersion());
488 properties.put(Thing.PROPERTY_HARDWARE_VERSION, localAppliance.getModule().getHardwareVersion());
489 properties.put(Thing.PROPERTY_VENDOR, localAppliance.getModule().getVendorName());
490 properties.put(Thing.PROPERTY_MODEL_ID, localAppliance.getModule().getVendorModel());
492 updateProperties(properties);