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.DeviceStatus;
32 import org.openhab.binding.melcloud.internal.config.AcDeviceConfig;
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.types.StringType;
40 import org.openhab.core.library.unit.SIUnits;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.Channel;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.ThingStatusInfo;
48 import org.openhab.core.thing.binding.BaseThingHandler;
49 import org.openhab.core.thing.binding.ThingHandler;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * The {@link MelCloudDeviceHandler} is responsible for handling commands, which are
57 * sent to one of the channels.
59 * @author Luca Calcaterra - Initial contribution
60 * @author Pauli Anttila - Refactoring
63 public class MelCloudDeviceHandler extends BaseThingHandler {
65 private static final int EFFECTIVE_FLAG_POWER = 0x01;
66 private static final int EFFECTIVE_FLAG_OPERATION_MODE = 0x02;
67 private static final int EFFECTIVE_FLAG_TEMPERATURE = 0x04;
68 private static final int EFFECTIVE_FLAG_FAN_SPEED = 0x08;
69 private static final int EFFECTIVE_FLAG_VANE_VERTICAL = 0x10;
70 private static final int EFFECTIVE_FLAG_VANE_HORIZONTAL = 0x100;
72 private final Logger logger = LoggerFactory.getLogger(MelCloudDeviceHandler.class);
73 private AcDeviceConfig config;
74 private MelCloudAccountHandler melCloudHandler;
75 private DeviceStatus deviceStatus;
76 private ScheduledFuture<?> refreshTask;
78 public MelCloudDeviceHandler(Thing thing) {
83 public void initialize() {
84 logger.debug("Initializing {} handler.", getThing().getThingTypeUID());
86 Bridge bridge = getBridge();
88 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge Not set");
92 config = getConfigAs(AcDeviceConfig.class);
93 logger.debug("A.C. device config: {}", config);
95 initializeBridge(bridge.getHandler(), bridge.getStatus());
99 public void dispose() {
100 logger.debug("Running dispose()");
101 if (refreshTask != null) {
102 refreshTask.cancel(true);
105 melCloudHandler = null;
109 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
110 logger.debug("bridgeStatusChanged {} for thing {}", bridgeStatusInfo, getThing().getUID());
111 Bridge bridge = getBridge();
112 if (bridge != null) {
113 initializeBridge(bridge.getHandler(), bridgeStatusInfo.getStatus());
117 private void initializeBridge(ThingHandler thingHandler, ThingStatus bridgeStatus) {
118 logger.debug("initializeBridge {} for thing {}", bridgeStatus, getThing().getUID());
120 if (thingHandler != null && bridgeStatus != null) {
121 melCloudHandler = (MelCloudAccountHandler) thingHandler;
123 if (bridgeStatus == ThingStatus.ONLINE) {
124 updateStatus(ThingStatus.ONLINE);
125 startAutomaticRefresh();
127 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
130 updateStatus(ThingStatus.OFFLINE);
135 public void handleCommand(ChannelUID channelUID, Command command) {
136 logger.debug("Received command '{}' to channel {}", command, channelUID);
138 if (command instanceof RefreshType) {
139 logger.debug("Refresh command not supported");
143 if (melCloudHandler == null) {
144 logger.warn("No connection to MELCloud available, ignoring command");
148 if (deviceStatus == null) {
149 logger.info("No initial data available, bridge is probably offline. Ignoring command");
153 DeviceStatus cmdtoSend = getDeviceStatusCopy(deviceStatus);
154 cmdtoSend.setEffectiveFlags(0);
156 switch (channelUID.getId()) {
158 cmdtoSend.setPower(command == OnOffType.ON);
159 cmdtoSend.setEffectiveFlags(EFFECTIVE_FLAG_POWER);
161 case CHANNEL_OPERATION_MODE:
162 cmdtoSend.setOperationMode(Integer.parseInt(command.toString()));
163 cmdtoSend.setEffectiveFlags(EFFECTIVE_FLAG_OPERATION_MODE);
165 case CHANNEL_SET_TEMPERATURE:
166 BigDecimal val = null;
167 if (command instanceof QuantityType) {
168 QuantityType<Temperature> quantity = new QuantityType<Temperature>(command.toString())
170 if (quantity != null) {
171 val = quantity.toBigDecimal().setScale(1, RoundingMode.HALF_UP);
173 double v = Math.round(val.doubleValue() * 2) / 2.0;
174 cmdtoSend.setSetTemperature(v);
175 cmdtoSend.setEffectiveFlags(EFFECTIVE_FLAG_TEMPERATURE);
179 logger.debug("Can't convert '{}' to set temperature", command);
182 case CHANNEL_FAN_SPEED:
183 cmdtoSend.setSetFanSpeed(Integer.parseInt(command.toString()));
184 cmdtoSend.setEffectiveFlags(EFFECTIVE_FLAG_FAN_SPEED);
186 case CHANNEL_VANE_VERTICAL:
187 cmdtoSend.setVaneVertical(Integer.parseInt(command.toString()));
188 cmdtoSend.setEffectiveFlags(EFFECTIVE_FLAG_VANE_VERTICAL);
190 case CHANNEL_VANE_HORIZONTAL:
191 cmdtoSend.setVaneHorizontal(Integer.parseInt(command.toString()));
192 cmdtoSend.setEffectiveFlags(EFFECTIVE_FLAG_VANE_HORIZONTAL);
195 logger.debug("Read-only or unknown channel {}, skipping update", channelUID);
198 if (cmdtoSend.getEffectiveFlags() > 0) {
199 cmdtoSend.setHasPendingCommand(true);
200 cmdtoSend.setDeviceID(config.deviceID);
202 DeviceStatus newDeviceStatus = melCloudHandler.sendDeviceStatus(cmdtoSend);
203 updateChannels(newDeviceStatus);
204 } catch (MelCloudLoginException e) {
205 logger.warn("Command '{}' to channel '{}' failed due to login error, reason {}. ", command, channelUID,
207 } catch (MelCloudCommException e) {
208 logger.warn("Command '{}' to channel '{}' failed, reason {}. ", command, channelUID, e.getMessage(), e);
211 logger.debug("Nothing to send");
215 private DeviceStatus getDeviceStatusCopy(DeviceStatus deviceStatus) {
216 DeviceStatus copy = new DeviceStatus();
217 synchronized (this) {
218 copy.setPower(deviceStatus.getPower());
219 copy.setOperationMode(deviceStatus.getOperationMode());
220 copy.setSetTemperature(deviceStatus.getSetTemperature());
221 copy.setSetFanSpeed(deviceStatus.getSetFanSpeed());
222 copy.setVaneVertical(deviceStatus.getVaneVertical());
223 copy.setVaneHorizontal(deviceStatus.getVaneHorizontal());
224 copy.setEffectiveFlags(deviceStatus.getEffectiveFlags());
225 copy.setHasPendingCommand(deviceStatus.getHasPendingCommand());
226 copy.setDeviceID(deviceStatus.getDeviceID());
231 private void startAutomaticRefresh() {
232 if (refreshTask == null || refreshTask.isCancelled()) {
233 refreshTask = scheduler.scheduleWithFixedDelay(this::getDeviceDataAndUpdateChannels, 1,
234 config.pollingInterval, TimeUnit.SECONDS);
238 private void getDeviceDataAndUpdateChannels() {
239 if (melCloudHandler.isConnected()) {
240 logger.debug("Update device '{}' channels", getThing().getThingTypeUID());
242 DeviceStatus newDeviceStatus = melCloudHandler.fetchDeviceStatus(config.deviceID,
243 Optional.ofNullable(config.buildingID));
244 updateChannels(newDeviceStatus);
245 } catch (MelCloudLoginException e) {
246 logger.debug("Login error occurred during device '{}' polling, reason {}. ",
247 getThing().getThingTypeUID(), e.getMessage(), e);
248 } catch (MelCloudCommException e) {
249 logger.debug("Error occurred during device '{}' polling, reason {}. ", getThing().getThingTypeUID(),
253 logger.debug("Connection to MELCloud is not open, skipping periodic update");
257 private synchronized void updateChannels(DeviceStatus newDeviceStatus) {
258 deviceStatus = newDeviceStatus;
259 for (Channel channel : getThing().getChannels()) {
260 updateChannels(channel.getUID().getId(), deviceStatus);
264 private void updateChannels(String channelId, DeviceStatus deviceStatus) {
267 updateState(CHANNEL_POWER, OnOffType.from(deviceStatus.getPower()));
269 case CHANNEL_OPERATION_MODE:
270 updateState(CHANNEL_OPERATION_MODE, new StringType(deviceStatus.getOperationMode().toString()));
272 case CHANNEL_SET_TEMPERATURE:
273 updateState(CHANNEL_SET_TEMPERATURE,
274 new QuantityType<>(deviceStatus.getSetTemperature(), SIUnits.CELSIUS));
276 case CHANNEL_FAN_SPEED:
277 updateState(CHANNEL_FAN_SPEED, new StringType(deviceStatus.getSetFanSpeed().toString()));
279 case CHANNEL_VANE_HORIZONTAL:
280 updateState(CHANNEL_VANE_HORIZONTAL, new StringType(deviceStatus.getVaneHorizontal().toString()));
282 case CHANNEL_VANE_VERTICAL:
283 updateState(CHANNEL_VANE_VERTICAL, new StringType(deviceStatus.getVaneVertical().toString()));
285 case CHANNEL_OFFLINE:
286 updateState(CHANNEL_OFFLINE, OnOffType.from(deviceStatus.getOffline()));
288 case CHANNEL_HAS_PENDING_COMMAND:
289 updateState(CHANNEL_HAS_PENDING_COMMAND, OnOffType.from(deviceStatus.getHasPendingCommand()));
291 case CHANNEL_ROOM_TEMPERATURE:
292 updateState(CHANNEL_ROOM_TEMPERATURE, new DecimalType(deviceStatus.getRoomTemperature()));
294 case CHANNEL_LAST_COMMUNICATION:
295 updateState(CHANNEL_LAST_COMMUNICATION,
296 new DateTimeType(convertDateTime(deviceStatus.getLastCommunication())));
298 case CHANNEL_NEXT_COMMUNICATION:
299 updateState(CHANNEL_NEXT_COMMUNICATION,
300 new DateTimeType(convertDateTime(deviceStatus.getNextCommunication())));
305 private ZonedDateTime convertDateTime(String dateTime) {
306 return ZonedDateTime.ofInstant(LocalDateTime.parse(dateTime, DateTimeFormatter.ISO_LOCAL_DATE_TIME),
307 ZoneOffset.UTC, ZoneId.systemDefault());