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