]> git.basschouten.com Git - openhab-addons.git/blob
78b153f6697073a4f36efcf54472e46ff0a25a88
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.tacmi.internal.coe;
14
15 import static org.openhab.binding.tacmi.internal.TACmiBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.InetAddress;
19 import java.net.UnknownHostException;
20 import java.util.HashMap;
21 import java.util.Map;
22
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;
47
48 /**
49  * The {@link TACmiHandler} is responsible for handling commands, which are sent
50  * to one of the channels.
51  *
52  * @author Timo Wendt - Initial contribution
53  * @author Christian Niessner - Ported to OpenHAB2
54  */
55 @NonNullByDefault
56 public class TACmiHandler extends BaseThingHandler {
57
58     private final Logger logger = LoggerFactory.getLogger(TACmiHandler.class);
59
60     private final Map<PodIdentifier, PodData> podDatas = new HashMap<>();
61     private final Map<ChannelUID, TACmiChannelConfiguration> channelConfigByUID = new HashMap<>();
62
63     private @Nullable TACmiCoEBridgeHandler bridge;
64     private long lastMessageRecvTS; // last received message timestamp
65
66     /**
67      * the C.M.I.'s address
68      */
69     private @Nullable InetAddress cmiAddress;
70
71     /**
72      * the CoE CAN-Node we representing
73      */
74     private int node;
75
76     public TACmiHandler(final Thing thing) {
77         super(thing);
78     }
79
80     @Override
81     public void initialize() {
82         updateStatus(ThingStatus.UNKNOWN);
83
84         scheduler.execute(this::initializeDetached);
85     }
86
87     private void initializeDetached() {
88         final TACmiConfiguration config = getConfigAs(TACmiConfiguration.class);
89
90         if (config.host == null) {
91             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No host configured!");
92             return;
93         }
94         try {
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 + "'");
100             return;
101         }
102
103         this.node = config.node;
104
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);
126             if (outgoing) {
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
131                 // on TA side...
132                 boolean set = false;
133                 if (analog) {
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();
139                         @Nullable
140                         Message message = pd.message;
141                         if (message != null) {
142                             // shouldn't happen, just in case...
143                             message.setValue(outputIdx, (short) val, measureType.ordinal());
144                             set = true;
145                         }
146                     }
147                 } else {
148                     // digital...
149                     TACmiChannelConfigurationDigital ca = (TACmiChannelConfigurationDigital) channelConfig;
150                     Boolean initialValue = ca.initialValue;
151                     if (initialValue != null) {
152                         @Nullable
153                         DigitalMessage message = (DigitalMessage) pd.message;
154                         if (message != null) {
155                             // shouldn't happen, just in case...
156                             message.setPortState(outputIdx, initialValue);
157                             set = true;
158                         }
159                     }
160                 }
161                 podDataOutgoing.channeUIDs[outputIdx] = chann.getUID();
162                 podDataOutgoing.initialized[outputIdx] = set;
163             }
164         }
165
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!");
170             return;
171         }
172         bridge.registerCMI(this);
173         this.bridge = bridge;
174
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);
178     }
179
180     private PodData getPodData(final PodIdentifier pi) {
181         PodData pd = this.podDatas.get(pi);
182         if (pd == null) {
183             if (pi.outgoing) {
184                 pd = new PodDataOutgoing(pi, (byte) this.node);
185             } else {
186                 pd = new PodData(pi, (byte) this.node);
187             }
188             this.podDatas.put(pi, pd);
189         }
190         return pd;
191     }
192
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);
200         }
201         return (byte) (outputIdx == 0 ? 0 : 9);
202     }
203
204     /**
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...
207      *
208      * @param output
209      * @param analog
210      * @return
211      */
212     private int getOutputIndex(int output, boolean analog) {
213         int outputIdx = output - 1;
214         if (analog) {
215             outputIdx %= 4;
216         } else {
217             outputIdx %= 16;
218         }
219         return outputIdx;
220     }
221
222     @Override
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);
227             return;
228         }
229         final Channel channel = thing.getChannel(channelUID);
230         if (channel == null) {
231             return;
232         }
233
234         if (command instanceof RefreshType) {
235             // we try to find the last known state from cache and return it.
236             MessageType mt;
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;
241             } else {
242                 logger.debug("Recived unhandled command '{}' on unknown Channel type {} ", command, channelUID);
243                 return;
244             }
245             final byte podId = getPodId(mt, channelConfig.output);
246             PodData pd = getPodData(new PodIdentifier(mt, podId, true));
247             @Nullable
248             Message message = pd.message;
249             if (message == null) {
250                 // no data received yet from the C.M.I. and persistence might be disabled..
251                 return;
252             }
253             if (mt == MessageType.ANALOG) {
254                 final AnalogValue value = ((AnalogMessage) message).getAnalogValue(channelConfig.output);
255                 updateState(channel.getUID(), new DecimalType(value.value));
256             } else {
257                 final boolean state = ((DigitalMessage) message).getPortState(channelConfig.output);
258                 updateState(channel.getUID(), state ? OnOffType.ON : OnOffType.OFF);
259             }
260             return;
261         }
262         boolean analog;
263         MessageType mt;
264         if ((TACmiBindingConstants.CHANNEL_TYPE_COE_DIGITAL_OUT_UID.equals(channel.getChannelTypeUID()))) {
265             mt = MessageType.DIGITAL;
266             analog = false;
267         } else if ((TACmiBindingConstants.CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(channel.getChannelTypeUID()))) {
268             mt = MessageType.ANALOG;
269             analog = true;
270         } else {
271             logger.debug("Recived unhandled command '{}' on Channel {} ", command, channelUID);
272             return;
273         }
274
275         final byte podId = getPodId(mt, channelConfig.output);
276         PodDataOutgoing podDataOutgoing = (PodDataOutgoing) getPodData(new PodIdentifier(mt, podId, true));
277         @Nullable
278         Message message = podDataOutgoing.message;
279         if (message == null) {
280             logger.error("Internal error - BUG - no outgoing message for command '{}' on Channel {} ", command,
281                     channelUID);
282             return;
283         }
284         int outputIdx = getOutputIndex(channelConfig.output, analog);
285         boolean modified;
286         if (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());
292         } else {
293             final boolean state = OnOffType.ON.equals(command) ? true : false;
294             modified = ((DigitalMessage) message).setPortState(outputIdx, state);
295         }
296         podDataOutgoing.initialized[outputIdx] = true;
297         if (modified) {
298             try {
299                 @Nullable
300                 final TACmiCoEBridgeHandler br = this.bridge;
301                 @Nullable
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();
306                 }
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());
313             }
314         }
315     }
316
317     @Override
318     public void dispose() {
319         final TACmiCoEBridgeHandler br = this.bridge;
320         if (br != null) {
321             br.unregisterCMI(this);
322         }
323         super.dispose();
324     }
325
326     public boolean isFor(final InetAddress remoteAddress, final int node) {
327         @Nullable
328         final InetAddress cmia = this.cmiAddress;
329         if (cmia == null) {
330             return false;
331         }
332         return this.node == node && cmia.equals(remoteAddress);
333     }
334
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);
341         }
342         this.lastMessageRecvTS = System.currentTimeMillis();
343         for (final Channel channel : thing.getChannels()) {
344             if (!(channelType.equals(channel.getChannelTypeUID()))) {
345                 continue;
346             }
347             final int output = ((Number) channel.getConfiguration().get(TACmiBindingConstants.CHANNEL_CONFIG_OUTPUT))
348                     .intValue();
349             if (!message.hasPortnumber(output)) {
350                 continue;
351             }
352
353             if (message.getType() == MessageType.ANALOG) {
354                 final AnalogValue value = ((AnalogMessage) message).getAnalogValue(output);
355                 updateState(channel.getUID(), new DecimalType(value.value));
356             } else {
357                 final boolean state = ((DigitalMessage) message).getPortState(output);
358                 updateState(channel.getUID(), state ? OnOffType.ON : OnOffType.OFF);
359             }
360         }
361     }
362
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");
369         }
370         for (final PodData pd : this.podDatas.values()) {
371             if (!(pd instanceof PodDataOutgoing)) {
372                 continue;
373             }
374             PodDataOutgoing podDataOutgoing = (PodDataOutgoing) pd;
375             @Nullable
376             Message message = pd.message;
377             if (message != null && refTs - podDataOutgoing.lastSent > 300000) {
378                 // re-send every 300 seconds...
379                 @Nullable
380                 final InetAddress cmia = this.cmiAddress;
381                 if (podDataOutgoing.isAllValuesInitialized()) {
382                     try {
383                         @Nullable
384                         final TACmiCoEBridgeHandler br = this.bridge;
385                         if (br != null && cmia != null) {
386                             br.sendData(message.getRaw(), cmia);
387                             podDataOutgoing.lastSent = System.currentTimeMillis();
388                         }
389                     } catch (final IOException e) {
390                         logger.warn("Error sending message to C.M.I.: {}: {}", e.getClass().getName(), e.getMessage());
391                     }
392                 } else {
393                     // pod is not entirely initialized - log warn for user but also set lastSent to prevent flooding of
394                     // logs...
395                     if (cmia != null) {
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());
399                     }
400                     podDataOutgoing.lastSent = System.currentTimeMillis();
401                 }
402             }
403         }
404     }
405 }