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