2 * Copyright (c) 2010-2023 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.broadlinkthermostat.internal.handler;
15 import static org.openhab.binding.broadlinkthermostat.internal.BroadlinkBindingConstants.*;
17 import java.io.IOException;
18 import java.time.LocalTime;
19 import java.time.ZonedDateTime;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.core.cache.ExpiringCache;
26 import org.openhab.core.library.types.DateTimeType;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.QuantityType;
29 import org.openhab.core.library.types.StringType;
30 import org.openhab.core.library.unit.SIUnits;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.types.Command;
36 import org.openhab.core.types.RefreshType;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.github.mob41.blapi.FloureonDevice;
41 import com.github.mob41.blapi.dev.hysen.AdvancedStatusInfo;
42 import com.github.mob41.blapi.dev.hysen.BaseStatusInfo;
43 import com.github.mob41.blapi.dev.hysen.SensorControl;
44 import com.github.mob41.blapi.mac.Mac;
45 import com.github.mob41.blapi.pkt.cmd.hysen.SetTimeCommand;
48 * The {@link FloureonThermostatHandler} is responsible for handling thermostats labeled as Floureon Thermostat.
50 * @author Florian Mueller - Initial contribution
53 public class FloureonThermostatHandler extends BroadlinkBaseHandler {
55 private final Logger logger = LoggerFactory.getLogger(FloureonThermostatHandler.class);
56 private @Nullable FloureonDevice floureonDevice;
58 private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toSeconds(3);
59 private final ExpiringCache<AdvancedStatusInfo> advancedStatusInfoExpiringCache = new ExpiringCache<>(CACHE_EXPIRY,
60 this::refreshAdvancedStatus);
62 private @Nullable ScheduledFuture<?> scanJob;
65 * Creates a new instance of this class for the {@link FloureonThermostatHandler}.
67 * @param thing the thing that should be handled, not null
69 public FloureonThermostatHandler(Thing thing) {
74 * Initializes a new instance of a {@link FloureonThermostatHandler}.
77 public void initialize() {
79 // schedule a new scan every minute
80 scanJob = scheduler.scheduleWithFixedDelay(this::refreshData, 0, 1, TimeUnit.MINUTES);
81 if (!host.isBlank() && !macAddress.isBlank()) {
83 blDevice = new FloureonDevice(host, new Mac(macAddress));
84 this.floureonDevice = (FloureonDevice) blDevice;
85 updateStatus(ThingStatus.ONLINE);
86 } catch (IOException e) {
87 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
88 "Could not find broadlink device at host " + host + " with MAC " + macAddress + ": "
92 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Missing device configuration");
97 public void handleCommand(ChannelUID channelUID, Command command) {
98 logger.debug("Command: {}", command.toFullString());
101 if (command == RefreshType.REFRESH) {
106 switch (channelUID.getIdWithoutGroup()) {
108 handleSetpointCommand(channelUID, command);
111 handlePowerCommand(channelUID, command);
114 handleModeCommand(channelUID, command);
117 handleSensorCommand(channelUID, command);
120 handleRemoteLockCommand(channelUID, command);
123 handleSetTimeCommand(channelUID, command);
126 logger.warn("Channel {} does not support command {}", channelUID, command);
130 private void handlePowerCommand(ChannelUID channelUID, Command command) {
131 FloureonDevice floureonDevice = this.floureonDevice;
132 if (command instanceof OnOffType && floureonDevice != null) {
134 floureonDevice.setPower(command == OnOffType.ON);
135 } catch (Exception e) {
136 logger.warn("Error while setting power of {} to {}: {}", thing.getUID(), command, e.getMessage());
139 logger.warn("Channel {} does not support command {}", channelUID, command);
143 private void handleModeCommand(ChannelUID channelUID, Command command) {
144 FloureonDevice floureonDevice = this.floureonDevice;
145 if (command instanceof StringType && floureonDevice != null) {
147 if (MODE_AUTO.equals(command.toFullString())) {
148 floureonDevice.switchToAuto();
150 floureonDevice.switchToManual();
152 } catch (Exception e) {
153 logger.warn("Error while setting power off {} to {}: {}", thing.getUID(), command, e.getMessage());
156 logger.warn("Channel {} does not support command {}", channelUID, command);
160 private void handleSetpointCommand(ChannelUID channelUID, Command command) {
161 FloureonDevice floureonDevice = this.floureonDevice;
162 if (command instanceof QuantityType && floureonDevice != null) {
164 QuantityType<?> temperatureQuantityType = ((QuantityType<?>) command).toUnit(SIUnits.CELSIUS);
165 if (temperatureQuantityType != null) {
166 floureonDevice.setThermostatTemp(temperatureQuantityType.doubleValue());
168 logger.warn("Could not convert {} to °C", command);
170 } catch (Exception e) {
171 logger.warn("Error while setting setpoint of {} to {}: {}", thing.getUID(), command, e.getMessage());
175 logger.warn("Channel {} does not support command {}", channelUID, command);
179 private void handleSensorCommand(ChannelUID channelUID, Command command) {
180 FloureonDevice floureonDevice = this.floureonDevice;
181 if (command instanceof StringType && floureonDevice != null) {
183 BaseStatusInfo statusInfo = floureonDevice.getBasicStatus();
184 if (SENSOR_INTERNAL.equals(command.toFullString())) {
185 floureonDevice.setMode(statusInfo.getAutoMode(), statusInfo.getLoopMode(), SensorControl.INTERNAL);
186 } else if (SENSOR_EXTERNAL.equals(command.toFullString())) {
187 floureonDevice.setMode(statusInfo.getAutoMode(), statusInfo.getLoopMode(), SensorControl.EXTERNAL);
189 floureonDevice.setMode(statusInfo.getAutoMode(), statusInfo.getLoopMode(),
190 SensorControl.INTERNAL_TEMP_EXTERNAL_LIMIT);
192 } catch (Exception e) {
193 logger.warn("Error while trying to set sensor mode {}: {}", command, e.getMessage());
196 logger.warn("Channel {} does not support command {}", channelUID, command);
200 private void handleRemoteLockCommand(ChannelUID channelUID, Command command) {
201 FloureonDevice floureonDevice = this.floureonDevice;
202 if (command instanceof OnOffType && floureonDevice != null) {
204 floureonDevice.setLock(command == OnOffType.ON);
205 } catch (Exception e) {
206 logger.warn("Error while setting remote lock of {} to {}: {}", thing.getUID(), command, e.getMessage());
209 logger.warn("Channel {} does not support command {}", channelUID, command);
213 private void handleSetTimeCommand(ChannelUID channelUID, Command command) {
214 if (command instanceof DateTimeType) {
215 ZonedDateTime zonedDateTime = ((DateTimeType) command).getZonedDateTime();
217 new SetTimeCommand(tob(zonedDateTime.getHour()), tob(zonedDateTime.getMinute()),
218 tob(zonedDateTime.getSecond()), tob(zonedDateTime.getDayOfWeek().getValue()))
219 .execute(floureonDevice);
220 } catch (Exception e) {
221 logger.warn("Error while setting time of {} to {}: {}", thing.getUID(), command, e.getMessage());
224 logger.warn("Channel {} does not support command {}", channelUID, command);
229 private AdvancedStatusInfo refreshAdvancedStatus() {
230 if (ThingStatus.ONLINE != thing.getStatus()) {
234 FloureonDevice floureonDevice = this.floureonDevice;
235 if (floureonDevice != null) {
237 AdvancedStatusInfo advancedStatusInfo = floureonDevice.getAdvancedStatus();
238 if (advancedStatusInfo == null) {
239 logger.warn("Device {} did not return any data. Trying to reauthenticate...", thing.getUID());
241 advancedStatusInfo = floureonDevice.getAdvancedStatus();
243 if (advancedStatusInfo == null) {
244 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device not responding.");
247 return advancedStatusInfo;
248 } catch (Exception e) {
249 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
250 "Error while retrieving data for " + thing.getUID() + ": " + e.getMessage());
257 public void dispose() {
258 ScheduledFuture<?> currentScanJob = scanJob;
259 if (currentScanJob != null) {
260 currentScanJob.cancel(true);
264 protected void refreshData() {
266 AdvancedStatusInfo advancedStatusInfo = advancedStatusInfoExpiringCache.getValue();
267 if (advancedStatusInfo == null) {
270 logger.trace("Retrieved data from device {}: {}", thing.getUID(), advancedStatusInfo);
271 updateState(ROOM_TEMPERATURE, new QuantityType<>(advancedStatusInfo.getRoomTemp(), SIUnits.CELSIUS));
272 updateState(ROOM_TEMPERATURE_EXTERNAL_SENSOR,
273 new QuantityType<>(advancedStatusInfo.getExternalTemp(), SIUnits.CELSIUS));
274 updateState(SETPOINT, new QuantityType<>(advancedStatusInfo.getThermostatTemp(), SIUnits.CELSIUS));
275 updateState(POWER, OnOffType.from(advancedStatusInfo.getPower()));
276 updateState(MODE, StringType.valueOf(advancedStatusInfo.getAutoMode() ? "auto" : "manual"));
277 updateState(SENSOR, StringType.valueOf(advancedStatusInfo.getSensorControl().name()));
278 updateState(TEMPERATURE_OFFSET, new QuantityType<>(advancedStatusInfo.getDif(), SIUnits.CELSIUS));
279 updateState(ACTIVE, OnOffType.from(advancedStatusInfo.getActive()));
280 updateState(REMOTE_LOCK, OnOffType.from(advancedStatusInfo.getRemoteLock()));
281 updateState(TIME, new DateTimeType(getTimestamp(advancedStatusInfo)));
284 private ZonedDateTime getTimestamp(AdvancedStatusInfo advancedStatusInfo) {
285 ZonedDateTime now = ZonedDateTime.now();
287 LocalTime.of(advancedStatusInfo.getHour(), advancedStatusInfo.getMin(), advancedStatusInfo.getSec()));
290 private static byte tob(int in) {
291 return (byte) (in & 0xff);