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.resol.handler;
15 import java.text.SimpleDateFormat;
16 import java.util.ArrayList;
17 import java.util.List;
18 import java.util.Locale;
19 import java.util.TimeZone;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.resol.internal.ResolBindingConstants;
24 import org.openhab.binding.resol.internal.ResolStateDescriptionOptionProvider;
25 import org.openhab.binding.resol.internal.providers.ResolChannelTypeProvider;
26 import org.openhab.core.library.types.DateTimeType;
27 import org.openhab.core.library.types.QuantityType;
28 import org.openhab.core.library.types.StringType;
29 import org.openhab.core.library.unit.Units;
30 import org.openhab.core.thing.Bridge;
31 import org.openhab.core.thing.Channel;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.binding.ThingHandler;
35 import org.openhab.core.thing.binding.builder.ChannelBuilder;
36 import org.openhab.core.thing.binding.builder.ThingBuilder;
37 import org.openhab.core.thing.type.ChannelTypeUID;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.StateOption;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import de.resol.vbus.Packet;
44 import de.resol.vbus.Specification;
45 import de.resol.vbus.Specification.PacketFieldSpec;
46 import de.resol.vbus.Specification.PacketFieldValue;
47 import de.resol.vbus.SpecificationFile;
48 import de.resol.vbus.SpecificationFile.Enum;
49 import de.resol.vbus.SpecificationFile.EnumVariant;
50 import de.resol.vbus.SpecificationFile.Language;
53 * The {@link ResolThingHandler} is responsible for handling commands, which are
54 * sent to one of the channels.
56 * @author Raphael Mack - Initial contribution
59 public class ResolThingHandler extends ResolBaseThingHandler {
61 private final Logger logger = LoggerFactory.getLogger(ResolThingHandler.class);
63 private ResolStateDescriptionOptionProvider stateDescriptionProvider;
65 private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(
66 DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS_GENERAL);
68 private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm");
70 private static final SimpleDateFormat WEEK_FORMAT = new SimpleDateFormat("EEE,HH:mm");
73 synchronized (DATE_FORMAT) {
74 DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
76 synchronized (TIME_FORMAT) {
77 TIME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
79 synchronized (WEEK_FORMAT) {
80 WEEK_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
84 public ResolThingHandler(Thing thing, ResolStateDescriptionOptionProvider stateDescriptionProvider) {
86 this.stateDescriptionProvider = stateDescriptionProvider;
90 public void handleCommand(ChannelUID channelUID, Command command) {
91 /* we ignore the commands for now on purpose */
95 public void initialize() {
96 ResolBridgeHandler bridgeHandler = getBridgeHandler();
97 if (bridgeHandler != null) {
98 updateStatus(bridgeHandler.getStatus());
102 private synchronized @Nullable ResolBridgeHandler getBridgeHandler() {
103 Bridge bridge = getBridge();
104 if (bridge == null) {
105 logger.debug("Required bridge not defined for device.");
108 return getBridgeHandler(bridge);
112 private synchronized @Nullable ResolBridgeHandler getBridgeHandler(Bridge bridge) {
113 ResolBridgeHandler bridgeHandler = null;
115 ThingHandler handler = bridge.getHandler();
116 if (handler instanceof ResolBridgeHandler resolBridgeHandler) {
117 bridgeHandler = resolBridgeHandler;
119 logger.debug("No available bridge handler found yet. Bridge: {} .", bridge.getUID());
121 return bridgeHandler;
125 protected void packetReceived(Specification spec, Language lang, Packet packet) {
126 PacketFieldValue[] pfvs = spec.getPacketFieldValuesForHeaders(new Packet[] { packet });
127 for (PacketFieldValue pfv : pfvs) {
128 logger.trace("Id: {}, Name: {}, Raw: {}, Text: {}", pfv.getPacketFieldId(), pfv.getName(lang),
129 pfv.getRawValueDouble(), pfv.formatTextValue(null, Locale.getDefault()));
131 String channelId = pfv.getName(); // use English name as channel
132 channelId = channelId.replace(" [", "-");
133 channelId = channelId.replace("]", "");
134 channelId = channelId.replace("(", "-");
135 channelId = channelId.replace(")", "");
136 channelId = channelId.replace(" #", "-");
137 channelId = channelId.replaceAll("[^A-Za-z0-9_-]+", "_");
139 channelId = channelId.toLowerCase(Locale.ENGLISH);
141 ChannelTypeUID channelTypeUID;
143 if (pfv.getPacketFieldSpec().getUnit().getUnitId() >= 0) {
144 channelTypeUID = new ChannelTypeUID(ResolBindingConstants.BINDING_ID,
145 pfv.getPacketFieldSpec().getUnit().getUnitCodeText());
146 } else if (pfv.getPacketFieldSpec().getType() == SpecificationFile.Type.DateTime) {
147 channelTypeUID = new ChannelTypeUID(ResolBindingConstants.BINDING_ID, "datetime");
148 } else if (pfv.getPacketFieldSpec().getType() == SpecificationFile.Type.WeekTime) {
149 channelTypeUID = new ChannelTypeUID(ResolBindingConstants.BINDING_ID, "weektime");
150 } else if (pfv.getPacketFieldSpec().getType() == SpecificationFile.Type.Time) {
151 channelTypeUID = new ChannelTypeUID(ResolBindingConstants.BINDING_ID, "time");
153 /* used for enums and the numeric types without unit */
154 channelTypeUID = new ChannelTypeUID(ResolBindingConstants.BINDING_ID, "None");
157 String acceptedItemType;
159 Thing thing = getThing();
160 switch (pfv.getPacketFieldSpec().getType()) {
162 acceptedItemType = "DateTime";
165 acceptedItemType = ResolChannelTypeProvider.itemTypeForUnit(pfv.getPacketFieldSpec().getUnit());
170 acceptedItemType = "String";
173 Channel a = thing.getChannel(channelId);
176 /* channel doesn't exit, let's create it */
177 ThingBuilder thingBuilder = editThing();
178 ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
180 if (pfv.getEnumVariant() != null) {
181 /* create a state option channel */
182 List<StateOption> options = new ArrayList<>();
183 PacketFieldSpec ff = pfv.getPacketFieldSpec();
184 Enum e = ff.getEnum();
185 for (long l : e.getValues()) {
186 EnumVariant v = e.getEnumVariantForValue(l);
187 options.add(new StateOption(Long.toString(l), v.getText(lang)));
190 stateDescriptionProvider.setStateOptions(channelUID, options);
192 Channel channel = ChannelBuilder.create(channelUID, "Number").withType(channelTypeUID)
193 .withLabel(pfv.getName(lang)).build();
195 thingBuilder.withChannel(channel).withLabel(thing.getLabel());
196 updateThing(thingBuilder.build());
197 } else if ("DateTime".equals(acceptedItemType)) {
199 Channel channel = ChannelBuilder.create(channelUID, acceptedItemType).withType(channelTypeUID)
200 .withLabel(pfv.getName(lang)).build();
202 thingBuilder.withChannel(channel).withLabel(thing.getLabel());
203 updateThing(thingBuilder.build());
205 } else if ("String".equals(acceptedItemType)) {
206 /* a string channel */
207 Channel channel = ChannelBuilder.create(channelUID, "String").withType(channelTypeUID)
208 .withLabel(pfv.getName(lang)).build();
210 thingBuilder.withChannel(channel).withLabel(thing.getLabel());
211 updateThing(thingBuilder.build());
212 } else if (pfv.getRawValueDouble() != null) {
213 /* a number channel */
214 Channel channel = ChannelBuilder.create(channelUID, acceptedItemType).withType(channelTypeUID)
215 .withLabel(pfv.getName(lang)).build();
217 thingBuilder.withChannel(channel).withLabel(thing.getLabel());
218 updateThing(thingBuilder.build());
220 /* a string channel */
221 Channel channel = ChannelBuilder.create(channelUID, "String").withType(channelTypeUID)
222 .withLabel(pfv.getName(lang)).build();
224 thingBuilder.withChannel(channel).withLabel(thing.getLabel());
225 updateThing(thingBuilder.build());
227 logger.debug("Creating channel: {}", channelUID);
230 if (pfv.getEnumVariant() != null) {
231 /* update the enum / State channel */
232 this.updateState(channelId, new StringType(Long.toString(pfv.getRawValueLong())));
235 switch (pfv.getPacketFieldSpec().getType()) {
237 Double dd = pfv.getRawValueDouble();
239 if (!isSpecialValue(dd)) {
240 /* only set the value if no error occurred */
242 String str = pfv.formatText();
243 if (str.endsWith("RH")) {
244 /* unit %RH for relative humidity is not known in openHAB UoM, so we remove it */
245 str = str.substring(0, str.length() - 2);
247 if (str.endsWith("Ω")) {
248 QuantityType<?> q = new QuantityType<>(dd, Units.OHM);
249 this.updateState(channelId, q);
252 QuantityType<?> q = new QuantityType<>(str, Locale
253 .getDefault()); /* vbus library returns the value in default locale */
254 this.updateState(channelId, q);
255 } catch (IllegalArgumentException e) {
256 logger.debug("unit of '{}' unknown in openHAB", str);
257 QuantityType<?> q = new QuantityType<>(dd, Units.ONE);
258 this.updateState(channelId, q);
265 * field not available in this packet, e. g. old firmware version not (yet) transmitting it
270 synchronized (TIME_FORMAT) {
271 this.updateState(channelId, new StringType(TIME_FORMAT.format(pfv.getRawValueDate())));
275 synchronized (WEEK_FORMAT) {
276 this.updateState(channelId, new StringType(WEEK_FORMAT.format(pfv.getRawValueDate())));
280 synchronized (DATE_FORMAT) {
281 DateTimeType d = new DateTimeType(DATE_FORMAT.format(pfv.getRawValueDate()));
282 this.updateState(channelId, d);
286 Bridge b = getBridge();
288 ResolBridgeHandler handler = (ResolBridgeHandler) b.getHandler();
291 if (handler != null) {
292 loc = handler.getLocale();
294 loc = Locale.getDefault();
296 value = pfv.formatTextValue(pfv.getPacketFieldSpec().getUnit(), loc);
298 QuantityType<?> q = new QuantityType<>(value, loc);
299 this.updateState(channelId, q);
300 } catch (IllegalArgumentException e) {
301 this.updateState(channelId, new StringType(value));
302 logger.debug("unit of '{}' unknown in openHAB, using string", value);
310 /* check if the given value is a special one like 888.8 or 999.9 for shortcut or open load on a sensor wire */
311 @SuppressWarnings("PMD.SimplifyBooleanReturns")
312 private boolean isSpecialValue(Double dd) {
313 if ((Math.abs(dd - 888.8) < 0.1) || (Math.abs(dd - (-888.8)) < 0.1)) {
314 /* value out of range */
317 if (Math.abs(dd - 999.9) < 0.1) {
318 /* sensor not reachable */