]> git.basschouten.com Git - openhab-addons.git/blob
79a0217fef23648a2e2a574e1e678a5b527efe9a
[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.gce.internal.handler;
14
15 import static org.openhab.binding.gce.internal.GCEBindingConstants.*;
16
17 import java.time.Duration;
18 import java.time.ZoneId;
19 import java.time.ZonedDateTime;
20 import java.time.temporal.ChronoUnit;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Optional;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.openhab.binding.gce.internal.action.Ipx800Actions;
32 import org.openhab.binding.gce.internal.config.AnalogInputConfiguration;
33 import org.openhab.binding.gce.internal.config.DigitalInputConfiguration;
34 import org.openhab.binding.gce.internal.config.Ipx800Configuration;
35 import org.openhab.binding.gce.internal.config.RelayOutputConfiguration;
36 import org.openhab.binding.gce.internal.model.M2MMessageParser;
37 import org.openhab.binding.gce.internal.model.PortData;
38 import org.openhab.binding.gce.internal.model.PortDefinition;
39 import org.openhab.binding.gce.internal.model.StatusFileInterpreter;
40 import org.openhab.binding.gce.internal.model.StatusFileInterpreter.StatusEntry;
41 import org.openhab.core.config.core.Configuration;
42 import org.openhab.core.library.CoreItemFactory;
43 import org.openhab.core.library.types.DecimalType;
44 import org.openhab.core.library.types.OnOffType;
45 import org.openhab.core.library.types.OpenClosedType;
46 import org.openhab.core.library.types.QuantityType;
47 import org.openhab.core.library.unit.Units;
48 import org.openhab.core.thing.Channel;
49 import org.openhab.core.thing.ChannelGroupUID;
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.thing.binding.builder.ChannelBuilder;
57 import org.openhab.core.thing.type.ChannelKind;
58 import org.openhab.core.thing.type.ChannelTypeUID;
59 import org.openhab.core.types.Command;
60 import org.openhab.core.types.State;
61 import org.openhab.core.types.UnDefType;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 /**
66  * The {@link Ipx800v3Handler} is responsible for handling commands, which are
67  * sent to one of the channels.
68  *
69  * @author GaĆ«l L'hopital - Initial contribution
70  */
71 @NonNullByDefault
72 public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventListener {
73     private static final String PROPERTY_SEPARATOR = "-";
74     private static final double ANALOG_SAMPLING = 0.000050354;
75
76     private final Logger logger = LoggerFactory.getLogger(Ipx800v3Handler.class);
77
78     private Optional<Ipx800DeviceConnector> connector = Optional.empty();
79     private Optional<M2MMessageParser> parser = Optional.empty();
80     private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
81
82     private final Map<String, PortData> portDatas = new HashMap<>();
83
84     private class LongPressEvaluator implements Runnable {
85         private final ZonedDateTime referenceTime;
86         private final String port;
87         private final String eventChannelId;
88
89         public LongPressEvaluator(Channel channel, String port, PortData portData) {
90             this.referenceTime = portData.getTimestamp();
91             this.port = port;
92             this.eventChannelId = channel.getUID().getId() + PROPERTY_SEPARATOR + TRIGGER_CONTACT;
93         }
94
95         @Override
96         public void run() {
97             PortData currentData = portDatas.get(port);
98             if (currentData != null && currentData.getValue() == 1
99                     && referenceTime.equals(currentData.getTimestamp())) {
100                 triggerChannel(eventChannelId, EVENT_LONG_PRESS);
101             }
102         }
103     }
104
105     public Ipx800v3Handler(Thing thing) {
106         super(thing);
107         logger.debug("Create an IPX800 Handler for thing '{}'", getThing().getUID());
108     }
109
110     @Override
111     public void initialize() {
112         logger.debug("Initializing IPX800 handler for uid '{}'", getThing().getUID());
113
114         Ipx800Configuration config = getConfigAs(Ipx800Configuration.class);
115         StatusFileInterpreter statusFile = new StatusFileInterpreter(config.hostname, this);
116
117         if (thing.getProperties().isEmpty()) {
118             updateProperties(Map.of(Thing.PROPERTY_VENDOR, "GCE Electronics", Thing.PROPERTY_FIRMWARE_VERSION,
119                     statusFile.getElement(StatusEntry.VERSION), Thing.PROPERTY_MAC_ADDRESS,
120                     statusFile.getElement(StatusEntry.CONFIG_MAC)));
121         }
122
123         List<Channel> channels = new ArrayList<>(getThing().getChannels());
124         PortDefinition.asStream().forEach(portDefinition -> {
125             int nbElements = statusFile.getMaxNumberofNodeType(portDefinition);
126             for (int i = 0; i < nbElements; i++) {
127                 ChannelUID portChannelUID = createChannels(portDefinition, i, channels);
128                 portDatas.put(portChannelUID.getId(), new PortData());
129             }
130         });
131
132         updateThing(editThing().withChannels(channels).build());
133
134         connector = Optional.of(new Ipx800DeviceConnector(config.hostname, config.portNumber, getThing().getUID()));
135         parser = Optional.of(new M2MMessageParser(connector.get(), this));
136
137         updateStatus(ThingStatus.UNKNOWN);
138
139         refreshJob = Optional.of(
140                 scheduler.scheduleWithFixedDelay(statusFile::read, 3000, config.pullInterval, TimeUnit.MILLISECONDS));
141
142         connector.get().start();
143     }
144
145     @Override
146     public void dispose() {
147         refreshJob.ifPresent(job -> job.cancel(true));
148         refreshJob = Optional.empty();
149
150         connector.ifPresent(Ipx800DeviceConnector::dispose);
151         connector = Optional.empty();
152
153         parser = Optional.empty();
154
155         portDatas.values().stream().forEach(PortData::dispose);
156         super.dispose();
157     }
158
159     private void addIfChannelAbsent(ChannelBuilder channelBuilder, List<Channel> channels) {
160         Channel newChannel = channelBuilder.build();
161         if (channels.stream().noneMatch(c -> c.getUID().equals(newChannel.getUID()))) {
162             channels.add(newChannel);
163         }
164     }
165
166     private ChannelUID createChannels(PortDefinition portDefinition, int portIndex, List<Channel> channels) {
167         String ndx = Integer.toString(portIndex + 1);
168         String advancedChannelTypeName = portDefinition.toString()
169                 + (portDefinition.isAdvanced(portIndex) ? "Advanced" : "");
170         ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), portDefinition.toString());
171         ChannelUID mainChannelUID = new ChannelUID(groupUID, ndx);
172         ChannelTypeUID channelType = new ChannelTypeUID(BINDING_ID, advancedChannelTypeName);
173         switch (portDefinition) {
174             case ANALOG:
175                 addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.NUMBER)
176                         .withLabel("Analog Input " + ndx).withType(channelType), channels);
177                 addIfChannelAbsent(
178                         ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-voltage"), "Number:ElectricPotential")
179                                 .withType(new ChannelTypeUID(BINDING_ID, CHANNEL_VOLTAGE)).withLabel("Voltage " + ndx),
180                         channels);
181                 break;
182             case CONTACT:
183                 addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.CONTACT)
184                         .withLabel("Contact " + ndx).withType(channelType), channels);
185                 addIfChannelAbsent(ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-event"), null)
186                         .withType(new ChannelTypeUID(BINDING_ID, TRIGGER_CONTACT + (portIndex < 8 ? "" : "Advanced")))
187                         .withLabel("Contact " + ndx + " Event").withKind(ChannelKind.TRIGGER), channels);
188                 break;
189             case COUNTER:
190                 addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.NUMBER)
191                         .withLabel("Counter " + ndx).withType(channelType), channels);
192                 break;
193             case RELAY:
194                 addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.SWITCH)
195                         .withLabel("Relay " + ndx).withType(channelType), channels);
196                 break;
197         }
198
199         addIfChannelAbsent(ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-duration"), "Number:Time")
200                 .withType(new ChannelTypeUID(BINDING_ID, CHANNEL_LAST_STATE_DURATION))
201                 .withLabel("Previous state duration " + ndx), channels);
202
203         return mainChannelUID;
204     }
205
206     @Override
207     public void errorOccurred(Exception e) {
208         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
209     }
210
211     private boolean ignoreCondition(double newValue, PortData portData, Configuration configuration,
212             PortDefinition portDefinition, ZonedDateTime now) {
213         if (!portData.isInitializing()) { // Always accept if portData is not initialized
214             double prevValue = portData.getValue();
215             if (newValue == prevValue) { // Always reject if the value did not change
216                 return true;
217             }
218             if (portDefinition == PortDefinition.ANALOG) { // For analog values, check histeresis
219                 AnalogInputConfiguration config = configuration.as(AnalogInputConfiguration.class);
220                 long hysteresis = config.hysteresis / 2;
221                 return (newValue <= prevValue + hysteresis && newValue >= prevValue - hysteresis);
222             } else if (portDefinition == PortDefinition.CONTACT) { // For contact values, check debounce
223                 DigitalInputConfiguration config = configuration.as(DigitalInputConfiguration.class);
224                 return (config.debouncePeriod != 0
225                         && now.isBefore(portData.getTimestamp().plus(config.debouncePeriod, ChronoUnit.MILLIS)));
226             }
227         }
228         return false;
229     }
230
231     @Override
232     public void dataReceived(String port, double value) {
233         updateStatus(ThingStatus.ONLINE);
234         Channel channel = thing.getChannel(PortDefinition.asChannelId(port));
235         if (channel != null) {
236             String channelId = channel.getUID().getId();
237             String groupId = channel.getUID().getGroupId();
238             PortData portData = portDatas.get(channelId);
239             if (portData != null && groupId != null) {
240                 ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault());
241                 long sinceLastChange = Duration.between(portData.getTimestamp(), now).toMillis();
242                 Configuration configuration = channel.getConfiguration();
243                 PortDefinition portDefinition = PortDefinition.fromGroupId(groupId);
244                 if (ignoreCondition(value, portData, configuration, portDefinition, now)) {
245                     logger.debug("Ignore condition met for port '{}' with data '{}'", port, value);
246                     return;
247                 }
248                 logger.debug("About to update port '{}' with data '{}'", port, value);
249                 State state = UnDefType.NULL;
250                 switch (portDefinition) {
251                     case COUNTER:
252                         state = new DecimalType(value);
253                         break;
254                     case RELAY:
255                         state = value == 1 ? OnOffType.ON : OnOffType.OFF;
256                         break;
257                     case ANALOG:
258                         state = new DecimalType(value);
259                         updateIfLinked(channelId + PROPERTY_SEPARATOR + CHANNEL_VOLTAGE,
260                                 new QuantityType<>(value * ANALOG_SAMPLING, Units.VOLT));
261                         break;
262                     case CONTACT:
263                         DigitalInputConfiguration config = configuration.as(DigitalInputConfiguration.class);
264                         portData.cancelPulsing();
265                         state = value == 1 ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
266                         switch ((OpenClosedType) state) {
267                             case CLOSED:
268                                 if (config.longPressTime != 0 && !portData.isInitializing()) {
269                                     scheduler.schedule(new LongPressEvaluator(channel, port, portData),
270                                             config.longPressTime, TimeUnit.MILLISECONDS);
271                                 } else if (config.pulsePeriod != 0) {
272                                     portData.setPulsing(scheduler.scheduleWithFixedDelay(() -> {
273                                         triggerPushButtonChannel(channel, EVENT_PULSE);
274                                     }, config.pulsePeriod, config.pulsePeriod, TimeUnit.MILLISECONDS));
275                                     if (config.pulseTimeout != 0) {
276                                         scheduler.schedule(portData::cancelPulsing, config.pulseTimeout,
277                                                 TimeUnit.MILLISECONDS);
278                                     }
279                                 }
280                                 break;
281                             case OPEN:
282                                 if (!portData.isInitializing() && config.longPressTime != 0
283                                         && sinceLastChange < config.longPressTime) {
284                                     triggerPushButtonChannel(channel, EVENT_SHORT_PRESS);
285                                 }
286                                 break;
287                         }
288                         if (!portData.isInitializing()) {
289                             triggerPushButtonChannel(channel, value == 1 ? EVENT_PRESSED : EVENT_RELEASED);
290                         }
291                         break;
292                 }
293
294                 updateIfLinked(channelId, state);
295                 if (!portData.isInitializing()) {
296                     updateIfLinked(channelId + PROPERTY_SEPARATOR + CHANNEL_LAST_STATE_DURATION,
297                             new QuantityType<>(sinceLastChange / 1000, Units.SECOND));
298                 }
299                 portData.setData(value, now);
300             } else {
301                 logger.debug("Received data '{}' for not configured port '{}'", value, port);
302             }
303         } else {
304             logger.debug("Received data '{}' for not configured channel '{}'", value, port);
305         }
306     }
307
308     private void updateIfLinked(String channelId, State state) {
309         if (isLinked(channelId)) {
310             updateState(channelId, state);
311         }
312     }
313
314     protected void triggerPushButtonChannel(Channel channel, String event) {
315         logger.debug("Triggering event '{}' on channel '{}'", event, channel.getUID());
316         triggerChannel(channel.getUID().getId() + PROPERTY_SEPARATOR + TRIGGER_CONTACT, event);
317     }
318
319     @Override
320     public void handleCommand(ChannelUID channelUID, Command command) {
321         logger.debug("Received channel: {}, command: {}", channelUID, command);
322
323         Channel channel = thing.getChannel(channelUID.getId());
324         String groupId = channelUID.getGroupId();
325
326         if (channel == null || groupId == null) {
327             return;
328         }
329         if (command instanceof OnOffType onOffCommand && isValidPortId(channelUID)
330                 && PortDefinition.fromGroupId(groupId) == PortDefinition.RELAY) {
331             RelayOutputConfiguration config = channel.getConfiguration().as(RelayOutputConfiguration.class);
332             String id = channelUID.getIdWithoutGroup();
333             parser.ifPresent(p -> p.setOutput(id, onOffCommand == OnOffType.ON ? 1 : 0, config.pulse));
334             return;
335         }
336         logger.debug("Can not handle command '{}' on channel '{}'", command, channelUID);
337     }
338
339     private boolean isValidPortId(ChannelUID channelUID) {
340         return channelUID.getIdWithoutGroup().chars().allMatch(Character::isDigit);
341     }
342
343     public void resetCounter(int counter) {
344         parser.ifPresent(p -> p.resetCounter(counter));
345     }
346
347     public void reset() {
348         parser.ifPresent(M2MMessageParser::resetPLC);
349     }
350
351     @Override
352     public Collection<Class<? extends ThingHandlerService>> getServices() {
353         return List.of(Ipx800Actions.class);
354     }
355 }