2 * Copyright (c) 2010-2024 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.velbus.internal.handler;
15 import static org.openhab.binding.velbus.internal.VelbusBindingConstants.*;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.OutputStream;
20 import java.time.Instant;
21 import java.time.ZonedDateTime;
22 import java.util.Collection;
23 import java.util.HashMap;
26 import java.util.TimeZone;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.velbus.internal.VelbusClockAlarm;
33 import org.openhab.binding.velbus.internal.VelbusClockAlarmConfiguration;
34 import org.openhab.binding.velbus.internal.VelbusPacketInputStream;
35 import org.openhab.binding.velbus.internal.VelbusPacketListener;
36 import org.openhab.binding.velbus.internal.config.VelbusBridgeConfig;
37 import org.openhab.binding.velbus.internal.discovery.VelbusThingDiscoveryService;
38 import org.openhab.binding.velbus.internal.packets.VelbusSetDatePacket;
39 import org.openhab.binding.velbus.internal.packets.VelbusSetDaylightSavingsStatusPacket;
40 import org.openhab.binding.velbus.internal.packets.VelbusSetLocalClockAlarmPacket;
41 import org.openhab.binding.velbus.internal.packets.VelbusSetRealtimeClockPacket;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.OnOffType;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.binding.BaseBridgeHandler;
49 import org.openhab.core.thing.binding.ThingHandlerService;
50 import org.openhab.core.types.Command;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * {@link VelbusBridgeHandler} is an abstract handler for a Velbus interface and connects it to
58 * @author Cedric Boon - Initial contribution
59 * @author Daniel Rosengarten - Add global alarm configuration from bridge (removed from modules), reduces bus flooding
60 * on alarm value update
63 public abstract class VelbusBridgeHandler extends BaseBridgeHandler {
64 private final Logger logger = LoggerFactory.getLogger(VelbusBridgeHandler.class);
66 private long lastPacketTimeMillis;
68 protected @Nullable VelbusPacketListener defaultPacketListener;
69 protected Map<Byte, VelbusPacketListener> packetListeners = new HashMap<>();
71 private @NonNullByDefault({}) VelbusBridgeConfig bridgeConfig;
72 private @Nullable ScheduledFuture<?> timeUpdateJob;
73 private @Nullable ScheduledFuture<?> reconnectionHandler;
75 private @NonNullByDefault({}) OutputStream outputStream;
76 private @NonNullByDefault({}) VelbusPacketInputStream inputStream;
78 private boolean listenerStopped;
80 private VelbusClockAlarmConfiguration alarmClockConfiguration = new VelbusClockAlarmConfiguration();
82 private long lastUpdateAlarm1TimeMillis;
83 private long lastUpdateAlarm2TimeMillis;
85 public VelbusBridgeHandler(Bridge velbusBridge) {
90 public void initialize() {
91 logger.debug("Initializing velbus bridge handler.");
93 bridgeConfig = getConfigAs(VelbusBridgeConfig.class);
96 initializeTimeUpdate();
99 private void initializeTimeUpdate() {
100 int timeUpdateInterval = bridgeConfig.timeUpdateInterval;
102 if (timeUpdateInterval > 0) {
103 startTimeUpdates(timeUpdateInterval);
107 private void startTimeUpdates(int timeUpdatesInterval) {
108 timeUpdateJob = scheduler.scheduleWithFixedDelay(this::updateDateTime, 0, timeUpdatesInterval,
112 private void updateDateTime() {
113 ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(Instant.now(), TimeZone.getDefault().toZoneId());
115 updateDate(zonedDateTime);
116 updateTime(zonedDateTime);
117 updateDaylightSavingsStatus(zonedDateTime);
120 private void updateTime(ZonedDateTime zonedDateTime) {
121 VelbusSetRealtimeClockPacket packet = new VelbusSetRealtimeClockPacket((byte) 0x00, zonedDateTime);
123 byte[] packetBytes = packet.getBytes();
124 this.sendPacket(packetBytes);
127 private void updateDate(ZonedDateTime zonedDateTime) {
128 VelbusSetDatePacket packet = new VelbusSetDatePacket((byte) 0x00, zonedDateTime);
130 byte[] packetBytes = packet.getBytes();
131 this.sendPacket(packetBytes);
134 private void updateDaylightSavingsStatus(ZonedDateTime zonedDateTime) {
135 VelbusSetDaylightSavingsStatusPacket packet = new VelbusSetDaylightSavingsStatusPacket((byte) 0x00,
138 byte[] packetBytes = packet.getBytes();
139 this.sendPacket(packetBytes);
142 protected void initializeStreams(OutputStream outputStream, InputStream inputStream) {
143 this.outputStream = outputStream;
144 this.inputStream = new VelbusPacketInputStream(inputStream);
148 public void dispose() {
149 final ScheduledFuture<?> timeUpdateJob = this.timeUpdateJob;
150 if (timeUpdateJob != null) {
151 timeUpdateJob.cancel(true);
157 public void handleCommand(ChannelUID channelUID, Command command) {
158 if (isAlarmClockChannel(channelUID)) {
159 byte alarmNumber = determineAlarmNumber(channelUID);
160 VelbusClockAlarm alarmClock = alarmClockConfiguration.getAlarmClock(alarmNumber);
162 alarmClock.setLocal(false);
164 switch (channelUID.getId()) {
165 case CHANNEL_BRIDGE_CLOCK_ALARM1_ENABLED:
166 case CHANNEL_BRIDGE_CLOCK_ALARM2_ENABLED: {
167 if (command instanceof OnOffType) {
168 boolean enabled = command == OnOffType.ON;
169 alarmClock.setEnabled(enabled);
173 case CHANNEL_BRIDGE_CLOCK_ALARM1_WAKEUP_HOUR:
174 case CHANNEL_BRIDGE_CLOCK_ALARM2_WAKEUP_HOUR: {
175 if (command instanceof DecimalType decimalCommand) {
176 byte wakeupHour = decimalCommand.byteValue();
177 alarmClock.setWakeupHour(wakeupHour);
181 case CHANNEL_BRIDGE_CLOCK_ALARM1_WAKEUP_MINUTE:
182 case CHANNEL_BRIDGE_CLOCK_ALARM2_WAKEUP_MINUTE: {
183 if (command instanceof DecimalType decimalCommand) {
184 byte wakeupMinute = decimalCommand.byteValue();
185 alarmClock.setWakeupMinute(wakeupMinute);
189 case CHANNEL_BRIDGE_CLOCK_ALARM1_BEDTIME_HOUR:
190 case CHANNEL_BRIDGE_CLOCK_ALARM2_BEDTIME_HOUR: {
191 if (command instanceof DecimalType decimalCommand) {
192 byte bedTimeHour = decimalCommand.byteValue();
193 alarmClock.setBedtimeHour(bedTimeHour);
197 case CHANNEL_BRIDGE_CLOCK_ALARM1_BEDTIME_MINUTE:
198 case CHANNEL_BRIDGE_CLOCK_ALARM2_BEDTIME_MINUTE: {
199 if (command instanceof DecimalType decimalCommand) {
200 byte bedTimeMinute = decimalCommand.byteValue();
201 alarmClock.setBedtimeMinute(bedTimeMinute);
207 if (alarmNumber == 1) {
208 lastUpdateAlarm1TimeMillis = System.currentTimeMillis();
210 lastUpdateAlarm2TimeMillis = System.currentTimeMillis();
213 VelbusSetLocalClockAlarmPacket packet = new VelbusSetLocalClockAlarmPacket((byte) 0x00, alarmNumber,
215 byte[] packetBytes = packet.getBytes();
217 // Schedule the send of the packet to see if there is another update in less than 10 secondes (reduce
218 // flooding of the bus)
219 scheduler.schedule(() -> {
220 sendAlarmPacket(alarmNumber, packetBytes);
221 }, DELAY_SEND_CLOCK_ALARM_UPDATE, TimeUnit.MILLISECONDS);
223 logger.debug("The command '{}' is not supported by this handler.", command.getClass());
227 public synchronized void sendAlarmPacket(int alarmNumber, byte[] packetBytes) {
228 long timeSinceLastUpdate;
230 if (alarmNumber == 1) {
231 timeSinceLastUpdate = System.currentTimeMillis() - lastUpdateAlarm1TimeMillis;
233 timeSinceLastUpdate = System.currentTimeMillis() - lastUpdateAlarm2TimeMillis;
236 // If a value of the alarm has been updated, discard this old update
237 if (timeSinceLastUpdate < DELAY_SEND_CLOCK_ALARM_UPDATE) {
241 sendPacket(packetBytes);
244 public synchronized void sendPacket(byte[] packet) {
245 long currentTimeMillis = System.currentTimeMillis();
246 long timeSinceLastPacket = currentTimeMillis - lastPacketTimeMillis;
248 if (timeSinceLastPacket < 60) {
249 // When sending you need a delay of 60ms between each packet (to prevent flooding the VMB1USB).
250 long timeToDelay = 60 - timeSinceLastPacket;
252 scheduler.schedule(() -> {
254 }, timeToDelay, TimeUnit.MILLISECONDS);
261 lastPacketTimeMillis = System.currentTimeMillis();
264 private void readPacket(byte[] packet) {
265 byte address = packet[2];
267 if (packetListeners.containsKey(address)) {
268 VelbusPacketListener packetListener = packetListeners.get(address);
269 packetListener.onPacketReceived(packet);
271 final VelbusPacketListener defaultPacketListener = this.defaultPacketListener;
272 if (defaultPacketListener != null) {
273 defaultPacketListener.onPacketReceived(packet);
278 protected void readPackets() {
279 if (inputStream == null) {
286 listenerStopped = false;
289 while (!listenerStopped & ((packet = inputStream.readPacket()).length > 0)) {
292 } catch (IOException e) {
293 if (!listenerStopped) {
299 private void writePacket(byte[] packet) {
300 if (outputStream == null) {
306 outputStream.write(packet);
307 outputStream.flush();
308 } catch (IOException e) {
313 protected void onConnectionLost() {
314 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
315 "A network communication error occurred.");
317 startReconnectionHandler();
321 * Makes a connection to the Velbus system.
323 * @return True if the connection succeeded, false if the connection did not succeed.
325 protected abstract boolean connect();
327 protected void disconnect() {
328 listenerStopped = true;
331 if (outputStream != null) {
332 outputStream.close();
334 } catch (IOException e) {
335 logger.debug("Error while closing output stream", e);
339 if (inputStream != null) {
342 } catch (IOException e) {
343 logger.debug("Error while closing input stream", e);
347 public void startReconnectionHandler() {
348 final ScheduledFuture<?> reconnectionHandler = this.reconnectionHandler;
349 if (reconnectionHandler == null || reconnectionHandler.isCancelled()) {
350 int reconnectionInterval = bridgeConfig.reconnectionInterval;
351 if (reconnectionInterval > 0) {
352 this.reconnectionHandler = scheduler.scheduleWithFixedDelay(() -> {
353 final ScheduledFuture<?> currentReconnectionHandler = this.reconnectionHandler;
354 if (connect() && currentReconnectionHandler != null) {
355 currentReconnectionHandler.cancel(false);
357 }, reconnectionInterval, reconnectionInterval, TimeUnit.SECONDS);
363 public Collection<Class<? extends ThingHandlerService>> getServices() {
364 return Set.of(VelbusThingDiscoveryService.class);
367 public void setDefaultPacketListener(VelbusPacketListener velbusPacketListener) {
368 defaultPacketListener = velbusPacketListener;
371 public void clearDefaultPacketListener() {
372 defaultPacketListener = null;
375 public void registerPacketListener(byte address, VelbusPacketListener packetListener) {
376 packetListeners.put(Byte.valueOf(address), packetListener);
379 public void unregisterRelayStatusListener(byte address) {
380 packetListeners.remove(Byte.valueOf(address));
383 protected boolean isAlarmClockChannel(ChannelUID channelUID) {
384 switch (channelUID.getId()) {
385 case CHANNEL_BRIDGE_CLOCK_ALARM1_ENABLED:
386 case CHANNEL_BRIDGE_CLOCK_ALARM1_WAKEUP_HOUR:
387 case CHANNEL_BRIDGE_CLOCK_ALARM1_WAKEUP_MINUTE:
388 case CHANNEL_BRIDGE_CLOCK_ALARM1_BEDTIME_HOUR:
389 case CHANNEL_BRIDGE_CLOCK_ALARM1_BEDTIME_MINUTE:
390 case CHANNEL_BRIDGE_CLOCK_ALARM2_ENABLED:
391 case CHANNEL_BRIDGE_CLOCK_ALARM2_WAKEUP_HOUR:
392 case CHANNEL_BRIDGE_CLOCK_ALARM2_WAKEUP_MINUTE:
393 case CHANNEL_BRIDGE_CLOCK_ALARM2_BEDTIME_HOUR:
394 case CHANNEL_BRIDGE_CLOCK_ALARM2_BEDTIME_MINUTE:
400 protected byte determineAlarmNumber(ChannelUID channelUID) {
401 switch (channelUID.getId()) {
402 case CHANNEL_BRIDGE_CLOCK_ALARM1_ENABLED:
403 case CHANNEL_BRIDGE_CLOCK_ALARM1_WAKEUP_HOUR:
404 case CHANNEL_BRIDGE_CLOCK_ALARM1_WAKEUP_MINUTE:
405 case CHANNEL_BRIDGE_CLOCK_ALARM1_BEDTIME_HOUR:
406 case CHANNEL_BRIDGE_CLOCK_ALARM1_BEDTIME_MINUTE:
408 case CHANNEL_BRIDGE_CLOCK_ALARM2_ENABLED:
409 case CHANNEL_BRIDGE_CLOCK_ALARM2_WAKEUP_HOUR:
410 case CHANNEL_BRIDGE_CLOCK_ALARM2_WAKEUP_MINUTE:
411 case CHANNEL_BRIDGE_CLOCK_ALARM2_BEDTIME_HOUR:
412 case CHANNEL_BRIDGE_CLOCK_ALARM2_BEDTIME_MINUTE:
416 throw new IllegalArgumentException("The given channelUID is not an alarm clock channel: " + channelUID);