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.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.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;
52 * The {@link TACmiHandler} is responsible for handling commands, which are sent
53 * to one of the channels.
55 * @author Timo Wendt - Initial contribution
56 * @author Christian Niessner - Ported to OpenHAB2
59 public class TACmiHandler extends BaseThingHandler {
61 private final Logger logger = LoggerFactory.getLogger(TACmiHandler.class);
63 private final Map<PodIdentifier, PodData> podDatas = new HashMap<>();
64 private final Map<ChannelUID, TACmiChannelConfiguration> channelConfigByUID = new HashMap<>();
66 private @Nullable TACmiCoEBridgeHandler bridge;
67 private long lastMessageRecvTS; // last received message timestamp
70 * the C.M.I.'s address
72 private @Nullable InetAddress cmiAddress;
75 * the CoE CAN-Node we representing
79 public TACmiHandler(final Thing thing) {
84 public void initialize() {
85 updateStatus(ThingStatus.UNKNOWN);
87 scheduler.execute(this::initializeDetached);
90 private void initializeDetached() {
91 final TACmiConfiguration config = getConfigAs(TACmiConfiguration.class);
93 if (config.host == null) {
94 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No host configured!");
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 + "'");
106 this.node = config.node;
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);
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
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();
143 Message message = pd.message;
144 if (message != null) {
145 // shouldn't happen, just in case...
146 message.setValue(outputIdx, (short) val, measureType.ordinal());
152 TACmiChannelConfigurationDigital ca = (TACmiChannelConfigurationDigital) channelConfig;
153 Boolean initialValue = ca.initialValue;
154 if (initialValue != null) {
156 DigitalMessage message = (DigitalMessage) pd.message;
157 if (message != null) {
158 // shouldn't happen, just in case...
159 message.setPortState(outputIdx, initialValue);
164 podDataOutgoing.channeUIDs[outputIdx] = chann.getUID();
165 podDataOutgoing.initialized[outputIdx] = set;
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!");
175 bridge.registerCMI(this);
176 this.bridge = bridge;
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);
183 private PodData getPodData(final PodIdentifier pi) {
184 PodData pd = this.podDatas.get(pi);
187 pd = new PodDataOutgoing(pi, (byte) this.node);
189 pd = new PodData(pi, (byte) this.node);
191 this.podDatas.put(pi, pd);
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);
204 return (byte) (outputIdx == 0 ? 0 : 9);
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...
215 private int getOutputIndex(int output, boolean analog) {
216 int outputIdx = output - 1;
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);
232 final Channel channel = thing.getChannel(channelUID);
233 if (channel == null) {
237 if (command instanceof RefreshType) {
238 // we try to find the last known state from cache and return it.
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;
245 logger.debug("Recived unhandled command '{}' on unknown Channel type {} ", command, channelUID);
248 final byte podId = getPodId(mt, channelConfig.output);
249 PodData pd = getPodData(new PodIdentifier(mt, podId, true));
251 Message message = pd.message;
252 if (message == null) {
253 // no data received yet from the C.M.I. and persistence might be disabled..
256 if (mt == MessageType.ANALOG) {
257 final AnalogValue value = ((AnalogMessage) message).getAnalogValue(channelConfig.output);
258 updateState(channel.getUID(), new DecimalType(value.value));
260 final boolean state = ((DigitalMessage) message).getPortState(channelConfig.output);
261 updateState(channel.getUID(), OnOffType.from(state));
267 if ((TACmiBindingConstants.CHANNEL_TYPE_COE_DIGITAL_OUT_UID.equals(channel.getChannelTypeUID()))) {
268 mt = MessageType.DIGITAL;
270 } else if ((TACmiBindingConstants.CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(channel.getChannelTypeUID()))) {
271 mt = MessageType.ANALOG;
274 logger.debug("Recived unhandled command '{}' on Channel {} ", command, channelUID);
278 final byte podId = getPodId(mt, channelConfig.output);
279 PodDataOutgoing podDataOutgoing = (PodDataOutgoing) getPodData(new PodIdentifier(mt, podId, true));
281 Message message = podDataOutgoing.message;
282 if (message == null) {
283 logger.error("Internal error - BUG - no outgoing message for command '{}' on Channel {} ", command,
287 int outputIdx = getOutputIndex(channelConfig.output, 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());
296 final boolean state = OnOffType.ON.equals(command) ? true : false;
297 modified = ((DigitalMessage) message).setPortState(outputIdx, state);
299 podDataOutgoing.initialized[outputIdx] = true;
303 final TACmiCoEBridgeHandler br = this.bridge;
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();
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());
321 public void dispose() {
322 final TACmiCoEBridgeHandler br = this.bridge;
324 br.unregisterCMI(this);
329 public boolean isFor(final InetAddress remoteAddress, final int node) {
331 final InetAddress cmia = this.cmiAddress;
335 return this.node == node && cmia.equals(remoteAddress);
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);
345 this.lastMessageRecvTS = System.currentTimeMillis();
346 for (final Channel channel : thing.getChannels()) {
347 if (!(channelType.equals(channel.getChannelTypeUID()))) {
350 final int output = ((Number) channel.getConfiguration().get(TACmiBindingConstants.CHANNEL_CONFIG_OUTPUT))
352 if (!message.hasPortnumber(output)) {
356 if (message.getType() == MessageType.ANALOG) {
357 final AnalogValue value = ((AnalogMessage) message).getAnalogValue(output);
359 switch (value.measureType) {
361 newState = new QuantityType<>(value.value, SIUnits.CELSIUS);
364 // TA uses kW, in OH we use W
365 newState = new QuantityType<>(value.value * 1000, Units.WATT);
368 newState = new QuantityType<>(value.value, Units.KILOWATT_HOUR);
371 newState = new QuantityType<>(value.value, Units.MEGAWATT_HOUR);
374 newState = new QuantityType<>(value.value, Units.SECOND);
377 newState = new DecimalType(value.value);
380 updateState(channel.getUID(), newState);
382 final boolean state = ((DigitalMessage) message).getPortState(output);
383 updateState(channel.getUID(), OnOffType.from(state));
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");
395 for (final PodData pd : this.podDatas.values()) {
396 if (!(pd instanceof PodDataOutgoing)) {
399 PodDataOutgoing podDataOutgoing = (PodDataOutgoing) pd;
401 Message message = pd.message;
402 if (message != null && refTs - podDataOutgoing.lastSent > 300000) {
403 // re-send every 300 seconds...
405 final InetAddress cmia = this.cmiAddress;
406 if (podDataOutgoing.isAllValuesInitialized()) {
409 final TACmiCoEBridgeHandler br = this.bridge;
410 if (br != null && cmia != null) {
411 br.sendData(message.getRaw(), cmia);
412 podDataOutgoing.lastSent = System.currentTimeMillis();
414 } catch (final IOException e) {
415 logger.warn("Error sending message to C.M.I.: {}: {}", e.getClass().getName(), e.getMessage());
418 // pod is not entirely initialized - log warn for user but also set lastSent to prevent flooding of
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());
425 podDataOutgoing.lastSent = System.currentTimeMillis();