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.lcn.internal;
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.HashMap;
19 import java.util.List;
21 import java.util.NoSuchElementException;
22 import java.util.Optional;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.lcn.internal.common.DimmerOutputCommand;
27 import org.openhab.binding.lcn.internal.common.LcnAddr;
28 import org.openhab.binding.lcn.internal.common.LcnAddrMod;
29 import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
30 import org.openhab.binding.lcn.internal.common.LcnException;
31 import org.openhab.binding.lcn.internal.connection.Connection;
32 import org.openhab.binding.lcn.internal.connection.ModInfo;
33 import org.openhab.binding.lcn.internal.converter.Converter;
34 import org.openhab.binding.lcn.internal.converter.Converters;
35 import org.openhab.binding.lcn.internal.converter.InversionConverter;
36 import org.openhab.binding.lcn.internal.converter.S0Converter;
37 import org.openhab.binding.lcn.internal.subhandler.AbstractLcnModuleSubHandler;
38 import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaAckSubHandler;
39 import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaFirmwareSubHandler;
40 import org.openhab.core.library.types.DecimalType;
41 import org.openhab.core.library.types.HSBType;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.library.types.PercentType;
44 import org.openhab.core.library.types.QuantityType;
45 import org.openhab.core.library.types.StopMoveType;
46 import org.openhab.core.library.types.StringType;
47 import org.openhab.core.library.types.UpDownType;
48 import org.openhab.core.thing.Bridge;
49 import org.openhab.core.thing.Channel;
50 import org.openhab.core.thing.ChannelUID;
51 import org.openhab.core.thing.Thing;
52 import org.openhab.core.thing.ThingStatus;
53 import org.openhab.core.thing.ThingStatusDetail;
54 import org.openhab.core.thing.binding.BaseThingHandler;
55 import org.openhab.core.thing.binding.ThingHandlerService;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.openhab.core.types.State;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
63 * The {@link LcnModuleHandler} is responsible for handling commands, which are
64 * sent to or received from one of the channels.
66 * @author Fabian Wolter - Initial contribution
69 public class LcnModuleHandler extends BaseThingHandler {
70 private final Logger logger = LoggerFactory.getLogger(LcnModuleHandler.class);
71 private static final int FIRMWARE_VERSION_LENGTH = 6;
72 private static final Map<String, Converter> VALUE_CONVERTERS = new HashMap<>();
73 private static final InversionConverter INVERSION_CONVERTER = new InversionConverter();
74 private @Nullable LcnAddrMod moduleAddress;
75 private final Map<LcnChannelGroup, AbstractLcnModuleSubHandler> subHandlers = new HashMap<>();
76 private final List<AbstractLcnModuleSubHandler> metadataSubHandlers = new ArrayList<>();
77 private final Map<ChannelUID, Converter> converters = new HashMap<>();
80 VALUE_CONVERTERS.put("temperature", Converters.TEMPERATURE);
81 VALUE_CONVERTERS.put("light", Converters.LIGHT);
82 VALUE_CONVERTERS.put("co2", Converters.CO2);
83 VALUE_CONVERTERS.put("current", Converters.CURRENT);
84 VALUE_CONVERTERS.put("voltage", Converters.VOLTAGE);
85 VALUE_CONVERTERS.put("angle", Converters.ANGLE);
86 VALUE_CONVERTERS.put("windspeed", Converters.WINDSPEED);
89 public LcnModuleHandler(Thing thing) {
94 public void initialize() {
95 LcnModuleConfiguration localConfig = getConfigAs(LcnModuleConfiguration.class);
96 LcnAddrMod localModuleAddress = moduleAddress = new LcnAddrMod(localConfig.segmentId, localConfig.moduleId);
99 ModInfo info = getPckGatewayHandler().getModInfo(localModuleAddress);
100 readFirmwareVersionFromProperty().ifPresent(info::setFirmwareVersion);
101 requestFirmwareVersionAndSerialNumberIfNotSet();
103 // create sub handlers
104 for (LcnChannelGroup type : LcnChannelGroup.values()) {
105 subHandlers.put(type, type.createSubHandler(this, info));
108 // meta sub handlers, which are not assigned to a channel group
109 metadataSubHandlers.add(new LcnModuleMetaAckSubHandler(this, info));
110 metadataSubHandlers.add(new LcnModuleMetaFirmwareSubHandler(this, info));
112 // initialize converters
113 for (Channel channel : thing.getChannels()) {
114 Object unitObject = channel.getConfiguration().get("unit");
115 Object parameterObject = channel.getConfiguration().get("parameter");
116 Object invertState = channel.getConfiguration().get("invertState");
117 Object invertUpDown = channel.getConfiguration().get("invertUpDown");
119 // Initialize value converters
120 if (unitObject instanceof String) {
121 switch ((String) unitObject) {
124 converters.put(channel.getUID(), new S0Converter(parameterObject));
127 Converter converter = VALUE_CONVERTERS.get(unitObject);
128 if (converter != null) {
129 converters.put(channel.getUID(), converter);
135 // Initialize inversion converter
136 if (Boolean.TRUE.equals(invertState) || Boolean.TRUE.equals(invertUpDown)) {
137 converters.put(channel.getUID(), INVERSION_CONVERTER);
142 // module is assumed as online, when the corresponding Bridge (PckGatewayHandler) is online.
143 updateStatus(ThingStatus.ONLINE);
145 // trigger REFRESH commands for all linked Channels to start polling
146 getThing().getChannels().forEach(channel -> {
147 if (isLinked(channel.getUID())) {
148 channelLinked(channel.getUID());
151 } catch (LcnException e) {
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
157 * Triggers requesting the firmware version of the LCN module. The message also contains the serial number.
159 * @throws LcnException when the handler is not initialized
161 protected void requestFirmwareVersionAndSerialNumberIfNotSet() throws LcnException {
162 if (readFirmwareVersionFromProperty().isEmpty()) {
163 LcnAddrMod localModuleAddress = moduleAddress;
164 if (localModuleAddress != null) {
165 getPckGatewayHandler().getModInfo(localModuleAddress).requestFirmwareVersion();
170 private Optional<Integer> readFirmwareVersionFromProperty() {
171 String prop = getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION);
173 if (prop == null || prop.length() != FIRMWARE_VERSION_LENGTH) {
174 return Optional.empty();
178 return Optional.of(Integer.parseInt(prop, 16));
179 } catch (NumberFormatException e) {
180 logger.warn("{}: Serial number property invalid", moduleAddress);
181 return Optional.empty();
186 public void handleCommand(ChannelUID channelUid, Command command) {
188 String groupId = channelUid.getGroupId();
190 if (!channelUid.isInGroup()) {
194 if (groupId == null) {
195 throw new LcnException("Group ID is null");
198 LcnChannelGroup channelGroup = LcnChannelGroup.valueOf(groupId.toUpperCase());
199 AbstractLcnModuleSubHandler subHandler = subHandlers.get(channelGroup);
201 if (subHandler == null) {
202 throw new LcnException("Sub Handler not found for: " + channelGroup);
205 Optional<Integer> number = channelUidToChannelNumber(channelUid, channelGroup);
207 if (command instanceof RefreshType) {
208 number.ifPresent(n -> subHandler.handleRefresh(channelGroup, n));
209 subHandler.handleRefresh(channelUid.getIdWithoutGroup());
210 } else if (command instanceof OnOffType) {
211 subHandler.handleCommandOnOff((OnOffType) command, channelGroup, number.get());
212 } else if (command instanceof DimmerOutputCommand) {
213 subHandler.handleCommandDimmerOutput((DimmerOutputCommand) command, number.get());
214 } else if (command instanceof PercentType && number.isPresent()) {
215 subHandler.handleCommandPercent((PercentType) command, channelGroup, number.get());
216 } else if (command instanceof HSBType) {
217 subHandler.handleCommandHsb((HSBType) command, channelUid.getIdWithoutGroup());
218 } else if (command instanceof PercentType) {
219 subHandler.handleCommandPercent((PercentType) command, channelGroup, channelUid.getIdWithoutGroup());
220 } else if (command instanceof StringType) {
221 subHandler.handleCommandString((StringType) command, number.orElse(0));
222 } else if (command instanceof DecimalType) {
223 DecimalType decimalType = (DecimalType) command;
224 DecimalType nativeValue = getConverter(channelUid).onCommandFromItem(decimalType.doubleValue());
225 subHandler.handleCommandDecimal(nativeValue, channelGroup, number.get());
226 } else if (command instanceof QuantityType) {
227 QuantityType<?> quantityType = (QuantityType<?>) command;
228 DecimalType nativeValue = getConverter(channelUid).onCommandFromItem(quantityType);
229 subHandler.handleCommandDecimal(nativeValue, channelGroup, number.get());
230 } else if (command instanceof UpDownType) {
231 Channel channel = thing.getChannel(channelUid);
232 if (channel != null) {
233 Object invertConfig = channel.getConfiguration().get("invertUpDown");
234 boolean invertUpDown = invertConfig instanceof Boolean && (boolean) invertConfig;
235 subHandler.handleCommandUpDown((UpDownType) command, channelGroup, number.get(), invertUpDown);
237 } else if (command instanceof StopMoveType) {
238 subHandler.handleCommandStopMove((StopMoveType) command, channelGroup, number.get());
240 throw new LcnException("Unsupported command type");
242 } catch (IllegalArgumentException | NoSuchElementException | LcnException e) {
243 logger.warn("{}: Failed to handle command {}: {}", channelUid, command.getClass().getSimpleName(),
248 @NonNullByDefault({}) // getOrDefault()
249 private Converter getConverter(ChannelUID channelUid) {
250 return converters.getOrDefault(channelUid, Converters.IDENTITY);
254 * Invoked when a PCK messages arrives from the PCK gateway
256 * @param pck the message without line termination
258 public void handleStatusMessage(String pck) {
259 subHandlers.values().forEach(h -> h.tryParse(pck));
261 metadataSubHandlers.forEach(h -> h.tryParse(pck));
264 private Optional<Integer> channelUidToChannelNumber(ChannelUID channelUid, LcnChannelGroup channelGroup)
265 throws LcnException {
267 int number = Integer.parseInt(channelUid.getIdWithoutGroup()) - 1;
269 if (!channelGroup.isValidId(number)) {
270 throw new LcnException("Out of range: " + number);
272 return Optional.of(number);
273 } catch (NumberFormatException e) {
274 return Optional.empty();
278 private PckGatewayHandler getPckGatewayHandler() throws LcnException {
279 Bridge bridge = getBridge();
280 if (bridge == null) {
281 throw new LcnException("No LCN-PCK gateway configured for this module");
284 PckGatewayHandler handler = (PckGatewayHandler) bridge.getHandler();
285 if (handler == null) {
286 throw new LcnException("Could not get PckGatewayHandler");
292 * Queues a PCK string for sending.
294 * @param command without the address part
295 * @throws LcnException when the module address is unknown
297 public void sendPck(String command) throws LcnException {
298 getPckGatewayHandler().queue(getCommandAddress(), true, command);
302 * Queues a PCK byte buffer for sending.
304 * @param command without the address part
305 * @throws LcnException when the module address is unknown
307 public void sendPck(byte[] command) throws LcnException {
308 getPckGatewayHandler().queue(getCommandAddress(), true, command);
312 * Gets the address, which shall be used when sending commands into the LCN bus. This can also be a group address.
314 * @return the address to send to
315 * @throws LcnException when the address is unknown
317 protected LcnAddr getCommandAddress() throws LcnException, LcnException {
318 LcnAddr localAddress = moduleAddress;
319 if (localAddress == null) {
320 throw new LcnException("Module address not set");
326 * Invoked when an update for this LCN module should be fired to openHAB.
328 * @param channelGroup the Channel to update
329 * @param channelId the ID within the Channel to update
330 * @param state the new state
332 public void updateChannel(LcnChannelGroup channelGroup, String channelId, State state) {
333 ChannelUID channelUid = createChannelUid(channelGroup, channelId);
334 Converter converter = converters.get(channelUid);
336 State convertedState = state;
337 if (converter != null) {
339 convertedState = converter.onStateUpdateFromHandler(state);
340 } catch (LcnException e) {
341 logger.warn("{}: {}{}: Value conversion failed: {}", moduleAddress, channelGroup, channelId,
346 updateState(channelUid, convertedState);
350 * Updates the LCN module's serial number property.
352 * @param serialNumber the new serial number
354 public void updateSerialNumberProperty(String serialNumber) {
355 updateProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
359 * Updates the LCN module's serial number property.
361 * @param serialNumber the new serial number
363 public void updateFirmwareVersionProperty(String firmwareVersion) {
364 updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
368 * Invoked when a trigger for this LCN module should be fired to openHAB.
370 * @param channelGroup the Channel to update
371 * @param channelId the ID within the Channel to update
372 * @param event the event used to trigger
374 public void triggerChannel(LcnChannelGroup channelGroup, String channelId, String event) {
375 triggerChannel(createChannelUid(channelGroup, channelId), event);
378 private ChannelUID createChannelUid(LcnChannelGroup channelGroup, String channelId) {
379 return new ChannelUID(thing.getUID(), channelGroup.name().toLowerCase() + "#" + channelId);
383 * Checks the LCN module address against the own.
385 * @param physicalSegmentId which is 0 if it is the local segment
387 * @return true, if the given address matches the own address
389 public boolean isMyAddress(String physicalSegmentId, String moduleId) {
391 return new LcnAddrMod(getPckGatewayHandler().toLogicalSegmentId(Integer.parseInt(physicalSegmentId)),
392 Integer.parseInt(moduleId)).equals(getStatusMessageAddress());
393 } catch (LcnException e) {
399 public Collection<Class<? extends ThingHandlerService>> getServices() {
400 return Collections.singleton(LcnModuleActions.class);
404 * Invoked when an Ack from this module has been received.
406 public void onAckRceived() {
408 Connection connection = getPckGatewayHandler().getConnection();
409 LcnAddrMod localModuleAddress = moduleAddress;
410 if (connection != null && localModuleAddress != null) {
411 getPckGatewayHandler().getModInfo(localModuleAddress).onAck(LcnBindingConstants.CODE_ACK, connection,
412 getPckGatewayHandler().getTimeoutMs(), System.currentTimeMillis());
414 } catch (LcnException e) {
415 logger.warn("Connection or module address not set");
420 * Gets the address the handler shall react to, when a status message from this address is processed.
422 * @return the address for status messages
424 public LcnAddrMod getStatusMessageAddress() {
425 LcnAddrMod localmoduleAddress = moduleAddress;
426 if (localmoduleAddress != null) {
427 return localmoduleAddress;
429 return new LcnAddrMod(0, 0);
434 public void dispose() {
435 metadataSubHandlers.clear();