2 * Copyright (c) 2010-2024 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.melcloud.internal.handler;
15 import static org.openhab.binding.melcloud.internal.MelCloudBindingConstants.*;
16 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
18 import java.math.BigDecimal;
19 import java.math.RoundingMode;
20 import java.time.LocalDateTime;
21 import java.time.ZoneId;
22 import java.time.ZoneOffset;
23 import java.time.ZonedDateTime;
24 import java.time.format.DateTimeFormatter;
25 import java.util.Optional;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
29 import javax.measure.quantity.Temperature;
31 import org.openhab.binding.melcloud.internal.api.json.HeatpumpDeviceStatus;
32 import org.openhab.binding.melcloud.internal.config.HeatpumpDeviceConfig;
33 import org.openhab.binding.melcloud.internal.exceptions.MelCloudCommException;
34 import org.openhab.binding.melcloud.internal.exceptions.MelCloudLoginException;
35 import org.openhab.core.library.types.DateTimeType;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.library.unit.SIUnits;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.Channel;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.ThingStatusInfo;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.thing.binding.ThingHandler;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * The {@link MelCloudHeatpumpDeviceHandler} is responsible for handling commands, which are
56 * sent to one of the channels.
58 * @author Wietse van Buitenen - Initial contribution
60 public class MelCloudHeatpumpDeviceHandler extends BaseThingHandler {
61 private static final long EFFECTIVE_FLAG_POWER = 1L;
62 private static final long EFFECTIVE_FLAG_TEMPERATURE_ZONE1 = 8589934720L;
63 private static final long EFFECTIVE_FLAG_HOTWATER = 65536L;
65 private final Logger logger = LoggerFactory.getLogger(MelCloudHeatpumpDeviceHandler.class);
66 private HeatpumpDeviceConfig config;
67 private MelCloudAccountHandler melCloudHandler;
68 private HeatpumpDeviceStatus heatpumpDeviceStatus;
69 private ScheduledFuture<?> refreshTask;
71 public MelCloudHeatpumpDeviceHandler(Thing thing) {
76 public void initialize() {
77 logger.debug("Initializing {} handler.", getThing().getThingTypeUID());
79 Bridge bridge = getBridge();
81 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge Not set");
85 config = getConfigAs(HeatpumpDeviceConfig.class);
86 logger.debug("Heatpump device config: {}", config);
88 initializeBridge(bridge.getHandler(), bridge.getStatus());
92 public void dispose() {
93 logger.debug("Running dispose()");
94 if (refreshTask != null) {
95 refreshTask.cancel(true);
98 melCloudHandler = null;
102 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
103 logger.debug("bridgeStatusChanged {} for thing {}", bridgeStatusInfo, getThing().getUID());
104 Bridge bridge = getBridge();
105 if (bridge != null) {
106 initializeBridge(bridge.getHandler(), bridgeStatusInfo.getStatus());
110 private void initializeBridge(ThingHandler thingHandler, ThingStatus bridgeStatus) {
111 logger.debug("initializeBridge {} for thing {}", bridgeStatus, getThing().getUID());
113 if (thingHandler != null && bridgeStatus != null) {
114 melCloudHandler = (MelCloudAccountHandler) thingHandler;
116 if (bridgeStatus == ThingStatus.ONLINE) {
117 updateStatus(ThingStatus.ONLINE);
118 startAutomaticRefresh();
120 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
123 updateStatus(ThingStatus.OFFLINE);
128 public void handleCommand(ChannelUID channelUID, Command command) {
129 logger.debug("Received command '{}' to channel {}", command, channelUID);
131 if (command instanceof RefreshType) {
132 logger.debug("Refresh command not supported");
136 if (melCloudHandler == null) {
137 logger.warn("No connection to MELCloud available, ignoring command");
141 if (heatpumpDeviceStatus == null) {
142 logger.info("No initial data available, bridge is probably offline. Ignoring command");
146 HeatpumpDeviceStatus cmdtoSend = getHeatpumpDeviceStatusCopy(heatpumpDeviceStatus);
147 cmdtoSend.setEffectiveFlags(0L);
149 switch (channelUID.getId()) {
151 cmdtoSend.setPower(command == OnOffType.ON);
152 cmdtoSend.setEffectiveFlags(EFFECTIVE_FLAG_POWER);
154 case CHANNEL_SET_TEMPERATURE_ZONE1:
155 BigDecimal val = null;
156 if (command instanceof QuantityType) {
157 QuantityType<Temperature> quantity = new QuantityType<Temperature>(command.toString())
159 if (quantity != null) {
160 val = quantity.toBigDecimal().setScale(1, RoundingMode.HALF_UP);
162 double v = Math.round(val.doubleValue() * 2) / 2.0;
163 cmdtoSend.setSetTemperatureZone1(v);
164 cmdtoSend.setEffectiveFlags(EFFECTIVE_FLAG_TEMPERATURE_ZONE1);
168 logger.debug("Can't convert '{}' to set temperature", command);
171 case CHANNEL_FORCED_HOTWATERMODE:
172 cmdtoSend.setForcedHotWaterMode(command == OnOffType.ON);
173 cmdtoSend.setEffectiveFlags(EFFECTIVE_FLAG_HOTWATER);
176 logger.debug("Read-only or unknown channel {}, skipping update", channelUID);
179 if (cmdtoSend.getEffectiveFlags() > 0) {
180 cmdtoSend.setHasPendingCommand(true);
181 cmdtoSend.setDeviceID(config.deviceID);
183 HeatpumpDeviceStatus newHeatpumpDeviceStatus = melCloudHandler.sendHeatpumpDeviceStatus(cmdtoSend);
184 updateChannels(newHeatpumpDeviceStatus);
185 } catch (MelCloudLoginException e) {
186 logger.warn("Command '{}' to channel '{}' failed due to login error, reason {}. ", command, channelUID,
188 } catch (MelCloudCommException e) {
189 logger.warn("Command '{}' to channel '{}' failed, reason {}. ", command, channelUID, e.getMessage(), e);
192 logger.debug("Nothing to send");
196 private HeatpumpDeviceStatus getHeatpumpDeviceStatusCopy(HeatpumpDeviceStatus heatpumpDeviceStatus) {
197 HeatpumpDeviceStatus copy = new HeatpumpDeviceStatus();
198 synchronized (this) {
199 copy.setDeviceID(heatpumpDeviceStatus.getDeviceID());
200 copy.setEffectiveFlags(heatpumpDeviceStatus.getEffectiveFlags());
201 copy.setPower(heatpumpDeviceStatus.getPower());
202 copy.setSetTemperatureZone1(heatpumpDeviceStatus.getSetTemperatureZone1());
203 copy.setForcedHotWaterMode(heatpumpDeviceStatus.getForcedHotWaterMode());
204 copy.setHasPendingCommand(heatpumpDeviceStatus.getHasPendingCommand());
209 private void startAutomaticRefresh() {
210 if (refreshTask == null || refreshTask.isCancelled()) {
211 refreshTask = scheduler.scheduleWithFixedDelay(this::getDeviceDataAndUpdateChannels, 1,
212 config.pollingInterval, TimeUnit.SECONDS);
216 private void getDeviceDataAndUpdateChannels() {
217 if (melCloudHandler.isConnected()) {
218 logger.debug("Update device '{}' channels", getThing().getThingTypeUID());
220 HeatpumpDeviceStatus newHeatpumpDeviceStatus = melCloudHandler
221 .fetchHeatpumpDeviceStatus(config.deviceID, Optional.ofNullable(config.buildingID));
222 updateChannels(newHeatpumpDeviceStatus);
223 } catch (MelCloudLoginException e) {
224 logger.debug("Login error occurred during device '{}' polling, reason {}. ",
225 getThing().getThingTypeUID(), e.getMessage(), e);
226 } catch (MelCloudCommException e) {
227 logger.debug("Error occurred during device '{}' polling, reason {}. ", getThing().getThingTypeUID(),
231 logger.debug("Connection to MELCloud is not open, skipping periodic update");
235 private synchronized void updateChannels(HeatpumpDeviceStatus newHeatpumpDeviceStatus) {
236 heatpumpDeviceStatus = newHeatpumpDeviceStatus;
237 for (Channel channel : getThing().getChannels()) {
238 updateChannels(channel.getUID().getId(), heatpumpDeviceStatus);
242 private void updateChannels(String channelId, HeatpumpDeviceStatus heatpumpDeviceStatus) {
245 updateState(CHANNEL_POWER, OnOffType.from(heatpumpDeviceStatus.getPower()));
247 case CHANNEL_TANKWATERTEMPERATURE:
248 updateState(CHANNEL_TANKWATERTEMPERATURE,
249 new DecimalType(heatpumpDeviceStatus.getTankWaterTemperature()));
251 case CHANNEL_SET_TEMPERATURE_ZONE1:
252 updateState(CHANNEL_SET_TEMPERATURE_ZONE1,
253 new QuantityType<>(heatpumpDeviceStatus.getSetTemperatureZone1(), SIUnits.CELSIUS));
255 case CHANNEL_ROOM_TEMPERATURE_ZONE1:
256 updateState(CHANNEL_ROOM_TEMPERATURE_ZONE1,
257 new DecimalType(heatpumpDeviceStatus.getRoomTemperatureZone1()));
259 case CHANNEL_FORCED_HOTWATERMODE:
260 updateState(CHANNEL_FORCED_HOTWATERMODE, OnOffType.from(heatpumpDeviceStatus.getForcedHotWaterMode()));
262 case CHANNEL_LAST_COMMUNICATION:
263 updateState(CHANNEL_LAST_COMMUNICATION,
264 new DateTimeType(convertDateTime(heatpumpDeviceStatus.getLastCommunication())));
266 case CHANNEL_NEXT_COMMUNICATION:
267 updateState(CHANNEL_NEXT_COMMUNICATION,
268 new DateTimeType(convertDateTime(heatpumpDeviceStatus.getNextCommunication())));
270 case CHANNEL_HAS_PENDING_COMMAND:
271 updateState(CHANNEL_HAS_PENDING_COMMAND, OnOffType.from(heatpumpDeviceStatus.getHasPendingCommand()));
273 case CHANNEL_OFFLINE:
274 updateState(CHANNEL_OFFLINE, OnOffType.from(heatpumpDeviceStatus.getOffline()));
279 private ZonedDateTime convertDateTime(String dateTime) {
280 return ZonedDateTime.ofInstant(LocalDateTime.parse(dateTime, DateTimeFormatter.ISO_LOCAL_DATE_TIME),
281 ZoneOffset.UTC, ZoneId.systemDefault());