2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.gce.internal.handler;
15 import static org.openhab.binding.gce.internal.GCEBindingConstants.*;
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;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
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;
68 * The {@link Ipx800v3Handler} is responsible for handling commands, which are
69 * sent to one of the channels.
71 * @author Gaƫl L'hopital - Initial contribution
74 public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventListener {
75 private static final String PROPERTY_SEPARATOR = "-";
76 private static final double ANALOG_SAMPLING = 0.000050354;
78 private final Logger logger = LoggerFactory.getLogger(Ipx800v3Handler.class);
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;
86 private final Map<String, @Nullable PortData> portDatas = new HashMap<>();
88 private class LongPressEvaluator implements Runnable {
89 private final ZonedDateTime referenceTime;
90 private final String port;
91 private final String eventChannelId;
93 public LongPressEvaluator(Channel channel, String port, PortData portData) {
94 this.referenceTime = portData.getTimestamp();
96 this.eventChannelId = channel.getUID().getId() + PROPERTY_SEPARATOR + TRIGGER_CONTACT;
101 PortData currentData = portDatas.get(port);
102 if (currentData != null && currentData.getValue() == 1 && currentData.getTimestamp() == referenceTime) {
103 triggerChannel(eventChannelId, EVENT_LONG_PRESS);
108 public Ipx800v3Handler(Thing thing) {
110 logger.debug("Create a IPX800 Handler for thing '{}'", getThing().getUID());
114 public void initialize() {
116 configuration = getConfigAs(Ipx800Configuration.class);
118 logger.debug("Initializing IPX800 handler for uid '{}'", getThing().getUID());
120 statusFile = new StatusFileInterpreter(configuration.hostname, this);
122 if (thing.getProperties().isEmpty()) {
123 discoverAttributes();
126 connector = new Ipx800DeviceConnector(configuration.hostname, configuration.portNumber, getThing().getUID());
127 parser = new M2MMessageParser(connector, this);
129 updateStatus(ThingStatus.UNKNOWN);
131 refreshJob = scheduler.scheduleWithFixedDelay(statusFile::read, 3000, configuration.pullInterval,
132 TimeUnit.MILLISECONDS);
138 public void dispose() {
139 if (refreshJob != null) {
140 refreshJob.cancel(true);
144 if (connector != null) {
145 connector.destroyAndExit();
149 portDatas.values().stream().forEach(portData -> {
150 if (portData != null) {
157 protected void discoverAttributes() {
158 final Map<String, String> properties = new HashMap<>();
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);
165 ThingBuilder thingBuilder = editThing();
166 List<Channel> channels = new ArrayList<>(getThing().getChannels());
168 PortDefinition.asStream().forEach(portDefinition -> {
169 int nbElements = statusFile.getMaxNumberofNodeType(portDefinition);
170 for (int i = 0; i < nbElements; i++) {
171 createChannels(portDefinition, i, channels);
175 thingBuilder.withChannels(channels);
176 updateThing(thingBuilder.build());
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) {
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());
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")))
203 channels.add(ChannelBuilder.create(mainChannelUID, CoreItemFactory.NUMBER).withLabel("Counter " + ndx)
204 .withType(channelType).build());
207 channels.add(ChannelBuilder.create(mainChannelUID, CoreItemFactory.SWITCH).withLabel("Relay " + ndx)
208 .withType(channelType).build());
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());
217 public void errorOccurred(Exception e) {
218 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
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
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) {
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))) {
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);
263 logger.debug("About to update port '{}' with data '{}'", port, value);
264 State state = UnDefType.UNDEF;
265 switch (portDefinition) {
267 state = new DecimalType(value);
270 state = value == 1 ? OnOffType.ON : OnOffType.OFF;
273 state = new DecimalType(value);
274 updateState(channelId + PROPERTY_SEPARATOR + CHANNEL_VOLTAGE,
275 new QuantityType<>(value * ANALOG_SAMPLING, SmartHomeUnits.VOLT));
278 DigitalInputConfiguration config = configuration.as(DigitalInputConfiguration.class);
279 portData.cancelPulsing();
280 state = value == 1 ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
281 switch ((OpenClosedType) state) {
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);
297 if (!portData.isInitializing() && config.longPressTime != 0
298 && sinceLastChange < config.longPressTime) {
299 triggerPushButtonChannel(channel, EVENT_SHORT_PRESS);
303 if (!portData.isInitializing()) {
304 triggerPushButtonChannel(channel, value == 1 ? EVENT_PRESSED : EVENT_RELEASED);
309 updateState(channelId, state);
310 if (!portData.isInitializing()) {
311 updateState(channelId + PROPERTY_SEPARATOR + CHANNEL_LAST_STATE_DURATION,
312 new QuantityType<>(sinceLastChange / 1000, SmartHomeUnits.SECOND));
314 portData.setData(value, now);
316 logger.debug("Received data '{}' for not configured port '{}'", value, port);
319 logger.debug("Received data '{}' for not configured channel '{}'", value, port);
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);
329 public void handleCommand(ChannelUID channelUID, Command command) {
330 logger.debug("Received channel: {}, command: {}", channelUID, command);
332 Channel channel = thing.getChannel(channelUID.getId());
333 String groupId = channelUID.getGroupId();
335 if (channel == null || groupId == null) {
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);
347 logger.debug("Can not handle command '{}' on channel '{}'", command, channelUID);
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);
363 private boolean isValidPortId(ChannelUID channelUID) {
364 return channelUID.getIdWithoutGroup().chars().allMatch(Character::isDigit);
368 public void channelUnlinked(ChannelUID channelUID) {
369 super.channelUnlinked(channelUID);
370 PortData portData = portDatas.remove(channelUID.getId());
371 if (portData != null) {
376 public void resetCounter(int counter) {
377 if (parser != null) {
378 parser.resetCounter(counter);
382 public void reset() {
383 if (parser != null) {
389 public Collection<Class<? extends ThingHandlerService>> getServices() {
390 return Collections.singletonList(Ipx800Actions.class);