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