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 if (command == OnOffType.ON) {
146 controller.switchRelayLockOn(entity);
148 controller.switchRelayLockOff(entity);
150 } catch (PlugwiseHAException e) {
151 logger.warn("Unable to switch relay lock {} for appliance '{}'", (State) command,
156 case APPLIANCE_OFFSET_CHANNEL:
157 if (command instanceof QuantityType) {
158 Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
160 : ImperialUnits.FAHRENHEIT;
161 QuantityType<?> state = ((QuantityType<?>) command).toUnit(unit);
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());
173 case APPLIANCE_POWER_CHANNEL:
174 if (command instanceof OnOffType) {
176 if (command == OnOffType.ON) {
177 controller.switchRelayOn(entity);
179 controller.switchRelayOff(entity);
181 } catch (PlugwiseHAException e) {
182 logger.warn("Unable to switch relay {} for appliance '{}'", (State) command, entity.getName());
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);
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());
203 logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
207 private State getDefaultState(String channelID) {
208 State state = UnDefType.NULL;
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;
233 case APPLIANCE_BATTERYLEVELLOW_CHANNEL:
234 case APPLIANCE_LOCK_CHANNEL:
235 case APPLIANCE_POWER_CHANNEL:
236 state = UnDefType.UNDEF;
243 protected void refreshChannel(Appliance entity, ChannelUID channelUID) {
244 String channelID = channelUID.getIdWithoutGroup();
245 State state = getDefaultState(channelID);
246 PlugwiseHAThingConfig config = getPlugwiseThingConfig();
249 case APPLIANCE_BATTERYLEVEL_CHANNEL: {
250 Double batteryLevel = entity.getBatteryLevel().orElse(null);
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);
258 updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.OFF);
263 case APPLIANCE_BATTERYLEVELLOW_CHANNEL: {
264 Double batteryLevel = entity.getBatteryLevel().orElse(null);
266 if (batteryLevel != null) {
268 if (batteryLevel <= config.getLowBatteryPercentage()) {
269 state = OnOffType.ON;
271 state = OnOffType.OFF;
276 case APPLIANCE_CHSTATE_CHANNEL:
277 if (entity.getCHState().isPresent()) {
278 state = OnOffType.from(entity.getCHState().get());
281 case APPLIANCE_DHWSTATE_CHANNEL:
282 if (entity.getDHWState().isPresent()) {
283 state = OnOffType.from(entity.getDHWState().get());
286 case APPLIANCE_LOCK_CHANNEL:
287 Boolean relayLockState = entity.getRelayLockState().orElse(null);
288 if (relayLockState != null) {
289 state = OnOffType.from(relayLockState);
292 case APPLIANCE_OFFSET_CHANNEL:
293 if (entity.getOffsetTemperature().isPresent()) {
294 Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
296 : ImperialUnits.FAHRENHEIT;
297 state = new QuantityType<Temperature>(entity.getOffsetTemperature().get(), unit);
300 case APPLIANCE_POWER_CHANNEL:
301 if (entity.getRelayState().isPresent()) {
302 state = OnOffType.from(entity.getRelayState().get());
305 case APPLIANCE_POWER_USAGE_CHANNEL:
306 if (entity.getPowerUsage().isPresent()) {
307 state = new QuantityType<Power>(entity.getPowerUsage().get(), Units.WATT);
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);
317 case APPLIANCE_TEMPERATURE_CHANNEL:
318 if (entity.getTemperature().isPresent()) {
319 Unit<Temperature> unit = entity.getTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
321 : ImperialUnits.FAHRENHEIT;
322 state = new QuantityType<Temperature>(entity.getTemperature().get(), unit);
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);
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);
337 case APPLIANCE_COOLINGSTATE_CHANNEL:
338 if (entity.getCoolingState().isPresent()) {
339 state = OnOffType.from(entity.getCoolingState().get());
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);
349 case APPLIANCE_FLAMESTATE_CHANNEL:
350 if (entity.getFlameState().isPresent()) {
351 state = OnOffType.from(entity.getFlameState().get());
354 case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
355 if (entity.getIntendedHeatingState().isPresent()) {
356 state = OnOffType.from(entity.getIntendedHeatingState().get());
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);
365 case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
366 if (entity.getOTAppFaultCode().isPresent()) {
367 state = new QuantityType<Dimensionless>(entity.getOTAppFaultCode().get().intValue(), Units.PERCENT);
370 case APPLIANCE_DHWTEMPERATURE_CHANNEL:
371 if (entity.getDHWTemp().isPresent()) {
372 Unit<Temperature> unit = entity.getDHWTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
374 : ImperialUnits.FAHRENHEIT;
375 state = new QuantityType<Temperature>(entity.getDHWTemp().get(), unit);
378 case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
379 if (entity.getOTOEMFaultcode().isPresent()) {
380 state = new QuantityType<Dimensionless>(entity.getOTOEMFaultcode().get().intValue(), Units.PERCENT);
383 case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
384 if (entity.getBoilerTemp().isPresent()) {
385 Unit<Temperature> unit = entity.getBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
387 : ImperialUnits.FAHRENHEIT;
388 state = new QuantityType<Temperature>(entity.getBoilerTemp().get(), unit);
391 case APPLIANCE_DHWSETPOINT_CHANNEL:
392 if (entity.getDHTSetpoint().isPresent()) {
393 Unit<Temperature> unit = entity.getDHTSetpointUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
395 : ImperialUnits.FAHRENHEIT;
396 state = new QuantityType<Temperature>(entity.getDHTSetpoint().get(), unit);
399 case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
400 if (entity.getMaxBoilerTemp().isPresent()) {
401 Unit<Temperature> unit = entity.getMaxBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
403 : ImperialUnits.FAHRENHEIT;
404 state = new QuantityType<Temperature>(entity.getMaxBoilerTemp().get(), unit);
407 case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
408 if (entity.getDHWComfortMode().isPresent()) {
409 state = OnOffType.from(entity.getDHWComfortMode().get());
416 if (state != UnDefType.NULL) {
417 updateState(channelID, state);
421 protected synchronized void addBatteryChannels() {
422 logger.debug("Battery operated appliance: {} detected: adding 'Battery level' and 'Battery low level' channels",
425 ChannelUID channelUIDBatteryLevel = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVEL_CHANNEL);
426 ChannelUID channelUIDBatteryLevelLow = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVELLOW_CHANNEL);
428 boolean channelBatteryLevelExists = false;
429 boolean channelBatteryLowExists = false;
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;
438 if (channelBatteryLevelExists && channelBatteryLowExists) {
443 if (!channelBatteryLevelExists) {
444 ThingBuilder thingBuilder = editThing();
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();
450 thingBuilder.withChannel(channelBatteryLevel);
452 updateThing(thingBuilder.build());
455 if (!channelBatteryLowExists) {
456 ThingBuilder thingBuilder = editThing();
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();
462 thingBuilder.withChannel(channelBatteryLow);
464 updateThing(thingBuilder.build());
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()));
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());
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());
493 updateProperties(properties);