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.comfoair.internal;
15 import java.util.Collection;
16 import java.util.List;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21 import java.util.stream.Collectors;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.comfoair.internal.datatypes.ComfoAirDataType;
26 import org.openhab.core.io.transport.serial.SerialPortManager;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.thing.Channel;
29 import org.openhab.core.thing.ChannelUID;
30 import org.openhab.core.thing.Thing;
31 import org.openhab.core.thing.ThingStatus;
32 import org.openhab.core.thing.ThingStatusDetail;
33 import org.openhab.core.thing.binding.BaseThingHandler;
34 import org.openhab.core.types.Command;
35 import org.openhab.core.types.RefreshType;
36 import org.openhab.core.types.State;
37 import org.openhab.core.types.UnDefType;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
42 * The {@link ComfoAirHandler} is responsible for handling commands, which are
43 * sent to one of the channels.
45 * @author Hans Böhm - Initial contribution
48 public class ComfoAirHandler extends BaseThingHandler {
49 private static final int DEFAULT_REFRESH_INTERVAL_SEC = 60;
51 private final Logger logger = LoggerFactory.getLogger(ComfoAirHandler.class);
52 private final ComfoAirConfiguration config = getConfigAs(ComfoAirConfiguration.class);
53 private final SerialPortManager serialPortManager;
54 private @Nullable ScheduledFuture<?> poller;
55 private @Nullable ScheduledFuture<?> affectedItemsPoller;
56 private @Nullable ComfoAirSerialConnector comfoAirConnector;
58 public static final int BAUDRATE = 9600;
59 public static final String ACTIVATE_CHANNEL_ID = ComfoAirBindingConstants.CG_CONTROL_PREFIX
60 + ComfoAirBindingConstants.CHANNEL_ACTIVATE;
62 public ComfoAirHandler(Thing thing, final SerialPortManager serialPortManager) {
64 this.serialPortManager = serialPortManager;
68 public void handleCommand(ChannelUID channelUID, Command command) {
69 String channelId = channelUID.getId();
70 if (comfoAirConnector != null) {
71 boolean isActive = !comfoAirConnector.getIsSuspended();
73 if (isActive || channelId.equals(ACTIVATE_CHANNEL_ID)) {
74 if (command instanceof RefreshType) {
75 Channel channel = this.thing.getChannel(channelUID);
76 if (channel != null) {
77 updateChannelState(channel);
80 ComfoAirCommand changeCommand = ComfoAirCommandType.getChangeCommand(channelId, command);
82 if (changeCommand != null) {
83 Set<String> keysToUpdate = getThing().getChannels().stream().map(Channel::getUID)
84 .filter(this::isLinked).map(ChannelUID::getId).collect(Collectors.toSet());
85 sendCommand(changeCommand, channelId);
87 Collection<ComfoAirCommand> affectedReadCommands = ComfoAirCommandType
88 .getAffectedReadCommands(channelId, keysToUpdate);
90 if (!affectedReadCommands.isEmpty()) {
91 Runnable updateThread = new AffectedItemsUpdateThread(affectedReadCommands, keysToUpdate);
92 affectedItemsPoller = scheduler.schedule(updateThread, 3, TimeUnit.SECONDS);
95 logger.warn("Unhandled command type: {}, channelId: {}", command.toString(), channelId);
99 logger.debug("Binding control is currently not active.");
105 public void initialize() {
106 String serialPort = this.config.serialPort;
108 if (serialPort.isEmpty()) {
109 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Serial port is not configured.");
112 ComfoAirSerialConnector comfoAirConnector = new ComfoAirSerialConnector(serialPortManager, serialPort,
114 this.comfoAirConnector = comfoAirConnector;
116 updateStatus(ThingStatus.UNKNOWN);
117 scheduler.submit(this::connect);
120 private void connect() {
121 if (comfoAirConnector != null) {
123 comfoAirConnector.open();
124 if (comfoAirConnector != null) {
125 updateStatus(ThingStatus.ONLINE);
126 pullDeviceProperties();
128 updateState(ACTIVATE_CHANNEL_ID, OnOffType.ON);
130 List<Channel> channels = this.thing.getChannels();
132 poller = scheduler.scheduleWithFixedDelay(() -> {
133 for (Channel channel : channels) {
134 updateChannelState(channel);
136 }, 0, (this.config.refreshInterval > 0) ? this.config.refreshInterval
137 : DEFAULT_REFRESH_INTERVAL_SEC, TimeUnit.SECONDS);
139 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
141 } catch (ComfoAirSerialException e) {
142 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
145 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
150 public void dispose() {
151 if (comfoAirConnector != null) {
152 comfoAirConnector.close();
155 final ScheduledFuture<?> localPoller = poller;
157 if (localPoller != null) {
158 localPoller.cancel(true);
162 final ScheduledFuture<?> localAffectedItemsPoller = affectedItemsPoller;
164 if (localAffectedItemsPoller != null) {
165 localAffectedItemsPoller.cancel(true);
166 affectedItemsPoller = null;
170 private void updateChannelState(Channel channel) {
171 if (!isLinked(channel.getUID())) {
175 if (comfoAirConnector != null) {
176 boolean isActive = !comfoAirConnector.getIsSuspended();
178 String commandKey = channel.getUID().getId();
179 if (commandKey.equals(ACTIVATE_CHANNEL_ID)) {
180 State state = OnOffType.from(isActive);
181 updateState(channel.getUID(), state);
186 logger.debug("Binding control is currently not active.");
190 ComfoAirCommand readCommand = ComfoAirCommandType.getReadCommand(commandKey);
191 if (readCommand != null && readCommand.getRequestCmd() != null) {
192 scheduler.submit(() -> {
193 State state = sendCommand(readCommand, commandKey);
194 updateState(channel.getUID(), state);
201 public void channelLinked(ChannelUID channelUID) {
202 super.channelLinked(channelUID);
203 Channel channel = this.thing.getChannel(channelUID);
204 if (channel != null) {
205 updateChannelState(channel);
209 private State sendCommand(ComfoAirCommand command, String commandKey) {
210 ComfoAirSerialConnector comfoAirConnector = this.comfoAirConnector;
212 if (comfoAirConnector != null) {
213 Integer requestCmd = command.getRequestCmd();
214 Integer replyCmd = command.getReplyCmd();
215 int[] requestData = command.getRequestData();
217 Integer preRequestCmd;
219 int[] preResponse = ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
221 if (requestCmd != null) {
222 switch (requestCmd) {
223 case ComfoAirCommandType.Constants.REQUEST_SET_ANALOGS:
224 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_ANALOGS;
225 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_ANALOGS;
227 case ComfoAirCommandType.Constants.REQUEST_SET_DELAYS:
228 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_DELAYS;
229 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_DELAYS;
231 case ComfoAirCommandType.Constants.REQUEST_SET_FAN_LEVEL:
232 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_FAN_LEVEL;
233 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_FAN_LEVEL;
235 case ComfoAirCommandType.Constants.REQUEST_SET_STATES:
236 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_STATES;
237 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_STATES;
239 case ComfoAirCommandType.Constants.REQUEST_SET_GHX:
240 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_GHX;
241 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_GHX;
244 preRequestCmd = requestCmd;
245 preReplyCmd = replyCmd;
248 if (!preRequestCmd.equals(requestCmd)) {
249 command.setRequestCmd(preRequestCmd);
250 command.setReplyCmd(preReplyCmd);
251 command.setRequestData(ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
253 preResponse = comfoAirConnector.sendCommand(command, ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
255 if (preResponse.length <= 0) {
256 return UnDefType.NULL;
258 command.setRequestCmd(requestCmd);
259 command.setReplyCmd(replyCmd);
260 command.setRequestData(requestData);
264 int[] response = comfoAirConnector.sendCommand(command, preResponse);
266 if (response.length > 0) {
267 ComfoAirCommandType comfoAirCommandType = ComfoAirCommandType.getCommandTypeByKey(commandKey);
268 State value = UnDefType.UNDEF;
270 if (comfoAirCommandType != null) {
271 ComfoAirDataType dataType = comfoAirCommandType.getDataType();
272 value = dataType.convertToState(response, comfoAirCommandType);
274 if (value instanceof UnDefType) {
275 if (logger.isWarnEnabled()) {
276 logger.warn("unexpected value for key '{}'. DATA: {}", commandKey,
277 ComfoAirSerialConnector.dumpData(response));
284 return UnDefType.UNDEF;
287 public void pullDeviceProperties() {
288 Map<String, String> properties = editProperties();
289 ComfoAirSerialConnector comfoAirConnector = this.comfoAirConnector;
291 if (comfoAirConnector != null) {
292 String[] namedProperties = new String[] { ComfoAirBindingConstants.PROPERTY_SOFTWARE_MAIN_VERSION,
293 ComfoAirBindingConstants.PROPERTY_SOFTWARE_MINOR_VERSION,
294 ComfoAirBindingConstants.PROPERTY_DEVICE_NAME };
296 for (String prop : namedProperties) {
297 ComfoAirCommand readCommand = ComfoAirCommandType.getReadCommand(prop);
298 if (readCommand != null) {
299 int[] response = comfoAirConnector.sendCommand(readCommand,
300 ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
301 if (response.length > 0) {
302 ComfoAirCommandType comfoAirCommandType = ComfoAirCommandType.getCommandTypeByKey(prop);
305 if (comfoAirCommandType != null) {
306 ComfoAirDataType dataType = comfoAirCommandType.getDataType();
307 if (prop.equals(ComfoAirBindingConstants.PROPERTY_DEVICE_NAME)) {
308 value = dataType.calculateStringValue(response, comfoAirCommandType);
310 value = String.valueOf(dataType.calculateNumberValue(response, comfoAirCommandType));
313 properties.put(prop, value);
317 thing.setProperties(properties);
321 private class AffectedItemsUpdateThread implements Runnable {
323 private Collection<ComfoAirCommand> affectedReadCommands;
324 private Set<String> linkedChannels;
326 public AffectedItemsUpdateThread(Collection<ComfoAirCommand> affectedReadCommands, Set<String> linkedChannels) {
327 this.affectedReadCommands = affectedReadCommands;
328 this.linkedChannels = linkedChannels;
333 for (ComfoAirCommand readCommand : this.affectedReadCommands) {
334 Integer replyCmd = readCommand.getReplyCmd();
335 if (replyCmd != null) {
336 List<ComfoAirCommandType> commandTypes = ComfoAirCommandType.getCommandTypesByReplyCmd(replyCmd);
338 for (ComfoAirCommandType commandType : commandTypes) {
339 String commandKey = commandType.getKey();
340 if (linkedChannels.contains(commandKey)) {
341 State state = sendCommand(readCommand, commandKey);
342 updateState(commandKey, state);