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.velbus.internal.handler;
15 import static org.openhab.binding.velbus.internal.VelbusBindingConstants.*;
17 import java.util.Arrays;
18 import java.util.HashSet;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.velbus.internal.VelbusChannelIdentifier;
26 import org.openhab.binding.velbus.internal.config.VelbusSensorConfig;
27 import org.openhab.binding.velbus.internal.packets.VelbusDimmerPacket;
28 import org.openhab.binding.velbus.internal.packets.VelbusPacket;
29 import org.openhab.binding.velbus.internal.packets.VelbusSensorReadoutRequestPacket;
30 import org.openhab.binding.velbus.internal.packets.VelbusStatusRequestPacket;
31 import org.openhab.core.library.types.PercentType;
32 import org.openhab.core.library.types.QuantityType;
33 import org.openhab.core.library.types.StringType;
34 import org.openhab.core.library.unit.MetricPrefix;
35 import org.openhab.core.library.unit.Units;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.ThingTypeUID;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.RefreshType;
45 * The {@link VelbusVMB4ANHandler} is responsible for handling commands, which are
46 * sent to one of the channels.
48 * @author Cedric Boon - Initial contribution
51 public class VelbusVMB4ANHandler extends VelbusSensorWithAlarmClockHandler {
52 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = new HashSet<>(Arrays.asList(THING_TYPE_VMB4AN));
54 private static final String ALARM_GROUP = "alarm";
55 private static final String ANALOG_INPUT_GROUP = "analogInput";
56 private static final String ANALOG_OUTPUT_GROUP = "analogOutput";
57 private static final String RAW_CHANNEL_SUFFIX = "Raw";
59 private static final byte VOLTAGE_SENSOR_TYPE = 0x00;
60 private static final byte CURRENT_SENSOR_TYPE = 0x01;
61 private static final byte RESISTANCE_SENSOR_TYPE = 0x02;
62 private static final byte PERIOD_MEASUREMENT_SENSOR_TYPE = 0x03;
64 private String[] channelText = new String[] { "", "", "", "" };
66 private @Nullable ScheduledFuture<?> refreshJob;
67 private @NonNullByDefault({}) VelbusSensorConfig sensorConfig;
69 public VelbusVMB4ANHandler(Thing thing) {
74 public void initialize() {
75 this.sensorConfig = getConfigAs(VelbusSensorConfig.class);
79 initializeAutomaticRefresh();
82 private void initializeAutomaticRefresh() {
83 int refreshInterval = sensorConfig.refresh;
85 if (refreshInterval > 0) {
86 startAutomaticRefresh(refreshInterval);
91 public void dispose() {
92 final ScheduledFuture<?> refreshJob = this.refreshJob;
93 if (refreshJob != null) {
94 refreshJob.cancel(true);
98 private void startAutomaticRefresh(int refreshInterval) {
99 VelbusBridgeHandler velbusBridgeHandler = getVelbusBridgeHandler();
100 if (velbusBridgeHandler == null) {
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
105 refreshJob = scheduler.scheduleWithFixedDelay(() -> {
106 sendSensorReadoutRequest(velbusBridgeHandler, ALL_CHANNELS);
107 }, 0, refreshInterval, TimeUnit.SECONDS);
111 public void handleCommand(ChannelUID channelUID, Command command) {
112 super.handleCommand(channelUID, command);
114 VelbusBridgeHandler velbusBridgeHandler = getVelbusBridgeHandler();
115 if (velbusBridgeHandler == null) {
116 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
120 byte channelByte = convertChannelUIDToChannelByte(channelUID);
122 if (command instanceof RefreshType) {
123 VelbusStatusRequestPacket packet = new VelbusStatusRequestPacket(channelByte);
125 byte[] packetBytes = packet.getBytes();
126 velbusBridgeHandler.sendPacket(packetBytes);
127 } else if (command instanceof PercentType percentCommand && isAnalogOutputChannel(channelUID)) {
128 VelbusDimmerPacket packet = new VelbusDimmerPacket(
129 new VelbusChannelIdentifier(this.getModuleAddress().getAddress(), channelByte), COMMAND_SET_VALUE,
130 percentCommand.byteValue(), 0x00, false);
132 byte[] packetBytes = packet.getBytes();
133 velbusBridgeHandler.sendPacket(packetBytes);
137 protected void sendSensorReadoutRequest(VelbusBridgeHandler velbusBridgeHandler, byte channel) {
138 VelbusSensorReadoutRequestPacket packet = new VelbusSensorReadoutRequestPacket(getModuleAddress().getAddress(),
141 byte[] packetBytes = packet.getBytes();
142 velbusBridgeHandler.sendPacket(packetBytes);
146 public void onPacketReceived(byte[] packet) {
147 super.onPacketReceived(packet);
149 logger.trace("onPacketReceived() was called");
151 if (packet[0] == VelbusPacket.STX && packet.length >= 5) {
152 byte command = packet[4];
154 if (command == COMMAND_SENSOR_RAW_DATA && packet.length >= 10) {
155 byte channel = packet[5];
156 byte operatingMode = packet[6];
157 byte upperByteSensorValue = packet[7];
158 byte highByteSensorValue = packet[8];
159 byte lowByteSensorValue = packet[9];
161 double sensorValue = (((upperByteSensorValue & 0xff) << 16) + ((highByteSensorValue & 0xff) << 8)
162 + (lowByteSensorValue & 0xff));
163 String channelUID = convertAnalogInputChannelByteToRawChannelUID(channel);
165 switch (operatingMode) {
166 case VOLTAGE_SENSOR_TYPE:
167 double voltageResolution = 0.25;
168 double voltageSensorValueState = sensorValue * voltageResolution;
169 updateState(channelUID,
170 new QuantityType<>(voltageSensorValueState, MetricPrefix.MILLI(Units.VOLT)));
172 case CURRENT_SENSOR_TYPE:
173 double currentResolution = 5;
174 double currentSensorValueState = sensorValue * currentResolution;
175 updateState(channelUID,
176 new QuantityType<>(currentSensorValueState, MetricPrefix.MICRO(Units.AMPERE)));
178 case RESISTANCE_SENSOR_TYPE:
179 double resistanceResolution = 0.25;
180 double resistanceSensorValueState = sensorValue * resistanceResolution;
181 updateState(channelUID, new QuantityType<>(resistanceSensorValueState, Units.OHM));
183 case PERIOD_MEASUREMENT_SENSOR_TYPE:
184 double periodResolution = 0.5;
185 double periodSensorValueState = sensorValue * periodResolution;
186 updateState(channelUID,
187 new QuantityType<>(periodSensorValueState, MetricPrefix.MICRO(Units.SECOND)));
190 } else if (command == COMMAND_TEXT) {
191 byte channel = packet[5];
192 byte textStartPosition = packet[6];
194 StringBuilder contents = new StringBuilder();
195 for (int i = 7; i < packet.length - 2; i++) {
196 byte currentChar = packet[i];
197 if (currentChar == (byte) -0x50) {
198 contents.append("°");
199 } else if (currentChar != (byte) 0x00) {
200 contents.append((char) currentChar);
204 channelText[channel - 9] = channelText[channel - 9].substring(0, textStartPosition)
205 + contents.toString()
206 + (channelText[channel - 9].length() > textStartPosition + 5 ? channelText[channel - 9]
207 .substring(textStartPosition + 5, channelText[channel - 9].length()) : "");
209 String channelUID = convertAnalogInputChannelByteToChannelUID(channel);
210 updateState(channelUID, new StringType(channelText[channel - 9]));
215 protected byte convertChannelUIDToChannelByte(ChannelUID channelUID) {
216 if (isAlarmChannel(channelUID)) {
217 return convertAlarmChannelUIDToChannelByte(channelUID);
218 } else if (isTextAnalogInputChannel(channelUID)) {
219 return convertTextAnalogInputChannelUIDToChannelByte(channelUID);
220 } else if (isRawAnalogInputChannel(channelUID)) {
221 return convertRawAnalogInputChannelUIDToChannelByte(channelUID);
222 } else if (isAnalogOutputChannel(channelUID)) {
223 return convertAnalogOutputChannelUIDToChannelByte(channelUID);
225 throw new UnsupportedOperationException(
226 "The channel '" + channelUID + "' is not supported on a VMB4AN module.");
230 protected boolean isAlarmChannel(ChannelUID channelUID) {
231 return ALARM_GROUP.equals(channelUID.getGroupId());
234 protected byte convertAlarmChannelUIDToChannelByte(ChannelUID channelUID) {
235 return Byte.parseByte(channelUID.getIdWithoutGroup().replaceAll(CHANNEL, ""));
238 protected boolean isTextAnalogInputChannel(ChannelUID channelUID) {
239 return ANALOG_INPUT_GROUP.equals(channelUID.getGroupId())
240 && !channelUID.getIdWithoutGroup().endsWith(RAW_CHANNEL_SUFFIX);
243 protected boolean isRawAnalogInputChannel(ChannelUID channelUID) {
244 return ANALOG_INPUT_GROUP.equals(channelUID.getGroupId())
245 && channelUID.getIdWithoutGroup().endsWith(RAW_CHANNEL_SUFFIX);
248 protected byte convertRawAnalogInputChannelUIDToChannelByte(ChannelUID channelUID) {
250 .parseByte(channelUID.getIdWithoutGroup().replaceAll(CHANNEL, "").replaceAll(RAW_CHANNEL_SUFFIX, ""));
253 protected byte convertTextAnalogInputChannelUIDToChannelByte(ChannelUID channelUID) {
254 return Byte.parseByte(channelUID.getIdWithoutGroup().replaceAll(CHANNEL, ""));
257 protected String convertAnalogInputChannelByteToRawChannelUID(byte channelByte) {
258 return convertAnalogInputChannelByteToChannelUID(channelByte) + RAW_CHANNEL_SUFFIX;
261 protected String convertAnalogInputChannelByteToChannelUID(byte channelByte) {
262 return ANALOG_INPUT_GROUP + "#" + CHANNEL + channelByte;
265 protected boolean isAnalogOutputChannel(ChannelUID channelUID) {
266 return ANALOG_OUTPUT_GROUP.equals(channelUID.getGroupId());
269 protected byte convertAnalogOutputChannelUIDToChannelByte(ChannelUID channelUID) {
270 return Byte.parseByte(channelUID.getIdWithoutGroup().replaceAll(CHANNEL, ""));
274 protected int getClockAlarmAndProgramSelectionIndexInModuleStatus() {