2 * Copyright (c) 2010-2020 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.tacmi.internal.coe;
15 import static org.openhab.binding.tacmi.internal.TACmiBindingConstants.*;
17 import java.io.IOException;
18 import java.net.InetAddress;
19 import java.net.UnknownHostException;
20 import java.util.HashMap;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.tacmi.internal.TACmiBindingConstants;
26 import org.openhab.binding.tacmi.internal.TACmiMeasureType;
27 import org.openhab.binding.tacmi.internal.message.AnalogMessage;
28 import org.openhab.binding.tacmi.internal.message.AnalogValue;
29 import org.openhab.binding.tacmi.internal.message.DigitalMessage;
30 import org.openhab.binding.tacmi.internal.message.Message;
31 import org.openhab.binding.tacmi.internal.message.MessageType;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.thing.Bridge;
35 import org.openhab.core.thing.Channel;
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.binding.BaseThingHandler;
41 import org.openhab.core.thing.type.ChannelTypeUID;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.State;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * The {@link TACmiHandler} is responsible for handling commands, which are sent
50 * to one of the channels.
52 * @author Timo Wendt - Initial contribution
53 * @author Christian Niessner - Ported to OpenHAB2
56 public class TACmiHandler extends BaseThingHandler {
58 private final Logger logger = LoggerFactory.getLogger(TACmiHandler.class);
60 private final Map<PodIdentifier, PodData> podDatas = new HashMap<>();
61 private final Map<ChannelUID, TACmiChannelConfiguration> channelConfigByUID = new HashMap<>();
63 private @Nullable TACmiCoEBridgeHandler bridge;
64 private long lastMessageRecvTS; // last received message timestamp
67 * the C.M.I.'s address
69 private @Nullable InetAddress cmiAddress;
72 * the CoE CAN-Node we representing
76 public TACmiHandler(final Thing thing) {
81 public void initialize() {
82 updateStatus(ThingStatus.UNKNOWN);
84 scheduler.execute(this::initializeDetached);
87 private void initializeDetached() {
88 final TACmiConfiguration config = getConfigAs(TACmiConfiguration.class);
90 if (config.host == null) {
91 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No host configured!");
95 cmiAddress = InetAddress.getByName(config.host);
96 } catch (final UnknownHostException e1) {
97 // message logged by framework via updateStatus
98 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
99 "Failed to get IP of CMI for '" + config.host + "'");
103 this.node = config.node;
105 // initialize lookup maps...
106 this.channelConfigByUID.clear();
107 this.podDatas.clear();
108 for (final Channel chann : getThing().getChannels()) {
109 final ChannelTypeUID ct = chann.getChannelTypeUID();
110 final boolean analog = CHANNEL_TYPE_COE_ANALOG_IN_UID.equals(ct)
111 || CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(ct);
112 final boolean outgoing = CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(ct)
113 || CHANNEL_TYPE_COE_DIGITAL_OUT_UID.equals(ct);
114 // for the analog out channel we have the measurement type. for the input
115 // channel we take it from the C.M.I.
116 final Class<? extends TACmiChannelConfiguration> ccClass = CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(ct)
117 ? TACmiChannelConfigurationAnalog.class
118 : TACmiChannelConfigurationDigital.class;
119 final TACmiChannelConfiguration channelConfig = chann.getConfiguration().as(ccClass);
120 this.channelConfigByUID.put(chann.getUID(), channelConfig);
121 final MessageType messageType = analog ? MessageType.ANALOG : MessageType.DIGITAL;
122 final byte podId = this.getPodId(messageType, channelConfig.output);
123 final PodIdentifier pi = new PodIdentifier(messageType, podId, outgoing);
124 // initialize podData
125 PodData pd = this.getPodData(pi);
127 int outputIdx = getOutputIndex(channelConfig.output, analog);
128 PodDataOutgoing podDataOutgoing = (PodDataOutgoing) pd;
129 // we have to track value state for all outgoing channels to ensure we have valid values for all
130 // channels in use before we send a message to the C.M.I. otherwise it could trigger some strange things
134 TACmiChannelConfigurationAnalog ca = (TACmiChannelConfigurationAnalog) channelConfig;
135 Double initialValue = ca.initialValue;
136 if (initialValue != null) {
137 final TACmiMeasureType measureType = TACmiMeasureType.values()[ca.type];
138 final double val = initialValue.doubleValue() * measureType.getOffset();
140 Message message = pd.message;
141 if (message != null) {
142 // shouldn't happen, just in case...
143 message.setValue(outputIdx, (short) val, measureType.ordinal());
149 TACmiChannelConfigurationDigital ca = (TACmiChannelConfigurationDigital) channelConfig;
150 Boolean initialValue = ca.initialValue;
151 if (initialValue != null) {
153 DigitalMessage message = (DigitalMessage) pd.message;
154 if (message != null) {
155 // shouldn't happen, just in case...
156 message.setPortState(outputIdx, initialValue);
161 podDataOutgoing.channeUIDs[outputIdx] = chann.getUID();
162 podDataOutgoing.initialized[outputIdx] = set;
166 final Bridge br = getBridge();
167 final TACmiCoEBridgeHandler bridge = br == null ? null : (TACmiCoEBridgeHandler) br.getHandler();
168 if (bridge == null) {
169 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "No Bridge configured!");
172 bridge.registerCMI(this);
173 this.bridge = bridge;
175 // we set it to UNKNOWN. Will be set to ONLIN~E as soon as we start receiving
176 // data or to OFFLINE when no data is received within 900 seconds.
177 updateStatus(ThingStatus.UNKNOWN);
180 private PodData getPodData(final PodIdentifier pi) {
181 PodData pd = this.podDatas.get(pi);
184 pd = new PodDataOutgoing(pi, (byte) this.node);
186 pd = new PodData(pi, (byte) this.node);
188 this.podDatas.put(pi, pd);
193 private byte getPodId(final MessageType messageType, final int output) {
194 assert output >= 1 && output <= 32; // range 1-32
195 // pod ID's: 0 & 9 for digital states, 1-8 for analog values
196 boolean analog = messageType == MessageType.ANALOG;
197 int outputIdx = getOutputIndex(output, analog);
198 if (messageType == MessageType.ANALOG) {
199 return (byte) (outputIdx + 1);
201 return (byte) (outputIdx == 0 ? 0 : 9);
205 * calculates output index position within the POD.
206 * TA output index starts with 1, our arrays starts at 0. We also have to keep the pod size in mind...
212 private int getOutputIndex(int output, boolean analog) {
213 int outputIdx = output - 1;
223 public void handleCommand(final ChannelUID channelUID, final Command command) {
224 final TACmiChannelConfiguration channelConfig = this.channelConfigByUID.get(channelUID);
225 if (channelConfig == null) {
226 logger.debug("Recived unhandled command '{}' for unknown Channel {} ", command, channelUID);
229 final Channel channel = thing.getChannel(channelUID);
230 if (channel == null) {
234 if (command instanceof RefreshType) {
235 // we try to find the last known state from cache and return it.
237 if ((TACmiBindingConstants.CHANNEL_TYPE_COE_DIGITAL_IN_UID.equals(channel.getChannelTypeUID()))) {
238 mt = MessageType.DIGITAL;
239 } else if ((TACmiBindingConstants.CHANNEL_TYPE_COE_ANALOG_IN_UID.equals(channel.getChannelTypeUID()))) {
240 mt = MessageType.ANALOG;
242 logger.debug("Recived unhandled command '{}' on unknown Channel type {} ", command, channelUID);
245 final byte podId = getPodId(mt, channelConfig.output);
246 PodData pd = getPodData(new PodIdentifier(mt, podId, true));
248 Message message = pd.message;
249 if (message == null) {
250 // no data received yet from the C.M.I. and persistence might be disabled..
253 if (mt == MessageType.ANALOG) {
254 final AnalogValue value = ((AnalogMessage) message).getAnalogValue(channelConfig.output);
255 updateState(channel.getUID(), new DecimalType(value.value));
257 final boolean state = ((DigitalMessage) message).getPortState(channelConfig.output);
258 updateState(channel.getUID(), state ? OnOffType.ON : OnOffType.OFF);
264 if ((TACmiBindingConstants.CHANNEL_TYPE_COE_DIGITAL_OUT_UID.equals(channel.getChannelTypeUID()))) {
265 mt = MessageType.DIGITAL;
267 } else if ((TACmiBindingConstants.CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(channel.getChannelTypeUID()))) {
268 mt = MessageType.ANALOG;
271 logger.debug("Recived unhandled command '{}' on Channel {} ", command, channelUID);
275 final byte podId = getPodId(mt, channelConfig.output);
276 PodDataOutgoing podDataOutgoing = (PodDataOutgoing) getPodData(new PodIdentifier(mt, podId, true));
278 Message message = podDataOutgoing.message;
279 if (message == null) {
280 logger.error("Internal error - BUG - no outgoing message for command '{}' on Channel {} ", command,
284 int outputIdx = getOutputIndex(channelConfig.output, analog);
287 final TACmiMeasureType measureType = TACmiMeasureType
288 .values()[((TACmiChannelConfigurationAnalog) channelConfig).type];
289 final DecimalType dt = (DecimalType) command;
290 final double val = dt.doubleValue() * measureType.getOffset();
291 modified = message.setValue(outputIdx, (short) val, measureType.ordinal());
293 final boolean state = OnOffType.ON.equals(command) ? true : false;
294 modified = ((DigitalMessage) message).setPortState(outputIdx, state);
296 podDataOutgoing.initialized[outputIdx] = true;
300 final TACmiCoEBridgeHandler br = this.bridge;
302 final InetAddress cmia = this.cmiAddress;
303 if (br != null && cmia != null && podDataOutgoing.isAllValuesInitialized()) {
304 br.sendData(message.getRaw(), cmia);
305 podDataOutgoing.lastSent = System.currentTimeMillis();
307 // we also update the local state after we successfully sent out the command
308 // there is no feedback from the C.M.I. so we only could assume the message has been received when we
309 // were able to send it...
310 updateState(channel.getUID(), (State) command);
311 } catch (final IOException e) {
312 logger.warn("Error sending message: {}: {}", e.getClass().getName(), e.getMessage());
318 public void dispose() {
319 final TACmiCoEBridgeHandler br = this.bridge;
321 br.unregisterCMI(this);
326 public boolean isFor(final InetAddress remoteAddress, final int node) {
328 final InetAddress cmia = this.cmiAddress;
332 return this.node == node && cmia.equals(remoteAddress);
335 public void handleCoE(final Message message) {
336 final ChannelTypeUID channelType = message.getType() == MessageType.DIGITAL
337 ? TACmiBindingConstants.CHANNEL_TYPE_COE_DIGITAL_IN_UID
338 : TACmiBindingConstants.CHANNEL_TYPE_COE_ANALOG_IN_UID;
339 if (getThing().getStatus() != ThingStatus.ONLINE) {
340 updateStatus(ThingStatus.ONLINE);
342 this.lastMessageRecvTS = System.currentTimeMillis();
343 for (final Channel channel : thing.getChannels()) {
344 if (!(channelType.equals(channel.getChannelTypeUID()))) {
347 final int output = ((Number) channel.getConfiguration().get(TACmiBindingConstants.CHANNEL_CONFIG_OUTPUT))
349 if (!message.hasPortnumber(output)) {
353 if (message.getType() == MessageType.ANALOG) {
354 final AnalogValue value = ((AnalogMessage) message).getAnalogValue(output);
355 updateState(channel.getUID(), new DecimalType(value.value));
357 final boolean state = ((DigitalMessage) message).getPortState(output);
358 updateState(channel.getUID(), state ? OnOffType.ON : OnOffType.OFF);
363 public void checkForTimeout() {
364 final long refTs = System.currentTimeMillis();
365 if (refTs - this.lastMessageRecvTS > 900000 && getThing().getStatus() != ThingStatus.OFFLINE) {
366 // no data received for 900 seconds - set thing status to offline..
367 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
368 "No update from C.M.I. for 15 min");
370 for (final PodData pd : this.podDatas.values()) {
371 if (!(pd instanceof PodDataOutgoing)) {
374 PodDataOutgoing podDataOutgoing = (PodDataOutgoing) pd;
376 Message message = pd.message;
377 if (message != null && refTs - podDataOutgoing.lastSent > 300000) {
378 // re-send every 300 seconds...
380 final InetAddress cmia = this.cmiAddress;
381 if (podDataOutgoing.isAllValuesInitialized()) {
384 final TACmiCoEBridgeHandler br = this.bridge;
385 if (br != null && cmia != null) {
386 br.sendData(message.getRaw(), cmia);
387 podDataOutgoing.lastSent = System.currentTimeMillis();
389 } catch (final IOException e) {
390 logger.warn("Error sending message to C.M.I.: {}: {}", e.getClass().getName(), e.getMessage());
393 // pod is not entirely initialized - log warn for user but also set lastSent to prevent flooding of
396 logger.warn("Sending data to {} {}.{} is blocked as we don't have valid values for channels {}",
397 cmia.getHostAddress(), this.node, podDataOutgoing.podId,
398 podDataOutgoing.getUninitializedChannelNames());
400 podDataOutgoing.lastSent = System.currentTimeMillis();