2 * Copyright (c) 2010-2020 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.ArrayList;
16 import java.util.Collection;
17 import java.util.List;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.stream.Collectors;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.comfoair.internal.datatypes.ComfoAirDataType;
27 import org.openhab.core.io.transport.serial.SerialPortManager;
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.thing.binding.builder.ThingBuilder;
35 import org.openhab.core.types.Command;
36 import org.openhab.core.types.RefreshType;
37 import org.openhab.core.types.State;
38 import org.openhab.core.types.UnDefType;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * The {@link ComfoAirHandler} is responsible for handling commands, which are
44 * sent to one of the channels.
46 * @author Hans Böhm - Initial contribution
49 public class ComfoAirHandler extends BaseThingHandler {
50 private static final int DEFAULT_REFRESH_INTERVAL_SEC = 60;
52 private final Logger logger = LoggerFactory.getLogger(ComfoAirHandler.class);
53 private final ComfoAirConfiguration config = getConfigAs(ComfoAirConfiguration.class);
54 private final SerialPortManager serialPortManager;
55 private @Nullable ScheduledFuture<?> poller;
56 private @Nullable ScheduledFuture<?> affectedItemsPoller;
57 private @Nullable ComfoAirSerialConnector comfoAirConnector;
59 public static final int BAUDRATE = 9600;
61 public ComfoAirHandler(Thing thing, final SerialPortManager serialPortManager) {
63 this.serialPortManager = serialPortManager;
67 public void handleCommand(ChannelUID channelUID, Command command) {
68 String channelId = channelUID.getId();
70 if (command instanceof RefreshType) {
71 Channel channel = this.thing.getChannel(channelUID);
72 if (channel != null) {
73 updateChannelState(channel);
76 ComfoAirCommand changeCommand = ComfoAirCommandType.getChangeCommand(channelId, command);
78 if (changeCommand != null) {
79 Set<String> keysToUpdate = getThing().getChannels().stream().map(Channel::getUID).filter(this::isLinked)
80 .map(ChannelUID::getId).collect(Collectors.toSet());
81 sendCommand(changeCommand, channelId);
83 Collection<ComfoAirCommand> affectedReadCommands = ComfoAirCommandType
84 .getAffectedReadCommands(channelId, keysToUpdate);
86 if (affectedReadCommands.size() > 0) {
87 Runnable updateThread = new AffectedItemsUpdateThread(affectedReadCommands);
88 affectedItemsPoller = scheduler.schedule(updateThread, 3, TimeUnit.SECONDS);
91 logger.warn("Unhandled command type: {}, channelId: {}", command.toString(), channelId);
97 public void initialize() {
98 String serialPort = this.config.serialPort;
100 if (serialPort.isEmpty()) {
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Serial port is not configured.");
104 ComfoAirSerialConnector comfoAirConnector = new ComfoAirSerialConnector(serialPortManager, serialPort,
106 this.comfoAirConnector = comfoAirConnector;
108 updateStatus(ThingStatus.UNKNOWN);
109 scheduler.submit(this::connect);
112 private void connect() {
113 if (comfoAirConnector != null) {
115 comfoAirConnector.open();
116 if (comfoAirConnector != null) {
117 updateStatus(ThingStatus.ONLINE);
118 pullDeviceProperties();
119 Map<String, String> properties = thing.getProperties();
121 List<Channel> toBeRemovedChannels = new ArrayList<>();
122 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_PREHEATER)
123 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
124 toBeRemovedChannels.addAll(getThing()
125 .getChannelsOfGroup(ComfoAirBindingConstants.CG_PREHEATER_PREFIX.replaceAll("#$", "")));
126 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
127 + ComfoAirBindingConstants.CHANNEL_FROST_STATE);
128 if (stateChannel != null) {
129 toBeRemovedChannels.add(stateChannel);
132 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_BYPASS)
133 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
134 toBeRemovedChannels.addAll(getThing()
135 .getChannelsOfGroup(ComfoAirBindingConstants.CG_BYPASS_PREFIX.replaceAll("#$", "")));
136 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
137 + ComfoAirBindingConstants.CHANNEL_BYPASS_STATE);
138 if (stateChannel != null) {
139 toBeRemovedChannels.add(stateChannel);
142 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_CHIMNEY)
143 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
144 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
145 + ComfoAirBindingConstants.CHANNEL_CHIMNEY_STATE);
146 if (stateChannel != null) {
147 toBeRemovedChannels.add(stateChannel);
150 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_COOKERHOOD)
151 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
152 toBeRemovedChannels.addAll(getThing().getChannelsOfGroup(
153 ComfoAirBindingConstants.CG_COOKERHOOD_PREFIX.replaceAll("#$", "")));
154 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
155 + ComfoAirBindingConstants.CHANNEL_COOKERHOOD_STATE);
156 if (stateChannel != null) {
157 toBeRemovedChannels.add(stateChannel);
160 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_HEATER)
161 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
162 toBeRemovedChannels.addAll(getThing()
163 .getChannelsOfGroup(ComfoAirBindingConstants.CG_HEATER_PREFIX.replaceAll("#$", "")));
164 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
165 + ComfoAirBindingConstants.CHANNEL_HEATER_STATE);
166 if (stateChannel != null) {
167 toBeRemovedChannels.add(stateChannel);
170 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_ENTHALPY)
171 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
172 toBeRemovedChannels.addAll(getThing()
173 .getChannelsOfGroup(ComfoAirBindingConstants.CG_ENTHALPY_PREFIX.replaceAll("#$", "")));
174 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
175 + ComfoAirBindingConstants.CHANNEL_ENTHALPY_STATE);
176 if (stateChannel != null) {
177 toBeRemovedChannels.add(stateChannel);
180 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_EWT)
181 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
182 toBeRemovedChannels.addAll(getThing()
183 .getChannelsOfGroup(ComfoAirBindingConstants.CG_EWT_PREFIX.replaceAll("#$", "")));
184 Channel stateChannel = getThing().getChannel(
185 ComfoAirBindingConstants.CG_MENUP9_PREFIX + ComfoAirBindingConstants.CHANNEL_EWT_STATE);
186 if (stateChannel != null) {
187 toBeRemovedChannels.add(stateChannel);
190 ThingBuilder builder = editThing().withoutChannels(toBeRemovedChannels);
191 updateThing(builder.build());
193 List<Channel> channels = this.thing.getChannels();
195 poller = scheduler.scheduleWithFixedDelay(() -> {
196 for (Channel channel : channels) {
197 updateChannelState(channel);
199 }, 0, (this.config.refreshInterval > 0) ? this.config.refreshInterval
200 : DEFAULT_REFRESH_INTERVAL_SEC, TimeUnit.SECONDS);
202 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
204 } catch (ComfoAirSerialException e) {
205 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
208 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
213 public void dispose() {
214 if (comfoAirConnector != null) {
215 comfoAirConnector.close();
218 final ScheduledFuture<?> localPoller = poller;
220 if (localPoller != null) {
221 localPoller.cancel(true);
225 final ScheduledFuture<?> localAffectedItemsPoller = affectedItemsPoller;
227 if (localAffectedItemsPoller != null) {
228 localAffectedItemsPoller.cancel(true);
229 affectedItemsPoller = null;
233 private void updateChannelState(Channel channel) {
234 if (!isLinked(channel.getUID())) {
237 String commandKey = channel.getUID().getId();
239 ComfoAirCommand readCommand = ComfoAirCommandType.getReadCommand(commandKey);
240 if (readCommand != null) {
241 scheduler.submit(() -> {
242 State state = sendCommand(readCommand, commandKey);
243 updateState(channel.getUID(), state);
248 private State sendCommand(ComfoAirCommand command, String commandKey) {
249 ComfoAirSerialConnector comfoAirConnector = this.comfoAirConnector;
251 if (comfoAirConnector != null) {
252 Integer requestCmd = command.getRequestCmd();
253 Integer replyCmd = command.getReplyCmd();
254 int[] requestData = command.getRequestData();
256 Integer preRequestCmd;
258 int[] preResponse = ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
260 if (requestCmd != null) {
261 switch (requestCmd) {
262 case ComfoAirCommandType.Constants.REQUEST_SET_ANALOGS:
263 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_ANALOGS;
264 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_ANALOGS;
266 case ComfoAirCommandType.Constants.REQUEST_SET_DELAYS:
267 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_DELAYS;
268 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_DELAYS;
270 case ComfoAirCommandType.Constants.REQUEST_SET_FAN_LEVEL:
271 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_FAN_LEVEL;
272 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_FAN_LEVEL;
274 case ComfoAirCommandType.Constants.REQUEST_SET_STATES:
275 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_STATES;
276 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_STATES;
278 case ComfoAirCommandType.Constants.REQUEST_SET_EWT:
279 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_EWT;
280 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_EWT;
283 preRequestCmd = requestCmd;
284 preReplyCmd = replyCmd;
287 if (!preRequestCmd.equals(requestCmd)) {
288 command.setRequestCmd(preRequestCmd);
289 command.setReplyCmd(preReplyCmd);
290 command.setRequestData(ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
292 preResponse = comfoAirConnector.sendCommand(command, ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
294 if (preResponse.length <= 0) {
295 return UnDefType.NULL;
297 command.setRequestCmd(requestCmd);
298 command.setReplyCmd(replyCmd);
299 command.setRequestData(requestData);
303 int[] response = comfoAirConnector.sendCommand(command, preResponse);
305 if (response.length > 0) {
306 ComfoAirCommandType comfoAirCommandType = ComfoAirCommandType.getCommandTypeByKey(commandKey);
307 State value = UnDefType.UNDEF;
309 if (comfoAirCommandType != null) {
310 ComfoAirDataType dataType = comfoAirCommandType.getDataType();
311 value = dataType.convertToState(response, comfoAirCommandType);
313 if (value instanceof UnDefType) {
314 if (logger.isWarnEnabled()) {
315 logger.warn("unexpected value for DATA: {}", ComfoAirSerialConnector.dumpData(response));
322 return UnDefType.UNDEF;
325 public void pullDeviceProperties() {
326 Map<String, String> properties = editProperties();
327 ComfoAirSerialConnector comfoAirConnector = this.comfoAirConnector;
329 if (comfoAirConnector != null) {
330 String[] namedProperties = new String[] { ComfoAirBindingConstants.PROPERTY_SOFTWARE_MAIN_VERSION,
331 ComfoAirBindingConstants.PROPERTY_SOFTWARE_MINOR_VERSION,
332 ComfoAirBindingConstants.PROPERTY_DEVICE_NAME };
333 String[] optionProperties = new String[] { ComfoAirBindingConstants.PROPERTY_OPTION_PREHEATER,
334 ComfoAirBindingConstants.PROPERTY_OPTION_BYPASS, ComfoAirBindingConstants.PROPERTY_OPTION_RECU_TYPE,
335 ComfoAirBindingConstants.PROPERTY_OPTION_RECU_SIZE,
336 ComfoAirBindingConstants.PROPERTY_OPTION_CHIMNEY,
337 ComfoAirBindingConstants.PROPERTY_OPTION_COOKERHOOD,
338 ComfoAirBindingConstants.PROPERTY_OPTION_HEATER, ComfoAirBindingConstants.PROPERTY_OPTION_ENTHALPY,
339 ComfoAirBindingConstants.PROPERTY_OPTION_EWT };
341 for (String prop : namedProperties) {
342 ComfoAirCommand readCommand = ComfoAirCommandType.getReadCommand(prop);
343 if (readCommand != null) {
344 int[] response = comfoAirConnector.sendCommand(readCommand,
345 ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
346 if (response.length > 0) {
347 ComfoAirCommandType comfoAirCommandType = ComfoAirCommandType.getCommandTypeByKey(prop);
350 if (comfoAirCommandType != null) {
351 ComfoAirDataType dataType = comfoAirCommandType.getDataType();
352 if (prop.equals(ComfoAirBindingConstants.PROPERTY_DEVICE_NAME)) {
353 value = dataType.calculateStringValue(response, comfoAirCommandType);
355 value = String.valueOf(dataType.calculateNumberValue(response, comfoAirCommandType));
358 properties.put(prop, value);
363 ComfoAirCommand optionsReadCommand = new ComfoAirCommand(ComfoAirBindingConstants.PROPERTY_OPTION_PREHEATER,
364 ComfoAirCommandType.Constants.REQUEST_GET_STATES, ComfoAirCommandType.Constants.REPLY_GET_STATES,
365 ComfoAirCommandType.Constants.EMPTY_INT_ARRAY, null, null);
366 int[] response = comfoAirConnector.sendCommand(optionsReadCommand,
367 ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
368 if (response.length > 0) {
369 for (String prop : optionProperties) {
370 ComfoAirCommandType comfoAirCommandType = ComfoAirCommandType.getCommandTypeByKey(prop);
373 if (comfoAirCommandType != null) {
374 ComfoAirDataType dataType = comfoAirCommandType.getDataType();
375 int intValue = dataType.calculateNumberValue(response, comfoAirCommandType);
378 case ComfoAirBindingConstants.PROPERTY_OPTION_RECU_TYPE:
379 value = intValue == 1 ? "LEFT" : "RIGHT";
381 case ComfoAirBindingConstants.PROPERTY_OPTION_RECU_SIZE:
382 value = intValue == 1 ? "BIG" : "SMALL";
384 case ComfoAirBindingConstants.PROPERTY_OPTION_ENTHALPY:
386 value = ComfoAirBindingConstants.COMMON_OPTION_STATES[1];
387 } else if (intValue == 2) {
388 value = "Installed w\\o sensor";
390 value = ComfoAirBindingConstants.COMMON_OPTION_STATES[0];
393 case ComfoAirBindingConstants.PROPERTY_OPTION_EWT:
396 } else if (intValue == 2) {
397 value = "Unregulated";
399 value = ComfoAirBindingConstants.COMMON_OPTION_STATES[0];
403 value = intValue > 0 ? ComfoAirBindingConstants.COMMON_OPTION_STATES[1]
404 : ComfoAirBindingConstants.COMMON_OPTION_STATES[0];
408 properties.put(prop, value);
411 thing.setProperties(properties);
415 private class AffectedItemsUpdateThread implements Runnable {
417 private Collection<ComfoAirCommand> affectedReadCommands;
419 public AffectedItemsUpdateThread(Collection<ComfoAirCommand> affectedReadCommands) {
420 this.affectedReadCommands = affectedReadCommands;
425 for (ComfoAirCommand readCommand : this.affectedReadCommands) {
426 Integer replyCmd = readCommand.getReplyCmd();
427 if (replyCmd != null) {
428 List<ComfoAirCommandType> commandTypes = ComfoAirCommandType.getCommandTypesByReplyCmd(replyCmd);
430 for (ComfoAirCommandType commandType : commandTypes) {
431 String commandKey = commandType.getKey();
432 State state = sendCommand(readCommand, commandKey);
433 updateState(commandKey, state);