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