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.regoheatpump.internal.handler;
15 import static org.openhab.binding.regoheatpump.internal.RegoHeatPumpBindingConstants.*;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.OutputStream;
20 import java.util.Collection;
21 import java.util.Date;
22 import java.util.HashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.function.Consumer;
27 import java.util.function.Function;
28 import java.util.stream.Collectors;
30 import javax.measure.Unit;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.regoheatpump.internal.protocol.RegoConnection;
35 import org.openhab.binding.regoheatpump.internal.rego6xx.CommandFactory;
36 import org.openhab.binding.regoheatpump.internal.rego6xx.ErrorLine;
37 import org.openhab.binding.regoheatpump.internal.rego6xx.Rego6xxProtocolException;
38 import org.openhab.binding.regoheatpump.internal.rego6xx.RegoRegisterMapper;
39 import org.openhab.binding.regoheatpump.internal.rego6xx.ResponseParser;
40 import org.openhab.binding.regoheatpump.internal.rego6xx.ResponseParserFactory;
41 import org.openhab.core.library.types.DateTimeType;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.QuantityType;
44 import org.openhab.core.library.types.StringType;
45 import org.openhab.core.thing.Channel;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.binding.BaseThingHandler;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.types.State;
54 import org.openhab.core.types.UnDefType;
55 import org.openhab.core.util.HexUtils;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
60 * The {@link Rego6xxHeatPumpHandler} is responsible for handling commands, which are
61 * sent to one of the channels.
63 * @author Boris Krivonog - Initial contribution
66 abstract class Rego6xxHeatPumpHandler extends BaseThingHandler {
68 private static final class ChannelDescriptor {
69 private @Nullable Date lastUpdate;
70 private byte @Nullable [] cachedValue;
72 public byte @Nullable [] cachedValueIfNotExpired(int refreshTime) {
73 Date lastUpdate = this.lastUpdate;
74 if (lastUpdate == null || (lastUpdate.getTime() + refreshTime * 900 < new Date().getTime())) {
81 public void setValue(byte[] value) {
82 lastUpdate = new Date();
87 private final Logger logger = LoggerFactory.getLogger(Rego6xxHeatPumpHandler.class);
88 private final Map<String, ChannelDescriptor> channelDescriptors = new HashMap<>();
89 private final RegoRegisterMapper mapper = RegoRegisterMapper.REGO600;
90 private @Nullable RegoConnection connection;
91 private @Nullable ScheduledFuture<?> scheduledRefreshFuture;
92 private int refreshInterval;
94 protected Rego6xxHeatPumpHandler(Thing thing) {
98 protected abstract RegoConnection createConnection() throws IOException;
101 public void initialize() {
102 refreshInterval = ((Number) getConfig().get(REFRESH_INTERVAL)).intValue();
105 connection = createConnection();
106 } catch (IOException e) {
107 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
111 scheduledRefreshFuture = scheduler.scheduleWithFixedDelay(this::refresh, 2, refreshInterval, TimeUnit.SECONDS);
113 updateStatus(ThingStatus.UNKNOWN);
117 public void dispose() {
120 RegoConnection connection = this.connection;
121 this.connection = null;
122 if (connection != null) {
126 ScheduledFuture<?> scheduledRefreshFuture = this.scheduledRefreshFuture;
127 this.scheduledRefreshFuture = null;
128 if (scheduledRefreshFuture != null) {
129 scheduledRefreshFuture.cancel(true);
132 synchronized (channelDescriptors) {
133 channelDescriptors.clear();
138 public void handleCommand(ChannelUID channelUID, Command command) {
139 if (command instanceof RefreshType) {
140 processChannelReadRequest(channelUID.getId());
142 RegoRegisterMapper.Channel channel = mapper.map(channelUID.getId());
143 if (channel != null) {
144 logger.debug("Executing command '{}' for channel '{}'", command, channelUID.getId());
145 processChannelWriteRequest(channel, command);
147 logger.debug("Unsupported channel {}", channelUID.getId());
152 private static double commandToValue(Command command) {
153 if (command instanceof QuantityType<?>) {
154 return ((QuantityType<?>) command).doubleValue();
157 if (command instanceof DecimalType) {
158 return ((DecimalType) command).doubleValue();
161 throw new NumberFormatException("Command '" + command + "' not supported");
164 private void processChannelWriteRequest(RegoRegisterMapper.Channel channel, Command command) {
165 short value = (short) Math.round(commandToValue(command) / channel.scaleFactor());
166 byte[] commandPayload = CommandFactory.createWriteToSystemRegisterCommand(channel.address(), value);
167 executeCommand(null, commandPayload, ResponseParserFactory.WRITE, result -> {
168 // Ignore result since it is a write command.
172 private void processChannelReadRequest(String channelIID) {
173 switch (channelIID) {
174 case CHANNEL_LAST_ERROR_TYPE:
175 readAndUpdateLastErrorType();
177 case CHANNEL_LAST_ERROR_TIMESTAMP:
178 readAndUpdateLastErrorTimestamp();
180 case CHANNEL_FRONT_PANEL_POWER_LAMP:
181 readAndUpdateFrontPanel(channelIID, (short) 0x0012);
183 case CHANNEL_FRONT_PANEL_PUMP_LAMP:
184 readAndUpdateFrontPanel(channelIID, (short) 0x0013);
186 case CHANNEL_FRONT_PANEL_ADDITIONAL_HEAT_LAMP:
187 readAndUpdateFrontPanel(channelIID, (short) 0x0014);
189 case CHANNEL_FRONT_PANEL_WATER_HEATER_LAMP:
190 readAndUpdateFrontPanel(channelIID, (short) 0x0015);
192 case CHANNEL_FRONT_PANEL_ALARM_LAMP:
193 readAndUpdateFrontPanel(channelIID, (short) 0x0016);
196 readAndUpdateSystemRegister(channelIID);
201 private Collection<String> linkedChannels() {
202 return thing.getChannels().stream().map(Channel::getUID).map(ChannelUID::getId).filter(this::isLinked)
203 .collect(Collectors.toList());
206 private void refresh() {
207 for (String channelIID : linkedChannels()) {
208 if (Thread.interrupted()) {
212 processChannelReadRequest(channelIID);
214 if (thing.getStatus() != ThingStatus.ONLINE) {
220 private void readAndUpdateLastErrorType() {
221 readAndUpdateLastError(CHANNEL_LAST_ERROR_TYPE, e -> new StringType(Byte.toString(e.error())));
224 private void readAndUpdateLastErrorTimestamp() {
225 readAndUpdateLastError(CHANNEL_LAST_ERROR_TIMESTAMP, e -> new DateTimeType(e.timestamp()));
228 private void readAndUpdateLastError(String channelIID, Function<ErrorLine, State> converter) {
229 executeCommandAndUpdateState(channelIID, CommandFactory.createReadLastErrorCommand(),
230 ResponseParserFactory.ERROR_LINE, e -> {
231 return e == ErrorLine.NO_ERROR ? UnDefType.NULL : converter.apply(e);
235 private void readAndUpdateFrontPanel(String channelIID, short address) {
236 byte[] command = CommandFactory.createReadFromFrontPanelCommand(address);
237 executeCommandAndUpdateState(channelIID, command, ResponseParserFactory.SHORT, DecimalType::new);
240 private void readAndUpdateSystemRegister(String channelIID) {
241 RegoRegisterMapper.Channel channel = mapper.map(channelIID);
242 if (channel != null) {
243 byte[] command = CommandFactory.createReadFromSystemRegisterCommand(channel.address());
244 executeCommandAndUpdateState(channelIID, command, ResponseParserFactory.SHORT, value -> {
245 Unit<?> unit = channel.unit();
246 double result = Math.round(channel.convertValue(value) * channel.scaleFactor() * 10.0) / 10.0;
247 return unit != null ? new QuantityType<>(result, unit) : new DecimalType(result);
250 logger.debug("Unsupported channel {}", channelIID);
254 private <T> void executeCommandAndUpdateState(String channelIID, byte[] command, ResponseParser<T> parser,
255 Function<T, State> converter) {
256 logger.debug("Reading value for channel '{}' ...", channelIID);
257 executeCommand(channelIID, command, parser, result -> {
258 logger.debug("Got value for '{}' = {}", channelIID, result);
259 updateState(channelIID, converter.apply(result));
263 private synchronized <T> void executeCommand(@Nullable String channelIID, byte[] command, ResponseParser<T> parser,
264 Consumer<T> resultProcessor) {
266 T result = executeCommandWithRetry(channelIID, command, parser, 5);
267 resultProcessor.accept(result);
268 } catch (IOException e) {
269 logger.warn("Executing command for channel '{}' failed.", channelIID, e);
271 synchronized (channelDescriptors) {
272 channelDescriptors.clear();
275 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
276 } catch (Rego6xxProtocolException | RuntimeException e) {
277 logger.warn("Executing command for channel '{}' failed.", channelIID, e);
278 if (channelIID != null) {
279 updateState(channelIID, UnDefType.UNDEF);
281 } catch (InterruptedException e) {
282 logger.debug("Execution interrupted when accessing value for channel '{}'.", channelIID, e);
283 Thread.currentThread().interrupt();
287 private <T> T executeCommandWithRetry(@Nullable String channelIID, byte[] command, ResponseParser<T> parser,
288 int retry) throws Rego6xxProtocolException, IOException, InterruptedException {
291 return executeCommand(channelIID, command, parser);
292 } catch (IOException | Rego6xxProtocolException e) {
294 logger.debug("Executing command for channel '{}' failed, retry {}.", channelIID, retry, e);
296 return executeCommandWithRetry(channelIID, command, parser, retry - 1);
303 private void checkRegoDevice() throws Rego6xxProtocolException, IOException, InterruptedException {
304 if (thing.getStatus() != ThingStatus.ONLINE) {
305 logger.debug("Reading Rego device version...");
306 Short regoVersion = executeCommand(null, CommandFactory.createReadRegoVersionCommand(),
307 ResponseParserFactory.SHORT);
309 if (regoVersion != 600) {
310 throw new IOException("Invalid rego version received " + regoVersion.toString());
313 updateStatus(ThingStatus.ONLINE);
314 logger.debug("Connected to Rego version {}.", regoVersion);
318 private ChannelDescriptor channelDescriptorForChannel(String channelIID) {
319 synchronized (channelDescriptors) {
320 ChannelDescriptor descriptor = channelDescriptors.get(channelIID);
321 if (descriptor == null) {
322 descriptor = new ChannelDescriptor();
323 channelDescriptors.put(channelIID, descriptor);
329 private <T> T executeCommand(@Nullable String channelIID, byte[] command, ResponseParser<T> parser)
330 throws Rego6xxProtocolException, IOException, InterruptedException {
332 return executeCommandInternal(channelIID, command, parser);
333 } catch (IOException e) {
334 RegoConnection connection = this.connection;
335 if (connection != null) {
343 private <T> T executeCommandInternal(@Nullable String channelIID, byte[] command, ResponseParser<T> parser)
344 throws Rego6xxProtocolException, IOException, InterruptedException {
345 // CHANNEL_LAST_ERROR_CODE and CHANNEL_LAST_ERROR_TIMESTAMP are read from same
346 // register. To prevent accessing same register twice when both channels are linked,
347 // use same name for both so only a single fetch will be triggered.
348 String mappedChannelIID = (CHANNEL_LAST_ERROR_TYPE.equals(channelIID)
349 || CHANNEL_LAST_ERROR_TIMESTAMP.equals(channelIID)) ? CHANNEL_LAST_ERROR : channelIID;
351 // Use transient channel descriptor for null (not cached) channels.
352 ChannelDescriptor descriptor;
353 if (mappedChannelIID == null) {
354 descriptor = new ChannelDescriptor();
356 descriptor = channelDescriptorForChannel(mappedChannelIID);
359 byte[] cachedValue = descriptor.cachedValueIfNotExpired(refreshInterval);
360 if (cachedValue != null) {
361 logger.debug("Cache did not yet expire, using cached value for {}", mappedChannelIID);
362 return parser.parse(cachedValue);
365 // Send command to device and wait for response.
366 RegoConnection connection = this.connection;
367 if (connection == null) {
368 throw new IOException("Unable to execute command - no connection available");
371 if (!connection.isConnected()) {
372 connection.connect();
375 // Give heat pump some time between commands. Feeding commands too quickly
376 // might cause heat pump not to respond.
379 // Protocol is request driven so there should be no data available before sending
380 // a command to the heat pump.
381 InputStream inputStream = connection.inputStream();
382 int available = inputStream.available();
384 // Limit to max 64 bytes, fuse.
385 byte[] buffer = new byte[Math.min(64, available)];
386 inputStream.read(buffer);
387 logger.debug("There are {} unexpected bytes available. Skipping {}.", available, buffer);
390 if (logger.isDebugEnabled()) {
391 logger.debug("Sending {}", HexUtils.bytesToHex(command));
395 OutputStream outputStream = connection.outputStream();
396 outputStream.write(command);
397 outputStream.flush();
399 // Read response, wait for max 2 second for data to arrive.
400 byte[] response = new byte[parser.responseLength()];
401 long timeout = System.currentTimeMillis() + 2000;
405 int len = inputStream.read(response, pos, response.length - pos);
409 // Give some time for response to arrive...
413 } while (pos < response.length && timeout > System.currentTimeMillis());
415 if (pos < response.length) {
416 logger.debug("Response not received, read {} bytes => {}", pos, response);
418 throw new IOException("Response not received - got " + Integer.toString(pos) + " bytes of "
419 + Integer.toString(response.length));
422 if (logger.isDebugEnabled()) {
423 logger.debug("Received {}", HexUtils.bytesToHex(response));
426 T result = parser.parse(response);
428 // If reading/parsing was done successfully, cache response payload.
429 descriptor.setValue(response);