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 state = UnDefType.NULL;
225 case APPLIANCE_BATTERYLEVELLOW_CHANNEL:
226 case APPLIANCE_LOCK_CHANNEL:
227 case APPLIANCE_POWER_CHANNEL:
228 state = UnDefType.UNDEF;
235 protected void refreshChannel(Appliance entity, ChannelUID channelUID) {
236 String channelID = channelUID.getIdWithoutGroup();
237 State state = getDefaultState(channelID);
238 PlugwiseHAThingConfig config = getPlugwiseThingConfig();
241 case APPLIANCE_BATTERYLEVEL_CHANNEL: {
242 Double batteryLevel = entity.getBatteryLevel().orElse(null);
244 if (batteryLevel != null) {
245 batteryLevel = batteryLevel * 100;
246 state = new QuantityType<Dimensionless>(batteryLevel.intValue(), Units.PERCENT);
247 if (batteryLevel <= config.getLowBatteryPercentage()) {
248 updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.ON);
250 updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.OFF);
255 case APPLIANCE_BATTERYLEVELLOW_CHANNEL: {
256 Double batteryLevel = entity.getBatteryLevel().orElse(null);
258 if (batteryLevel != null) {
260 if (batteryLevel <= config.getLowBatteryPercentage()) {
261 state = OnOffType.ON;
263 state = OnOffType.OFF;
268 case APPLIANCE_CHSTATE_CHANNEL:
269 if (entity.getCHState().isPresent()) {
270 state = OnOffType.from(entity.getCHState().get());
273 case APPLIANCE_DHWSTATE_CHANNEL:
274 if (entity.getDHWState().isPresent()) {
275 state = OnOffType.from(entity.getDHWState().get());
278 case APPLIANCE_LOCK_CHANNEL:
279 Boolean relayLockState = entity.getRelayLockState().orElse(null);
280 if (relayLockState != null) {
281 state = OnOffType.from(relayLockState);
284 case APPLIANCE_OFFSET_CHANNEL:
285 if (entity.getOffsetTemperature().isPresent()) {
286 Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
288 : ImperialUnits.FAHRENHEIT;
289 state = new QuantityType<Temperature>(entity.getOffsetTemperature().get(), unit);
292 case APPLIANCE_POWER_CHANNEL:
293 if (entity.getRelayState().isPresent()) {
294 state = OnOffType.from(entity.getRelayState().get());
297 case APPLIANCE_POWER_USAGE_CHANNEL:
298 if (entity.getPowerUsage().isPresent()) {
299 state = new QuantityType<Power>(entity.getPowerUsage().get(), Units.WATT);
302 case APPLIANCE_SETPOINT_CHANNEL:
303 if (entity.getSetpointTemperature().isPresent()) {
304 Unit<Temperature> unit = entity.getSetpointTemperatureUnit().orElse(UNIT_CELSIUS)
305 .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
306 state = new QuantityType<Temperature>(entity.getSetpointTemperature().get(), unit);
309 case APPLIANCE_TEMPERATURE_CHANNEL:
310 if (entity.getTemperature().isPresent()) {
311 Unit<Temperature> unit = entity.getTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
313 : ImperialUnits.FAHRENHEIT;
314 state = new QuantityType<Temperature>(entity.getTemperature().get(), unit);
317 case APPLIANCE_VALVEPOSITION_CHANNEL:
318 if (entity.getValvePosition().isPresent()) {
319 Double valvePosition = entity.getValvePosition().get() * 100;
320 state = new QuantityType<Dimensionless>(valvePosition.intValue(), Units.PERCENT);
323 case APPLIANCE_WATERPRESSURE_CHANNEL:
324 if (entity.getWaterPressure().isPresent()) {
325 Unit<Pressure> unit = HECTO(SIUnits.PASCAL);
326 state = new QuantityType<Pressure>(entity.getWaterPressure().get(), unit);
329 case APPLIANCE_COOLINGSTATE_CHANNEL:
330 if (entity.getCoolingState().isPresent()) {
331 state = OnOffType.from(entity.getCoolingState().get());
334 case APPLIANCE_INTENDEDBOILERTEMP_CHANNEL:
335 if (entity.getIntendedBoilerTemp().isPresent()) {
336 Unit<Temperature> unit = entity.getIntendedBoilerTempUnit().orElse(UNIT_CELSIUS)
337 .equals(UNIT_CELSIUS) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
338 state = new QuantityType<Temperature>(entity.getIntendedBoilerTemp().get(), unit);
341 case APPLIANCE_FLAMESTATE_CHANNEL:
342 if (entity.getFlameState().isPresent()) {
343 state = OnOffType.from(entity.getFlameState().get());
346 case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
347 if (entity.getIntendedHeatingState().isPresent()) {
348 state = OnOffType.from(entity.getIntendedHeatingState().get());
351 case APPLIANCE_MODULATIONLEVEL_CHANNEL:
352 if (entity.getModulationLevel().isPresent()) {
353 Double modulationLevel = entity.getModulationLevel().get() * 100;
354 state = new QuantityType<Dimensionless>(modulationLevel.intValue(), Units.PERCENT);
357 case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
358 if (entity.getOTAppFaultCode().isPresent()) {
359 state = new QuantityType<Dimensionless>(entity.getOTAppFaultCode().get().intValue(), Units.PERCENT);
362 case APPLIANCE_DHWTEMPERATURE_CHANNEL:
363 if (entity.getDHWTemp().isPresent()) {
364 Unit<Temperature> unit = entity.getDHWTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
366 : ImperialUnits.FAHRENHEIT;
367 state = new QuantityType<Temperature>(entity.getDHWTemp().get(), unit);
370 case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
371 if (entity.getOTOEMFaultcode().isPresent()) {
372 state = new QuantityType<Dimensionless>(entity.getOTOEMFaultcode().get().intValue(), Units.PERCENT);
375 case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
376 if (entity.getBoilerTemp().isPresent()) {
377 Unit<Temperature> unit = entity.getBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
379 : ImperialUnits.FAHRENHEIT;
380 state = new QuantityType<Temperature>(entity.getBoilerTemp().get(), unit);
383 case APPLIANCE_DHWSETPOINT_CHANNEL:
384 if (entity.getDHTSetpoint().isPresent()) {
385 Unit<Temperature> unit = entity.getDHTSetpointUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
387 : ImperialUnits.FAHRENHEIT;
388 state = new QuantityType<Temperature>(entity.getDHTSetpoint().get(), unit);
391 case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
392 if (entity.getMaxBoilerTemp().isPresent()) {
393 Unit<Temperature> unit = entity.getMaxBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
395 : ImperialUnits.FAHRENHEIT;
396 state = new QuantityType<Temperature>(entity.getMaxBoilerTemp().get(), unit);
399 case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
400 if (entity.getDHWComfortMode().isPresent()) {
401 state = OnOffType.from(entity.getDHWComfortMode().get());
408 if (state != UnDefType.NULL) {
409 updateState(channelID, state);
413 protected synchronized void addBatteryChannels() {
414 logger.debug("Battery operated appliance: {} detected: adding 'Battery level' and 'Battery low level' channels",
417 ChannelUID channelUIDBatteryLevel = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVEL_CHANNEL);
418 ChannelUID channelUIDBatteryLevelLow = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVELLOW_CHANNEL);
420 boolean channelBatteryLevelExists = false;
421 boolean channelBatteryLowExists = false;
423 List<Channel> channels = getThing().getChannels();
424 for (Channel channel : channels) {
425 if (channel.getUID().equals(channelUIDBatteryLevel)) {
426 channelBatteryLevelExists = true;
427 } else if (channel.getUID().equals(channelUIDBatteryLevelLow)) {
428 channelBatteryLowExists = true;
430 if (channelBatteryLevelExists && channelBatteryLowExists) {
435 if (!channelBatteryLevelExists) {
436 ThingBuilder thingBuilder = editThing();
438 Channel channelBatteryLevel = ChannelBuilder.create(channelUIDBatteryLevel, "Number")
439 .withType(CHANNEL_TYPE_BATTERYLEVEL).withKind(ChannelKind.STATE).withLabel("Battery Level")
440 .withDescription("Represents the battery level as a percentage (0-100%)").build();
442 thingBuilder.withChannel(channelBatteryLevel);
444 updateThing(thingBuilder.build());
447 if (!channelBatteryLowExists) {
448 ThingBuilder thingBuilder = editThing();
450 Channel channelBatteryLow = ChannelBuilder.create(channelUIDBatteryLevelLow, "Switch")
451 .withType(CHANNEL_TYPE_BATTERYLEVELLOW).withKind(ChannelKind.STATE).withLabel("Battery Low Level")
452 .withDescription("Switches ON when battery level gets below threshold level").build();
454 thingBuilder.withChannel(channelBatteryLow);
456 updateThing(thingBuilder.build());
460 protected void setApplianceProperties() {
461 Map<String, String> properties = editProperties();
462 logger.debug("Setting thing properties to {}", thing.getLabel());
463 Appliance localAppliance = this.appliance;
464 if (localAppliance != null) {
465 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_DESCRIPTION, localAppliance.getDescription());
466 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_TYPE, localAppliance.getType());
467 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_FUNCTIONALITIES,
468 String.join(", ", localAppliance.getActuatorFunctionalities().keySet()));
470 if (localAppliance.isZigbeeDevice()) {
471 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_TYPE,
472 localAppliance.getZigbeeNode().getType());
473 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_REACHABLE,
474 localAppliance.getZigbeeNode().getReachable());
475 properties.put(PlugwiseHABindingConstants.APPLIANCE_PROPERTY_ZB_POWERSOURCE,
476 localAppliance.getZigbeeNode().getPowerSource());
477 properties.put(Thing.PROPERTY_MAC_ADDRESS, localAppliance.getZigbeeNode().getMacAddress());
480 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, localAppliance.getModule().getFirmwareVersion());
481 properties.put(Thing.PROPERTY_HARDWARE_VERSION, localAppliance.getModule().getHardwareVersion());
482 properties.put(Thing.PROPERTY_VENDOR, localAppliance.getModule().getVendorName());
483 properties.put(Thing.PROPERTY_MODEL_ID, localAppliance.getModule().getVendorModel());
485 updateProperties(properties);