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, true);
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, Boolean forceRefresh)
121 throws PlugwiseHAException {
122 PlugwiseHAThingConfig config = getPlugwiseThingConfig();
123 Appliance appliance = controller.getAppliance(config.getId(), forceRefresh);
129 protected void handleCommand(Appliance entity, ChannelUID channelUID, Command command) throws PlugwiseHAException {
130 String channelID = channelUID.getIdWithoutGroup();
132 PlugwiseHABridgeHandler bridge = this.getPlugwiseHABridge();
133 if (bridge == null) {
137 PlugwiseHAController controller = bridge.getController();
138 if (controller == null) {
143 case APPLIANCE_LOCK_CHANNEL:
144 if (command instanceof OnOffType) {
146 if (command == OnOffType.ON) {
147 controller.switchRelayLockOn(entity);
149 controller.switchRelayLockOff(entity);
151 } catch (PlugwiseHAException e) {
152 logger.warn("Unable to switch relay lock {} for appliance '{}'", (State) command,
157 case APPLIANCE_OFFSET_CHANNEL:
158 if (command instanceof QuantityType) {
159 Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
161 : ImperialUnits.FAHRENHEIT;
162 QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
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());
174 case APPLIANCE_POWER_CHANNEL:
175 if (command instanceof OnOffType) {
177 if (command == OnOffType.ON) {
178 controller.switchRelayOn(entity);
180 controller.switchRelayOff(entity);
182 } catch (PlugwiseHAException e) {
183 logger.warn("Unable to switch relay {} for appliance '{}'", (State) command, entity.getName());
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);
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());
204 logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
208 private State getDefaultState(String channelID) {
209 State state = UnDefType.NULL;
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;
234 case APPLIANCE_BATTERYLEVELLOW_CHANNEL:
235 case APPLIANCE_LOCK_CHANNEL:
236 case APPLIANCE_POWER_CHANNEL:
237 state = UnDefType.UNDEF;
244 protected void refreshChannel(Appliance entity, ChannelUID channelUID) {
245 String channelID = channelUID.getIdWithoutGroup();
246 State state = getDefaultState(channelID);
247 PlugwiseHAThingConfig config = getPlugwiseThingConfig();
250 case APPLIANCE_BATTERYLEVEL_CHANNEL: {
251 Double batteryLevel = entity.getBatteryLevel().orElse(null);
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);
259 updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.OFF);
264 case APPLIANCE_BATTERYLEVELLOW_CHANNEL: {
265 Double batteryLevel = entity.getBatteryLevel().orElse(null);
267 if (batteryLevel != null) {
269 if (batteryLevel <= config.getLowBatteryPercentage()) {
270 state = OnOffType.ON;
272 state = OnOffType.OFF;
277 case APPLIANCE_CHSTATE_CHANNEL:
278 if (entity.getCHState().isPresent()) {
279 state = OnOffType.from(entity.getCHState().get());
282 case APPLIANCE_DHWSTATE_CHANNEL:
283 if (entity.getDHWState().isPresent()) {
284 state = OnOffType.from(entity.getDHWState().get());
287 case APPLIANCE_LOCK_CHANNEL:
288 Boolean relayLockState = entity.getRelayLockState().orElse(null);
289 if (relayLockState != null) {
290 state = OnOffType.from(relayLockState);
293 case APPLIANCE_OFFSET_CHANNEL:
294 if (entity.getOffsetTemperature().isPresent()) {
295 Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
297 : ImperialUnits.FAHRENHEIT;
298 state = new QuantityType<Temperature>(entity.getOffsetTemperature().get(), unit);
301 case APPLIANCE_POWER_CHANNEL:
302 if (entity.getRelayState().isPresent()) {
303 state = OnOffType.from(entity.getRelayState().get());
306 case APPLIANCE_POWER_USAGE_CHANNEL:
307 if (entity.getPowerUsage().isPresent()) {
308 state = new QuantityType<Power>(entity.getPowerUsage().get(), Units.WATT);
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);
318 case APPLIANCE_TEMPERATURE_CHANNEL:
319 if (entity.getTemperature().isPresent()) {
320 Unit<Temperature> unit = entity.getTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
322 : ImperialUnits.FAHRENHEIT;
323 state = new QuantityType<Temperature>(entity.getTemperature().get(), unit);
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);
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);
338 case APPLIANCE_COOLINGSTATE_CHANNEL:
339 if (entity.getCoolingState().isPresent()) {
340 state = OnOffType.from(entity.getCoolingState().get());
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);
350 case APPLIANCE_FLAMESTATE_CHANNEL:
351 if (entity.getFlameState().isPresent()) {
352 state = OnOffType.from(entity.getFlameState().get());
355 case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
356 if (entity.getIntendedHeatingState().isPresent()) {
357 state = OnOffType.from(entity.getIntendedHeatingState().get());
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);
366 case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
367 if (entity.getOTAppFaultCode().isPresent()) {
368 state = new QuantityType<Dimensionless>(entity.getOTAppFaultCode().get().intValue(), Units.PERCENT);
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);