]> git.basschouten.com Git - openhab-addons.git/blob
449357836a5db96df3c328e056eaa7cdf41c778a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.plugwiseha.internal.handler;
14
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;
21
22 import java.util.List;
23 import java.util.Map;
24
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;
30
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;
55
56 /**
57  * The {@link PlugwiseHAApplianceHandler} class is responsible for handling
58  * commands and status updates for the Plugwise Home Automation appliances.
59  * Extends @{link PlugwiseHABaseHandler}
60  *
61  * @author Bas van Wetten - Initial contribution
62  * @author Leo Siepel - finish initial contribution
63  *
64  */
65 @NonNullByDefault
66 public class PlugwiseHAApplianceHandler extends PlugwiseHABaseHandler<Appliance, PlugwiseHAThingConfig> {
67
68     private @Nullable Appliance appliance;
69     private final Logger logger = LoggerFactory.getLogger(PlugwiseHAApplianceHandler.class);
70
71     // Constructor
72
73     public PlugwiseHAApplianceHandler(Thing thing) {
74         super(thing);
75     }
76
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);
82     }
83
84     // Overrides
85
86     @Override
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.");
93                 return;
94             }
95
96             try {
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();
104                         }
105                         setApplianceProperties();
106                         updateStatus(ONLINE);
107                     } else {
108                         updateStatus(OFFLINE);
109                     }
110                 } else {
111                     updateStatus(OFFLINE, BRIDGE_OFFLINE);
112                 }
113             } catch (PlugwiseHAException e) {
114                 updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getMessage());
115             }
116         }
117     }
118
119     @Override
120     protected @Nullable Appliance getEntity(PlugwiseHAController controller) throws PlugwiseHAException {
121         PlugwiseHAThingConfig config = getPlugwiseThingConfig();
122         return controller.getAppliance(config.getId());
123     }
124
125     @Override
126     protected void handleCommand(Appliance entity, ChannelUID channelUID, Command command) throws PlugwiseHAException {
127         String channelID = channelUID.getIdWithoutGroup();
128
129         PlugwiseHABridgeHandler bridge = this.getPlugwiseHABridge();
130         if (bridge == null) {
131             return;
132         }
133
134         PlugwiseHAController controller = bridge.getController();
135         if (controller == null) {
136             return;
137         }
138
139         switch (channelID) {
140             case APPLIANCE_LOCK_CHANNEL:
141                 if (command instanceof OnOffType onOffCommand) {
142                     try {
143                         controller.setRelay(entity, (command == OnOffType.ON));
144                     } catch (PlugwiseHAException e) {
145                         logger.warn("Unable to switch relay lock {} for appliance '{}'", onOffCommand,
146                                 entity.getName());
147                     }
148                 }
149                 break;
150             case APPLIANCE_OFFSET_CHANNEL:
151                 if (command instanceof QuantityType quantityCommand) {
152                     Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
153                             ? SIUnits.CELSIUS
154                             : ImperialUnits.FAHRENHEIT;
155                     QuantityType<?> state = quantityCommand.toUnit(unit);
156
157                     if (state != null) {
158                         try {
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());
163                         }
164                     }
165                 }
166                 break;
167             case APPLIANCE_POWER_CHANNEL:
168                 if (command instanceof OnOffType onOffCommand) {
169                     try {
170                         controller.setRelay(entity, command == OnOffType.ON);
171                     } catch (PlugwiseHAException e) {
172                         logger.warn("Unable to switch relay {} for appliance '{}'", onOffCommand, entity.getName());
173                     }
174                 }
175                 break;
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);
181
182                     if (state != null) {
183                         try {
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());
188                         }
189                     }
190                 }
191                 break;
192             default:
193                 logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
194         }
195     }
196
197     private State getDefaultState(String channelID) {
198         State state = UnDefType.NULL;
199         switch (channelID) {
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;
223                 break;
224             case APPLIANCE_BATTERYLEVELLOW_CHANNEL:
225             case APPLIANCE_LOCK_CHANNEL:
226             case APPLIANCE_POWER_CHANNEL:
227                 state = UnDefType.UNDEF;
228                 break;
229         }
230         return state;
231     }
232
233     @Override
234     protected void refreshChannel(Appliance entity, ChannelUID channelUID) {
235         String channelID = channelUID.getIdWithoutGroup();
236         State state = getDefaultState(channelID);
237         PlugwiseHAThingConfig config = getPlugwiseThingConfig();
238
239         switch (channelID) {
240             case APPLIANCE_BATTERYLEVEL_CHANNEL: {
241                 Double batteryLevel = entity.getBatteryLevel().orElse(null);
242
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);
248                     } else {
249                         updateState(APPLIANCE_BATTERYLEVELLOW_CHANNEL, OnOffType.OFF);
250                     }
251                 }
252                 break;
253             }
254             case APPLIANCE_BATTERYLEVELLOW_CHANNEL: {
255                 Double batteryLevel = entity.getBatteryLevel().orElse(null);
256
257                 if (batteryLevel != null) {
258                     batteryLevel *= 100;
259                     if (batteryLevel <= config.getLowBatteryPercentage()) {
260                         state = OnOffType.ON;
261                     } else {
262                         state = OnOffType.OFF;
263                     }
264                 }
265                 break;
266             }
267             case APPLIANCE_CHSTATE_CHANNEL:
268                 if (entity.getCHState().isPresent()) {
269                     state = OnOffType.from(entity.getCHState().get());
270                 }
271                 break;
272             case APPLIANCE_DHWSTATE_CHANNEL:
273                 if (entity.getDHWState().isPresent()) {
274                     state = OnOffType.from(entity.getDHWState().get());
275                 }
276                 break;
277             case APPLIANCE_LOCK_CHANNEL:
278                 Boolean relayLockState = entity.getRelayLockState().orElse(null);
279                 if (relayLockState != null) {
280                     state = OnOffType.from(relayLockState);
281                 }
282                 break;
283             case APPLIANCE_OFFSET_CHANNEL:
284                 if (entity.getOffsetTemperature().isPresent()) {
285                     Unit<Temperature> unit = entity.getOffsetTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
286                             ? SIUnits.CELSIUS
287                             : ImperialUnits.FAHRENHEIT;
288                     state = new QuantityType<Temperature>(entity.getOffsetTemperature().get(), unit);
289                 }
290                 break;
291             case APPLIANCE_POWER_CHANNEL:
292                 if (entity.getRelayState().isPresent()) {
293                     state = OnOffType.from(entity.getRelayState().get());
294                 }
295                 break;
296             case APPLIANCE_POWER_USAGE_CHANNEL:
297                 if (entity.getPowerUsage().isPresent()) {
298                     state = new QuantityType<Power>(entity.getPowerUsage().get(), Units.WATT);
299                 }
300                 break;
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);
306                 }
307                 break;
308             case APPLIANCE_TEMPERATURE_CHANNEL:
309                 if (entity.getTemperature().isPresent()) {
310                     Unit<Temperature> unit = entity.getTemperatureUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
311                             ? SIUnits.CELSIUS
312                             : ImperialUnits.FAHRENHEIT;
313                     state = new QuantityType<Temperature>(entity.getTemperature().get(), unit);
314                 }
315                 break;
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);
320                 }
321                 break;
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);
326                 }
327                 break;
328             case APPLIANCE_COOLINGSTATE_CHANNEL:
329                 if (entity.getCoolingState().isPresent()) {
330                     state = OnOffType.from(entity.getCoolingState().get());
331                 }
332                 break;
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);
338                 }
339                 break;
340             case APPLIANCE_FLAMESTATE_CHANNEL:
341                 if (entity.getFlameState().isPresent()) {
342                     state = OnOffType.from(entity.getFlameState().get());
343                 }
344                 break;
345             case APPLIANCE_INTENDEDHEATINGSTATE_CHANNEL:
346                 if (entity.getIntendedHeatingState().isPresent()) {
347                     state = OnOffType.from(entity.getIntendedHeatingState().get());
348                 }
349                 break;
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);
354                 }
355                 break;
356             case APPLIANCE_OTAPPLICATIONFAULTCODE_CHANNEL:
357                 if (entity.getOTAppFaultCode().isPresent()) {
358                     state = new QuantityType<Dimensionless>(entity.getOTAppFaultCode().get().intValue(), Units.PERCENT);
359                 }
360                 break;
361             case APPLIANCE_RETURNWATERTEMPERATURE_CHANNEL:
362                 if (entity.getBoilerTemp().isPresent()) {
363                     Unit<Temperature> unit = entity.getReturnWaterTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
364                             ? SIUnits.CELSIUS
365                             : ImperialUnits.FAHRENHEIT;
366                     state = new QuantityType<Temperature>(entity.getReturnWaterTemp().get(), unit);
367                 }
368                 break;
369             case APPLIANCE_DHWTEMPERATURE_CHANNEL:
370                 if (entity.getDHWTemp().isPresent()) {
371                     Unit<Temperature> unit = entity.getDHWTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
372                             ? SIUnits.CELSIUS
373                             : ImperialUnits.FAHRENHEIT;
374                     state = new QuantityType<Temperature>(entity.getDHWTemp().get(), unit);
375                 }
376                 break;
377             case APPLIANCE_OTOEMFAULTCODE_CHANNEL:
378                 if (entity.getOTOEMFaultcode().isPresent()) {
379                     state = new QuantityType<Dimensionless>(entity.getOTOEMFaultcode().get().intValue(), Units.PERCENT);
380                 }
381                 break;
382             case APPLIANCE_BOILERTEMPERATURE_CHANNEL:
383                 if (entity.getBoilerTemp().isPresent()) {
384                     Unit<Temperature> unit = entity.getBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
385                             ? SIUnits.CELSIUS
386                             : ImperialUnits.FAHRENHEIT;
387                     state = new QuantityType<Temperature>(entity.getBoilerTemp().get(), unit);
388                 }
389                 break;
390             case APPLIANCE_DHWSETPOINT_CHANNEL:
391                 if (entity.getDHTSetpoint().isPresent()) {
392                     Unit<Temperature> unit = entity.getDHTSetpointUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
393                             ? SIUnits.CELSIUS
394                             : ImperialUnits.FAHRENHEIT;
395                     state = new QuantityType<Temperature>(entity.getDHTSetpoint().get(), unit);
396                 }
397                 break;
398             case APPLIANCE_MAXBOILERTEMPERATURE_CHANNEL:
399                 if (entity.getMaxBoilerTemp().isPresent()) {
400                     Unit<Temperature> unit = entity.getMaxBoilerTempUnit().orElse(UNIT_CELSIUS).equals(UNIT_CELSIUS)
401                             ? SIUnits.CELSIUS
402                             : ImperialUnits.FAHRENHEIT;
403                     state = new QuantityType<Temperature>(entity.getMaxBoilerTemp().get(), unit);
404                 }
405                 break;
406             case APPLIANCE_DHWCOMFORTMODE_CHANNEL:
407                 if (entity.getDHWComfortMode().isPresent()) {
408                     state = OnOffType.from(entity.getDHWComfortMode().get());
409                 }
410                 break;
411             default:
412                 break;
413         }
414
415         if (state != UnDefType.NULL) {
416             updateState(channelID, state);
417         }
418     }
419
420     protected synchronized void addBatteryChannels() {
421         logger.debug("Battery operated appliance: {} detected: adding 'Battery level' and 'Battery low level' channels",
422                 thing.getLabel());
423
424         ChannelUID channelUIDBatteryLevel = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVEL_CHANNEL);
425         ChannelUID channelUIDBatteryLevelLow = new ChannelUID(getThing().getUID(), APPLIANCE_BATTERYLEVELLOW_CHANNEL);
426
427         boolean channelBatteryLevelExists = false;
428         boolean channelBatteryLowExists = false;
429
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;
436             }
437             if (channelBatteryLevelExists && channelBatteryLowExists) {
438                 break;
439             }
440         }
441
442         if (!channelBatteryLevelExists) {
443             ThingBuilder thingBuilder = editThing();
444
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();
448
449             thingBuilder.withChannel(channelBatteryLevel);
450
451             updateThing(thingBuilder.build());
452         }
453
454         if (!channelBatteryLowExists) {
455             ThingBuilder thingBuilder = editThing();
456
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();
460
461             thingBuilder.withChannel(channelBatteryLow);
462
463             updateThing(thingBuilder.build());
464         }
465     }
466
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()));
476
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());
485             }
486
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());
491         }
492         updateProperties(properties);
493     }
494 }