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 State state = sendCommand(changeCommand, channelId);
82 updateState(channelUID, state);
84 Collection<ComfoAirCommand> affectedReadCommands = ComfoAirCommandType
85 .getAffectedReadCommands(channelId, keysToUpdate);
87 if (affectedReadCommands.size() > 0) {
88 Runnable updateThread = new AffectedItemsUpdateThread(affectedReadCommands);
89 affectedItemsPoller = scheduler.schedule(updateThread, 3, TimeUnit.SECONDS);
92 logger.warn("Unhandled command type: {}, channelId: {}", command.toString(), channelId);
98 public void initialize() {
99 String serialPort = this.config.serialPort;
101 if (serialPort.isEmpty()) {
102 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Serial port is not configured.");
105 ComfoAirSerialConnector comfoAirConnector = new ComfoAirSerialConnector(serialPortManager, serialPort,
107 this.comfoAirConnector = comfoAirConnector;
109 updateStatus(ThingStatus.UNKNOWN);
110 scheduler.submit(this::connect);
113 private void connect() {
114 if (comfoAirConnector != null) {
116 comfoAirConnector.open();
117 if (comfoAirConnector != null) {
118 updateStatus(ThingStatus.ONLINE);
119 pullDeviceProperties();
120 Map<String, String> properties = thing.getProperties();
122 List<Channel> toBeRemovedChannels = new ArrayList<>();
123 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_PREHEATER)
124 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
125 toBeRemovedChannels.addAll(getThing()
126 .getChannelsOfGroup(ComfoAirBindingConstants.CG_PREHEATER_PREFIX.replaceAll("#$", "")));
127 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
128 + ComfoAirBindingConstants.CHANNEL_FROST_STATE);
129 if (stateChannel != null) {
130 toBeRemovedChannels.add(stateChannel);
133 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_BYPASS)
134 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
135 toBeRemovedChannels.addAll(getThing()
136 .getChannelsOfGroup(ComfoAirBindingConstants.CG_BYPASS_PREFIX.replaceAll("#$", "")));
137 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
138 + ComfoAirBindingConstants.CHANNEL_BYPASS_STATE);
139 if (stateChannel != null) {
140 toBeRemovedChannels.add(stateChannel);
143 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_CHIMNEY)
144 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
145 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
146 + ComfoAirBindingConstants.CHANNEL_CHIMNEY_STATE);
147 if (stateChannel != null) {
148 toBeRemovedChannels.add(stateChannel);
151 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_COOKERHOOD)
152 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
153 toBeRemovedChannels.addAll(getThing().getChannelsOfGroup(
154 ComfoAirBindingConstants.CG_COOKERHOOD_PREFIX.replaceAll("#$", "")));
155 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
156 + ComfoAirBindingConstants.CHANNEL_COOKERHOOD_STATE);
157 if (stateChannel != null) {
158 toBeRemovedChannels.add(stateChannel);
161 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_HEATER)
162 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
163 toBeRemovedChannels.addAll(getThing()
164 .getChannelsOfGroup(ComfoAirBindingConstants.CG_HEATER_PREFIX.replaceAll("#$", "")));
165 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
166 + ComfoAirBindingConstants.CHANNEL_HEATER_STATE);
167 if (stateChannel != null) {
168 toBeRemovedChannels.add(stateChannel);
171 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_ENTHALPY)
172 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
173 toBeRemovedChannels.addAll(getThing()
174 .getChannelsOfGroup(ComfoAirBindingConstants.CG_ENTHALPY_PREFIX.replaceAll("#$", "")));
175 Channel stateChannel = getThing().getChannel(ComfoAirBindingConstants.CG_MENUP9_PREFIX
176 + ComfoAirBindingConstants.CHANNEL_ENTHALPY_STATE);
177 if (stateChannel != null) {
178 toBeRemovedChannels.add(stateChannel);
181 if (properties.get(ComfoAirBindingConstants.PROPERTY_OPTION_EWT)
182 .equals(ComfoAirBindingConstants.COMMON_OPTION_STATES[0])) {
183 toBeRemovedChannels.addAll(getThing()
184 .getChannelsOfGroup(ComfoAirBindingConstants.CG_EWT_PREFIX.replaceAll("#$", "")));
185 Channel stateChannel = getThing().getChannel(
186 ComfoAirBindingConstants.CG_MENUP9_PREFIX + ComfoAirBindingConstants.CHANNEL_EWT_STATE);
187 if (stateChannel != null) {
188 toBeRemovedChannels.add(stateChannel);
191 ThingBuilder builder = editThing().withoutChannels(toBeRemovedChannels);
192 updateThing(builder.build());
194 List<Channel> channels = this.thing.getChannels();
196 poller = scheduler.scheduleWithFixedDelay(() -> {
197 for (Channel channel : channels) {
198 updateChannelState(channel);
200 }, 0, (this.config.refreshInterval > 0) ? this.config.refreshInterval
201 : DEFAULT_REFRESH_INTERVAL_SEC, TimeUnit.SECONDS);
203 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
205 } catch (ComfoAirSerialException e) {
206 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
209 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
214 public void dispose() {
215 if (comfoAirConnector != null) {
216 comfoAirConnector.close();
219 final ScheduledFuture<?> localPoller = poller;
221 if (localPoller != null) {
222 localPoller.cancel(true);
226 final ScheduledFuture<?> localAffectedItemsPoller = affectedItemsPoller;
228 if (localAffectedItemsPoller != null) {
229 localAffectedItemsPoller.cancel(true);
230 affectedItemsPoller = null;
234 private void updateChannelState(Channel channel) {
235 if (!isLinked(channel.getUID())) {
238 String commandKey = channel.getUID().getId();
240 ComfoAirCommand readCommand = ComfoAirCommandType.getReadCommand(commandKey);
241 if (readCommand != null) {
242 scheduler.submit(() -> {
243 State state = sendCommand(readCommand, commandKey);
244 updateState(channel.getUID(), state);
249 private State sendCommand(ComfoAirCommand command, String commandKey) {
250 ComfoAirSerialConnector comfoAirConnector = this.comfoAirConnector;
252 if (comfoAirConnector != null) {
253 Integer requestCmd = command.getRequestCmd();
254 Integer replyCmd = command.getReplyCmd();
255 int[] requestData = command.getRequestData();
257 Integer preRequestCmd;
259 int[] preResponse = ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
261 if (requestCmd != null) {
262 switch (requestCmd) {
263 case ComfoAirCommandType.Constants.REQUEST_SET_ANALOGS:
264 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_ANALOGS;
265 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_ANALOGS;
267 case ComfoAirCommandType.Constants.REQUEST_SET_DELAYS:
268 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_DELAYS;
269 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_DELAYS;
271 case ComfoAirCommandType.Constants.REQUEST_SET_FAN_LEVEL:
272 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_FAN_LEVEL;
273 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_FAN_LEVEL;
275 case ComfoAirCommandType.Constants.REQUEST_SET_STATES:
276 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_STATES;
277 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_STATES;
279 case ComfoAirCommandType.Constants.REQUEST_SET_EWT:
280 preRequestCmd = ComfoAirCommandType.Constants.REQUEST_GET_EWT;
281 preReplyCmd = ComfoAirCommandType.Constants.REPLY_GET_EWT;
284 preRequestCmd = requestCmd;
285 preReplyCmd = replyCmd;
288 if (!preRequestCmd.equals(requestCmd)) {
289 command.setRequestCmd(preRequestCmd);
290 command.setReplyCmd(preReplyCmd);
291 command.setRequestData(ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
293 preResponse = comfoAirConnector.sendCommand(command, ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
295 if (preResponse.length <= 0) {
296 return UnDefType.NULL;
298 command.setRequestCmd(requestCmd);
299 command.setReplyCmd(replyCmd);
300 command.setRequestData(requestData);
304 int[] response = comfoAirConnector.sendCommand(command, preResponse);
306 if (response.length > 0) {
307 ComfoAirCommandType comfoAirCommandType = ComfoAirCommandType.getCommandTypeByKey(commandKey);
308 State value = UnDefType.UNDEF;
310 if (comfoAirCommandType != null) {
311 ComfoAirDataType dataType = comfoAirCommandType.getDataType();
312 value = dataType.convertToState(response, comfoAirCommandType);
314 if (value instanceof UnDefType) {
315 if (logger.isWarnEnabled()) {
316 logger.warn("unexpected value for DATA: {}", ComfoAirSerialConnector.dumpData(response));
323 return UnDefType.UNDEF;
326 public void pullDeviceProperties() {
327 Map<String, String> properties = editProperties();
328 ComfoAirSerialConnector comfoAirConnector = this.comfoAirConnector;
330 if (comfoAirConnector != null) {
331 String[] namedProperties = new String[] { ComfoAirBindingConstants.PROPERTY_SOFTWARE_MAIN_VERSION,
332 ComfoAirBindingConstants.PROPERTY_SOFTWARE_MINOR_VERSION,
333 ComfoAirBindingConstants.PROPERTY_DEVICE_NAME };
334 String[] optionProperties = new String[] { ComfoAirBindingConstants.PROPERTY_OPTION_PREHEATER,
335 ComfoAirBindingConstants.PROPERTY_OPTION_BYPASS, ComfoAirBindingConstants.PROPERTY_OPTION_RECU_TYPE,
336 ComfoAirBindingConstants.PROPERTY_OPTION_RECU_SIZE,
337 ComfoAirBindingConstants.PROPERTY_OPTION_CHIMNEY,
338 ComfoAirBindingConstants.PROPERTY_OPTION_COOKERHOOD,
339 ComfoAirBindingConstants.PROPERTY_OPTION_HEATER, ComfoAirBindingConstants.PROPERTY_OPTION_ENTHALPY,
340 ComfoAirBindingConstants.PROPERTY_OPTION_EWT };
342 for (String prop : namedProperties) {
343 ComfoAirCommand readCommand = ComfoAirCommandType.getReadCommand(prop);
344 if (readCommand != null) {
345 int[] response = comfoAirConnector.sendCommand(readCommand,
346 ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
347 if (response.length > 0) {
348 ComfoAirCommandType comfoAirCommandType = ComfoAirCommandType.getCommandTypeByKey(prop);
351 if (comfoAirCommandType != null) {
352 ComfoAirDataType dataType = comfoAirCommandType.getDataType();
353 if (prop.equals(ComfoAirBindingConstants.PROPERTY_DEVICE_NAME)) {
354 value = dataType.calculateStringValue(response, comfoAirCommandType);
356 value = String.valueOf(dataType.calculateNumberValue(response, comfoAirCommandType));
359 properties.put(prop, value);
364 ComfoAirCommand optionsReadCommand = new ComfoAirCommand(ComfoAirBindingConstants.PROPERTY_OPTION_PREHEATER,
365 ComfoAirCommandType.Constants.REQUEST_GET_STATES, ComfoAirCommandType.Constants.REPLY_GET_STATES,
366 ComfoAirCommandType.Constants.EMPTY_INT_ARRAY, null, null);
367 int[] response = comfoAirConnector.sendCommand(optionsReadCommand,
368 ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
369 if (response.length > 0) {
370 for (String prop : optionProperties) {
371 ComfoAirCommandType comfoAirCommandType = ComfoAirCommandType.getCommandTypeByKey(prop);
374 if (comfoAirCommandType != null) {
375 ComfoAirDataType dataType = comfoAirCommandType.getDataType();
376 int intValue = dataType.calculateNumberValue(response, comfoAirCommandType);
379 case ComfoAirBindingConstants.PROPERTY_OPTION_RECU_TYPE:
380 value = intValue == 1 ? "LEFT" : "RIGHT";
382 case ComfoAirBindingConstants.PROPERTY_OPTION_RECU_SIZE:
383 value = intValue == 1 ? "BIG" : "SMALL";
385 case ComfoAirBindingConstants.PROPERTY_OPTION_ENTHALPY:
387 value = ComfoAirBindingConstants.COMMON_OPTION_STATES[1];
388 } else if (intValue == 2) {
389 value = "Installed w\\o sensor";
391 value = ComfoAirBindingConstants.COMMON_OPTION_STATES[0];
394 case ComfoAirBindingConstants.PROPERTY_OPTION_EWT:
397 } else if (intValue == 2) {
398 value = "Unregulated";
400 value = ComfoAirBindingConstants.COMMON_OPTION_STATES[0];
404 value = intValue > 0 ? ComfoAirBindingConstants.COMMON_OPTION_STATES[1]
405 : ComfoAirBindingConstants.COMMON_OPTION_STATES[0];
409 properties.put(prop, value);
412 thing.setProperties(properties);
416 private class AffectedItemsUpdateThread implements Runnable {
418 private Collection<ComfoAirCommand> affectedReadCommands;
420 public AffectedItemsUpdateThread(Collection<ComfoAirCommand> affectedReadCommands) {
421 this.affectedReadCommands = affectedReadCommands;
426 for (ComfoAirCommand readCommand : this.affectedReadCommands) {
427 Integer replyCmd = readCommand.getReplyCmd();
428 if (replyCmd != null) {
429 List<ComfoAirCommandType> commandTypes = ComfoAirCommandType.getCommandTypesByReplyCmd(replyCmd);
431 for (ComfoAirCommandType commandType : commandTypes) {
432 String commandKey = commandType.getKey();
433 State state = sendCommand(readCommand, commandKey);
434 updateState(commandKey, state);