]> git.basschouten.com Git - openhab-addons.git/blob
0128c90b52f2dafa588f01b967128bc37286b93f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.library.types.QuantityType;
35 import org.openhab.core.library.unit.SIUnits;
36 import org.openhab.core.library.unit.Units;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.Channel;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.thing.type.ChannelTypeUID;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.openhab.core.types.State;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * The {@link TACmiHandler} is responsible for handling commands, which are sent
53  * to one of the channels.
54  *
55  * @author Timo Wendt - Initial contribution
56  * @author Christian Niessner - Ported to OpenHAB2
57  */
58 @NonNullByDefault
59 public class TACmiHandler extends BaseThingHandler {
60
61     private final Logger logger = LoggerFactory.getLogger(TACmiHandler.class);
62
63     private final Map<PodIdentifier, PodData> podDatas = new HashMap<>();
64     private final Map<ChannelUID, TACmiChannelConfiguration> channelConfigByUID = new HashMap<>();
65
66     private @Nullable TACmiCoEBridgeHandler bridge;
67     private long lastMessageRecvTS; // last received message timestamp
68
69     /**
70      * the C.M.I.'s address
71      */
72     private @Nullable InetAddress cmiAddress;
73
74     /**
75      * the CoE CAN-Node we representing
76      */
77     private int node;
78
79     public TACmiHandler(final Thing thing) {
80         super(thing);
81     }
82
83     @Override
84     public void initialize() {
85         updateStatus(ThingStatus.UNKNOWN);
86
87         scheduler.execute(this::initializeDetached);
88     }
89
90     private void initializeDetached() {
91         final TACmiConfiguration config = getConfigAs(TACmiConfiguration.class);
92
93         if (config.host == null) {
94             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No host configured!");
95             return;
96         }
97         try {
98             cmiAddress = InetAddress.getByName(config.host);
99         } catch (final UnknownHostException e1) {
100             // message logged by framework via updateStatus
101             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
102                     "Failed to get IP of CMI for '" + config.host + "'");
103             return;
104         }
105
106         this.node = config.node;
107
108         // initialize lookup maps...
109         this.channelConfigByUID.clear();
110         this.podDatas.clear();
111         for (final Channel chann : getThing().getChannels()) {
112             final ChannelTypeUID ct = chann.getChannelTypeUID();
113             final boolean analog = CHANNEL_TYPE_COE_ANALOG_IN_UID.equals(ct)
114                     || CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(ct);
115             final boolean outgoing = CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(ct)
116                     || CHANNEL_TYPE_COE_DIGITAL_OUT_UID.equals(ct);
117             // for the analog out channel we have the measurement type. for the input
118             // channel we take it from the C.M.I.
119             final Class<? extends TACmiChannelConfiguration> ccClass = CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(ct)
120                     ? TACmiChannelConfigurationAnalog.class
121                     : TACmiChannelConfigurationDigital.class;
122             final TACmiChannelConfiguration channelConfig = chann.getConfiguration().as(ccClass);
123             this.channelConfigByUID.put(chann.getUID(), channelConfig);
124             final MessageType messageType = analog ? MessageType.ANALOG : MessageType.DIGITAL;
125             final byte podId = this.getPodId(messageType, channelConfig.output);
126             final PodIdentifier pi = new PodIdentifier(messageType, podId, outgoing);
127             // initialize podData
128             PodData pd = this.getPodData(pi);
129             if (outgoing) {
130                 int outputIdx = getOutputIndex(channelConfig.output, analog);
131                 PodDataOutgoing podDataOutgoing = (PodDataOutgoing) pd;
132                 // we have to track value state for all outgoing channels to ensure we have valid values for all
133                 // channels in use before we send a message to the C.M.I. otherwise it could trigger some strange things
134                 // on TA side...
135                 boolean set = false;
136                 if (analog) {
137                     TACmiChannelConfigurationAnalog ca = (TACmiChannelConfigurationAnalog) channelConfig;
138                     Double initialValue = ca.initialValue;
139                     if (initialValue != null) {
140                         final TACmiMeasureType measureType = TACmiMeasureType.values()[ca.type];
141                         final double val = initialValue.doubleValue() * measureType.getOffset();
142                         @Nullable
143                         Message message = pd.message;
144                         if (message != null) {
145                             // shouldn't happen, just in case...
146                             message.setValue(outputIdx, (short) val, measureType.ordinal());
147                             set = true;
148                         }
149                     }
150                 } else {
151                     // digital...
152                     TACmiChannelConfigurationDigital ca = (TACmiChannelConfigurationDigital) channelConfig;
153                     Boolean initialValue = ca.initialValue;
154                     if (initialValue != null) {
155                         @Nullable
156                         DigitalMessage message = (DigitalMessage) pd.message;
157                         if (message != null) {
158                             // shouldn't happen, just in case...
159                             message.setPortState(outputIdx, initialValue);
160                             set = true;
161                         }
162                     }
163                 }
164                 podDataOutgoing.channeUIDs[outputIdx] = chann.getUID();
165                 podDataOutgoing.initialized[outputIdx] = set;
166             }
167         }
168
169         final Bridge br = getBridge();
170         final TACmiCoEBridgeHandler bridge = br == null ? null : (TACmiCoEBridgeHandler) br.getHandler();
171         if (bridge == null) {
172             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "No Bridge configured!");
173             return;
174         }
175         bridge.registerCMI(this);
176         this.bridge = bridge;
177
178         // we set it to UNKNOWN. Will be set to ONLIN~E as soon as we start receiving
179         // data or to OFFLINE when no data is received within 900 seconds.
180         updateStatus(ThingStatus.UNKNOWN);
181     }
182
183     private PodData getPodData(final PodIdentifier pi) {
184         PodData pd = this.podDatas.get(pi);
185         if (pd == null) {
186             if (pi.outgoing) {
187                 pd = new PodDataOutgoing(pi, (byte) this.node);
188             } else {
189                 pd = new PodData(pi, (byte) this.node);
190             }
191             this.podDatas.put(pi, pd);
192         }
193         return pd;
194     }
195
196     private byte getPodId(final MessageType messageType, final int output) {
197         assert output >= 1 && output <= 32; // range 1-32
198         // pod ID's: 0 & 9 for digital states, 1-8 for analog values
199         boolean analog = messageType == MessageType.ANALOG;
200         int outputIdx = getOutputIndex(output, analog);
201         if (messageType == MessageType.ANALOG) {
202             return (byte) (outputIdx + 1);
203         }
204         return (byte) (outputIdx == 0 ? 0 : 9);
205     }
206
207     /**
208      * calculates output index position within the POD.
209      * TA output index starts with 1, our arrays starts at 0. We also have to keep the pod size in mind...
210      *
211      * @param output
212      * @param analog
213      * @return
214      */
215     private int getOutputIndex(int output, boolean analog) {
216         int outputIdx = output - 1;
217         if (analog) {
218             outputIdx %= 4;
219         } else {
220             outputIdx %= 16;
221         }
222         return outputIdx;
223     }
224
225     @Override
226     public void handleCommand(final ChannelUID channelUID, final Command command) {
227         final TACmiChannelConfiguration channelConfig = this.channelConfigByUID.get(channelUID);
228         if (channelConfig == null) {
229             logger.debug("Recived unhandled command '{}' for unknown Channel {} ", command, channelUID);
230             return;
231         }
232         final Channel channel = thing.getChannel(channelUID);
233         if (channel == null) {
234             return;
235         }
236
237         if (command instanceof RefreshType) {
238             // we try to find the last known state from cache and return it.
239             MessageType mt;
240             if ((TACmiBindingConstants.CHANNEL_TYPE_COE_DIGITAL_IN_UID.equals(channel.getChannelTypeUID()))) {
241                 mt = MessageType.DIGITAL;
242             } else if ((TACmiBindingConstants.CHANNEL_TYPE_COE_ANALOG_IN_UID.equals(channel.getChannelTypeUID()))) {
243                 mt = MessageType.ANALOG;
244             } else {
245                 logger.debug("Recived unhandled command '{}' on unknown Channel type {} ", command, channelUID);
246                 return;
247             }
248             final byte podId = getPodId(mt, channelConfig.output);
249             PodData pd = getPodData(new PodIdentifier(mt, podId, true));
250             @Nullable
251             Message message = pd.message;
252             if (message == null) {
253                 // no data received yet from the C.M.I. and persistence might be disabled..
254                 return;
255             }
256             if (mt == MessageType.ANALOG) {
257                 final AnalogValue value = ((AnalogMessage) message).getAnalogValue(channelConfig.output);
258                 updateState(channel.getUID(), new DecimalType(value.value));
259             } else {
260                 final boolean state = ((DigitalMessage) message).getPortState(channelConfig.output);
261                 updateState(channel.getUID(), OnOffType.from(state));
262             }
263             return;
264         }
265         boolean analog;
266         MessageType mt;
267         if ((TACmiBindingConstants.CHANNEL_TYPE_COE_DIGITAL_OUT_UID.equals(channel.getChannelTypeUID()))) {
268             mt = MessageType.DIGITAL;
269             analog = false;
270         } else if ((TACmiBindingConstants.CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(channel.getChannelTypeUID()))) {
271             mt = MessageType.ANALOG;
272             analog = true;
273         } else {
274             logger.debug("Recived unhandled command '{}' on Channel {} ", command, channelUID);
275             return;
276         }
277
278         final byte podId = getPodId(mt, channelConfig.output);
279         PodDataOutgoing podDataOutgoing = (PodDataOutgoing) getPodData(new PodIdentifier(mt, podId, true));
280         @Nullable
281         Message message = podDataOutgoing.message;
282         if (message == null) {
283             logger.error("Internal error - BUG - no outgoing message for command '{}' on Channel {} ", command,
284                     channelUID);
285             return;
286         }
287         int outputIdx = getOutputIndex(channelConfig.output, analog);
288         boolean modified;
289         if (analog) {
290             final TACmiMeasureType measureType = TACmiMeasureType
291                     .values()[((TACmiChannelConfigurationAnalog) channelConfig).type];
292             final Number dt = (Number) command;
293             final double val = dt.doubleValue() * measureType.getOffset();
294             modified = message.setValue(outputIdx, (short) val, measureType.ordinal());
295         } else {
296             final boolean state = OnOffType.ON.equals(command) ? true : false;
297             modified = ((DigitalMessage) message).setPortState(outputIdx, state);
298         }
299         podDataOutgoing.initialized[outputIdx] = true;
300         if (modified) {
301             try {
302                 @Nullable
303                 final TACmiCoEBridgeHandler br = this.bridge;
304                 @Nullable
305                 final InetAddress cmia = this.cmiAddress;
306                 if (br != null && cmia != null && podDataOutgoing.isAllValuesInitialized()) {
307                     br.sendData(message.getRaw(), cmia);
308                     podDataOutgoing.lastSent = System.currentTimeMillis();
309                 }
310                 // we also update the local state after we successfully sent out the command
311                 // there is no feedback from the C.M.I. so we only could assume the message has been received when we
312                 // were able to send it...
313                 updateState(channel.getUID(), (State) command);
314             } catch (final IOException e) {
315                 logger.warn("Error sending message: {}: {}", e.getClass().getName(), e.getMessage());
316             }
317         }
318     }
319
320     @Override
321     public void dispose() {
322         final TACmiCoEBridgeHandler br = this.bridge;
323         if (br != null) {
324             br.unregisterCMI(this);
325         }
326         super.dispose();
327     }
328
329     public boolean isFor(final InetAddress remoteAddress, final int node) {
330         @Nullable
331         final InetAddress cmia = this.cmiAddress;
332         if (cmia == null) {
333             return false;
334         }
335         return this.node == node && cmia.equals(remoteAddress);
336     }
337
338     public void handleCoE(final Message message) {
339         final ChannelTypeUID channelType = message.getType() == MessageType.DIGITAL
340                 ? TACmiBindingConstants.CHANNEL_TYPE_COE_DIGITAL_IN_UID
341                 : TACmiBindingConstants.CHANNEL_TYPE_COE_ANALOG_IN_UID;
342         if (getThing().getStatus() != ThingStatus.ONLINE) {
343             updateStatus(ThingStatus.ONLINE);
344         }
345         this.lastMessageRecvTS = System.currentTimeMillis();
346         for (final Channel channel : thing.getChannels()) {
347             if (!(channelType.equals(channel.getChannelTypeUID()))) {
348                 continue;
349             }
350             final int output = ((Number) channel.getConfiguration().get(TACmiBindingConstants.CHANNEL_CONFIG_OUTPUT))
351                     .intValue();
352             if (!message.hasPortnumber(output)) {
353                 continue;
354             }
355
356             if (message.getType() == MessageType.ANALOG) {
357                 final AnalogValue value = ((AnalogMessage) message).getAnalogValue(output);
358                 State newState;
359                 switch (value.measureType) {
360                     case TEMPERATURE:
361                         newState = new QuantityType<>(value.value, SIUnits.CELSIUS);
362                         break;
363                     case KILOWATT:
364                         // TA uses kW, in OH we use W
365                         newState = new QuantityType<>(value.value * 1000, Units.WATT);
366                         break;
367                     case KILOWATTHOURS:
368                         newState = new QuantityType<>(value.value, Units.KILOWATT_HOUR);
369                         break;
370                     case MEGAWATTHOURS:
371                         newState = new QuantityType<>(value.value, Units.MEGAWATT_HOUR);
372                         break;
373                     case SECONDS:
374                         newState = new QuantityType<>(value.value, Units.SECOND);
375                         break;
376                     default:
377                         newState = new DecimalType(value.value);
378                         break;
379                 }
380                 updateState(channel.getUID(), newState);
381             } else {
382                 final boolean state = ((DigitalMessage) message).getPortState(output);
383                 updateState(channel.getUID(), OnOffType.from(state));
384             }
385         }
386     }
387
388     public void checkForTimeout() {
389         final long refTs = System.currentTimeMillis();
390         if (refTs - this.lastMessageRecvTS > 900000 && getThing().getStatus() != ThingStatus.OFFLINE) {
391             // no data received for 900 seconds - set thing status to offline..
392             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
393                     "No update from C.M.I. for 15 min");
394         }
395         for (final PodData pd : this.podDatas.values()) {
396             if (!(pd instanceof PodDataOutgoing)) {
397                 continue;
398             }
399             PodDataOutgoing podDataOutgoing = (PodDataOutgoing) pd;
400             @Nullable
401             Message message = pd.message;
402             if (message != null && refTs - podDataOutgoing.lastSent > 300000) {
403                 // re-send every 300 seconds...
404                 @Nullable
405                 final InetAddress cmia = this.cmiAddress;
406                 if (podDataOutgoing.isAllValuesInitialized()) {
407                     try {
408                         @Nullable
409                         final TACmiCoEBridgeHandler br = this.bridge;
410                         if (br != null && cmia != null) {
411                             br.sendData(message.getRaw(), cmia);
412                             podDataOutgoing.lastSent = System.currentTimeMillis();
413                         }
414                     } catch (final IOException e) {
415                         logger.warn("Error sending message to C.M.I.: {}: {}", e.getClass().getName(), e.getMessage());
416                     }
417                 } else {
418                     // pod is not entirely initialized - log warn for user but also set lastSent to prevent flooding of
419                     // logs...
420                     if (cmia != null) {
421                         logger.warn("Sending data to {} {}.{} is blocked as we don't have valid values for channels {}",
422                                 cmia.getHostAddress(), this.node, podDataOutgoing.podId,
423                                 podDataOutgoing.getUninitializedChannelNames());
424                     }
425                     podDataOutgoing.lastSent = System.currentTimeMillis();
426                 }
427             }
428         }
429     }
430 }