]> git.basschouten.com Git - openhab-addons.git/blob
f5b67b65d85865c5cb5300bf26fbca61020b1801
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.regoheatpump.internal.handler;
14
15 import static org.openhab.binding.regoheatpump.internal.RegoHeatPumpBindingConstants.*;
16
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;
23 import java.util.Map;
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;
29
30 import javax.measure.Unit;
31
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;
58
59 /**
60  * The {@link Rego6xxHeatPumpHandler} is responsible for handling commands, which are
61  * sent to one of the channels.
62  *
63  * @author Boris Krivonog - Initial contribution
64  */
65 @NonNullByDefault
66 abstract class Rego6xxHeatPumpHandler extends BaseThingHandler {
67
68     private static final class ChannelDescriptor {
69         private @Nullable Date lastUpdate;
70         private byte @Nullable [] cachedValue;
71
72         public byte @Nullable [] cachedValueIfNotExpired(int refreshTime) {
73             Date lastUpdate = this.lastUpdate;
74             if (lastUpdate == null || (lastUpdate.getTime() + refreshTime * 900 < new Date().getTime())) {
75                 return null;
76             }
77
78             return cachedValue;
79         }
80
81         public void setValue(byte[] value) {
82             lastUpdate = new Date();
83             cachedValue = value;
84         }
85     }
86
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;
93
94     protected Rego6xxHeatPumpHandler(Thing thing) {
95         super(thing);
96     }
97
98     protected abstract RegoConnection createConnection() throws IOException;
99
100     @Override
101     public void initialize() {
102         refreshInterval = ((Number) getConfig().get(REFRESH_INTERVAL)).intValue();
103
104         try {
105             connection = createConnection();
106         } catch (IOException e) {
107             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
108             return;
109         }
110
111         scheduledRefreshFuture = scheduler.scheduleWithFixedDelay(this::refresh, 2, refreshInterval, TimeUnit.SECONDS);
112
113         updateStatus(ThingStatus.UNKNOWN);
114     }
115
116     @Override
117     public void dispose() {
118         super.dispose();
119
120         RegoConnection connection = this.connection;
121         this.connection = null;
122         if (connection != null) {
123             connection.close();
124         }
125
126         ScheduledFuture<?> scheduledRefreshFuture = this.scheduledRefreshFuture;
127         this.scheduledRefreshFuture = null;
128         if (scheduledRefreshFuture != null) {
129             scheduledRefreshFuture.cancel(true);
130         }
131
132         synchronized (channelDescriptors) {
133             channelDescriptors.clear();
134         }
135     }
136
137     @Override
138     public void handleCommand(ChannelUID channelUID, Command command) {
139         if (command instanceof RefreshType) {
140             processChannelReadRequest(channelUID.getId());
141         } else {
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);
146             } else {
147                 logger.debug("Unsupported channel {}", channelUID.getId());
148             }
149         }
150     }
151
152     private static double commandToValue(Command command) {
153         if (command instanceof QuantityType<?>) {
154             return ((QuantityType<?>) command).doubleValue();
155         }
156
157         if (command instanceof DecimalType) {
158             return ((DecimalType) command).doubleValue();
159         }
160
161         throw new NumberFormatException("Command '" + command + "' not supported");
162     }
163
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.
169         });
170     }
171
172     private void processChannelReadRequest(String channelIID) {
173         switch (channelIID) {
174             case CHANNEL_LAST_ERROR_TYPE:
175                 readAndUpdateLastErrorType();
176                 break;
177             case CHANNEL_LAST_ERROR_TIMESTAMP:
178                 readAndUpdateLastErrorTimestamp();
179                 break;
180             case CHANNEL_FRONT_PANEL_POWER_LAMP:
181                 readAndUpdateFrontPanel(channelIID, (short) 0x0012);
182                 break;
183             case CHANNEL_FRONT_PANEL_PUMP_LAMP:
184                 readAndUpdateFrontPanel(channelIID, (short) 0x0013);
185                 break;
186             case CHANNEL_FRONT_PANEL_ADDITIONAL_HEAT_LAMP:
187                 readAndUpdateFrontPanel(channelIID, (short) 0x0014);
188                 break;
189             case CHANNEL_FRONT_PANEL_WATER_HEATER_LAMP:
190                 readAndUpdateFrontPanel(channelIID, (short) 0x0015);
191                 break;
192             case CHANNEL_FRONT_PANEL_ALARM_LAMP:
193                 readAndUpdateFrontPanel(channelIID, (short) 0x0016);
194                 break;
195             default:
196                 readAndUpdateSystemRegister(channelIID);
197                 break;
198         }
199     }
200
201     private Collection<String> linkedChannels() {
202         return thing.getChannels().stream().map(Channel::getUID).map(ChannelUID::getId).filter(this::isLinked)
203                 .collect(Collectors.toList());
204     }
205
206     private void refresh() {
207         for (String channelIID : linkedChannels()) {
208             if (Thread.interrupted()) {
209                 break;
210             }
211
212             processChannelReadRequest(channelIID);
213
214             if (thing.getStatus() != ThingStatus.ONLINE) {
215                 break;
216             }
217         }
218     }
219
220     private void readAndUpdateLastErrorType() {
221         readAndUpdateLastError(CHANNEL_LAST_ERROR_TYPE, e -> new StringType(Byte.toString(e.error())));
222     }
223
224     private void readAndUpdateLastErrorTimestamp() {
225         readAndUpdateLastError(CHANNEL_LAST_ERROR_TIMESTAMP, e -> new DateTimeType(e.timestamp()));
226     }
227
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);
232                 });
233     }
234
235     private void readAndUpdateFrontPanel(String channelIID, short address) {
236         byte[] command = CommandFactory.createReadFromFrontPanelCommand(address);
237         executeCommandAndUpdateState(channelIID, command, ResponseParserFactory.SHORT, DecimalType::new);
238     }
239
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);
248             });
249         } else {
250             logger.debug("Unsupported channel {}", channelIID);
251         }
252     }
253
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));
260         });
261     }
262
263     private synchronized <T> void executeCommand(@Nullable String channelIID, byte[] command, ResponseParser<T> parser,
264             Consumer<T> resultProcessor) {
265         try {
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);
270
271             synchronized (channelDescriptors) {
272                 channelDescriptors.clear();
273             }
274
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);
280             }
281         } catch (InterruptedException e) {
282             logger.debug("Execution interrupted when accessing value for channel '{}'.", channelIID, e);
283             Thread.currentThread().interrupt();
284         }
285     }
286
287     private <T> T executeCommandWithRetry(@Nullable String channelIID, byte[] command, ResponseParser<T> parser,
288             int retry) throws Rego6xxProtocolException, IOException, InterruptedException {
289         try {
290             checkRegoDevice();
291             return executeCommand(channelIID, command, parser);
292         } catch (IOException | Rego6xxProtocolException e) {
293             if (retry > 0) {
294                 logger.debug("Executing command for channel '{}' failed, retry {}.", channelIID, retry, e);
295                 Thread.sleep(200);
296                 return executeCommandWithRetry(channelIID, command, parser, retry - 1);
297             }
298
299             throw e;
300         }
301     }
302
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);
308
309             if (regoVersion != 600) {
310                 throw new IOException("Invalid rego version received " + regoVersion.toString());
311             }
312
313             updateStatus(ThingStatus.ONLINE);
314             logger.debug("Connected to Rego version {}.", regoVersion);
315         }
316     }
317
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);
324             }
325             return descriptor;
326         }
327     }
328
329     private <T> T executeCommand(@Nullable String channelIID, byte[] command, ResponseParser<T> parser)
330             throws Rego6xxProtocolException, IOException, InterruptedException {
331         try {
332             return executeCommandInternal(channelIID, command, parser);
333         } catch (IOException e) {
334             RegoConnection connection = this.connection;
335             if (connection != null) {
336                 connection.close();
337             }
338
339             throw e;
340         }
341     }
342
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;
350
351         // Use transient channel descriptor for null (not cached) channels.
352         ChannelDescriptor descriptor;
353         if (mappedChannelIID == null) {
354             descriptor = new ChannelDescriptor();
355         } else {
356             descriptor = channelDescriptorForChannel(mappedChannelIID);
357         }
358
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);
363         }
364
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");
369
370         }
371         if (!connection.isConnected()) {
372             connection.connect();
373         }
374
375         // Give heat pump some time between commands. Feeding commands too quickly
376         // might cause heat pump not to respond.
377         Thread.sleep(80);
378
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();
383         if (available > 0) {
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);
388         }
389
390         if (logger.isDebugEnabled()) {
391             logger.debug("Sending {}", HexUtils.bytesToHex(command));
392         }
393
394         // Send command
395         OutputStream outputStream = connection.outputStream();
396         outputStream.write(command);
397         outputStream.flush();
398
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;
402         int pos = 0;
403
404         do {
405             int len = inputStream.read(response, pos, response.length - pos);
406             if (len > 0) {
407                 pos += len;
408             } else {
409                 // Give some time for response to arrive...
410                 Thread.sleep(50);
411             }
412
413         } while (pos < response.length && timeout > System.currentTimeMillis());
414
415         if (pos < response.length) {
416             logger.debug("Response not received, read {} bytes => {}", pos, response);
417
418             throw new IOException("Response not received - got " + Integer.toString(pos) + " bytes of "
419                     + Integer.toString(response.length));
420         }
421
422         if (logger.isDebugEnabled()) {
423             logger.debug("Received {}", HexUtils.bytesToHex(response));
424         }
425
426         T result = parser.parse(response);
427
428         // If reading/parsing was done successfully, cache response payload.
429         descriptor.setValue(response);
430
431         return result;
432     }
433 }