2 * Copyright (c) 2010-2023 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.HashMap;
24 import java.util.List;
26 import java.util.Optional;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.openhab.binding.gce.internal.action.Ipx800Actions;
32 import org.openhab.binding.gce.internal.config.AnalogInputConfiguration;
33 import org.openhab.binding.gce.internal.config.DigitalInputConfiguration;
34 import org.openhab.binding.gce.internal.config.Ipx800Configuration;
35 import org.openhab.binding.gce.internal.config.RelayOutputConfiguration;
36 import org.openhab.binding.gce.internal.model.M2MMessageParser;
37 import org.openhab.binding.gce.internal.model.PortData;
38 import org.openhab.binding.gce.internal.model.PortDefinition;
39 import org.openhab.binding.gce.internal.model.StatusFileInterpreter;
40 import org.openhab.binding.gce.internal.model.StatusFileInterpreter.StatusEntry;
41 import org.openhab.core.config.core.Configuration;
42 import org.openhab.core.library.CoreItemFactory;
43 import org.openhab.core.library.types.DecimalType;
44 import org.openhab.core.library.types.OnOffType;
45 import org.openhab.core.library.types.OpenClosedType;
46 import org.openhab.core.library.types.QuantityType;
47 import org.openhab.core.library.unit.Units;
48 import org.openhab.core.thing.Channel;
49 import org.openhab.core.thing.ChannelGroupUID;
50 import org.openhab.core.thing.ChannelUID;
51 import org.openhab.core.thing.Thing;
52 import org.openhab.core.thing.ThingStatus;
53 import org.openhab.core.thing.ThingStatusDetail;
54 import org.openhab.core.thing.binding.BaseThingHandler;
55 import org.openhab.core.thing.binding.ThingHandlerService;
56 import org.openhab.core.thing.binding.builder.ChannelBuilder;
57 import org.openhab.core.thing.type.ChannelKind;
58 import org.openhab.core.thing.type.ChannelTypeUID;
59 import org.openhab.core.types.Command;
60 import org.openhab.core.types.State;
61 import org.openhab.core.types.UnDefType;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
66 * The {@link Ipx800v3Handler} is responsible for handling commands, which are
67 * sent to one of the channels.
69 * @author Gaƫl L'hopital - Initial contribution
72 public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventListener {
73 private static final String PROPERTY_SEPARATOR = "-";
74 private static final double ANALOG_SAMPLING = 0.000050354;
76 private final Logger logger = LoggerFactory.getLogger(Ipx800v3Handler.class);
78 private Optional<Ipx800DeviceConnector> connector = Optional.empty();
79 private Optional<M2MMessageParser> parser = Optional.empty();
80 private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
82 private final Map<String, PortData> portDatas = new HashMap<>();
84 private class LongPressEvaluator implements Runnable {
85 private final ZonedDateTime referenceTime;
86 private final String port;
87 private final String eventChannelId;
89 public LongPressEvaluator(Channel channel, String port, PortData portData) {
90 this.referenceTime = portData.getTimestamp();
92 this.eventChannelId = channel.getUID().getId() + PROPERTY_SEPARATOR + TRIGGER_CONTACT;
97 PortData currentData = portDatas.get(port);
98 if (currentData != null && currentData.getValue() == 1
99 && referenceTime.equals(currentData.getTimestamp())) {
100 triggerChannel(eventChannelId, EVENT_LONG_PRESS);
105 public Ipx800v3Handler(Thing thing) {
107 logger.debug("Create an IPX800 Handler for thing '{}'", getThing().getUID());
111 public void initialize() {
112 logger.debug("Initializing IPX800 handler for uid '{}'", getThing().getUID());
114 Ipx800Configuration config = getConfigAs(Ipx800Configuration.class);
115 StatusFileInterpreter statusFile = new StatusFileInterpreter(config.hostname, this);
117 if (thing.getProperties().isEmpty()) {
118 updateProperties(Map.of(Thing.PROPERTY_VENDOR, "GCE Electronics", Thing.PROPERTY_FIRMWARE_VERSION,
119 statusFile.getElement(StatusEntry.VERSION), Thing.PROPERTY_MAC_ADDRESS,
120 statusFile.getElement(StatusEntry.CONFIG_MAC)));
123 List<Channel> channels = new ArrayList<>(getThing().getChannels());
124 PortDefinition.asStream().forEach(portDefinition -> {
125 int nbElements = statusFile.getMaxNumberofNodeType(portDefinition);
126 for (int i = 0; i < nbElements; i++) {
127 ChannelUID portChannelUID = createChannels(portDefinition, i, channels);
128 portDatas.put(portChannelUID.getId(), new PortData());
132 updateThing(editThing().withChannels(channels).build());
134 connector = Optional.of(new Ipx800DeviceConnector(config.hostname, config.portNumber, getThing().getUID()));
135 parser = Optional.of(new M2MMessageParser(connector.get(), this));
137 updateStatus(ThingStatus.UNKNOWN);
139 refreshJob = Optional.of(
140 scheduler.scheduleWithFixedDelay(statusFile::read, 3000, config.pullInterval, TimeUnit.MILLISECONDS));
142 connector.get().start();
146 public void dispose() {
147 refreshJob.ifPresent(job -> job.cancel(true));
148 refreshJob = Optional.empty();
150 connector.ifPresent(Ipx800DeviceConnector::dispose);
151 connector = Optional.empty();
153 parser = Optional.empty();
155 portDatas.values().stream().forEach(PortData::dispose);
159 private void addIfChannelAbsent(ChannelBuilder channelBuilder, List<Channel> channels) {
160 Channel newChannel = channelBuilder.build();
161 if (channels.stream().noneMatch(c -> c.getUID().equals(newChannel.getUID()))) {
162 channels.add(newChannel);
166 private ChannelUID createChannels(PortDefinition portDefinition, int portIndex, List<Channel> channels) {
167 String ndx = Integer.toString(portIndex + 1);
168 String advancedChannelTypeName = portDefinition.toString()
169 + (portDefinition.isAdvanced(portIndex) ? "Advanced" : "");
170 ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), portDefinition.toString());
171 ChannelUID mainChannelUID = new ChannelUID(groupUID, ndx);
172 ChannelTypeUID channelType = new ChannelTypeUID(BINDING_ID, advancedChannelTypeName);
173 switch (portDefinition) {
175 addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.NUMBER)
176 .withLabel("Analog Input " + ndx).withType(channelType), channels);
178 ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-voltage"), "Number:ElectricPotential")
179 .withType(new ChannelTypeUID(BINDING_ID, CHANNEL_VOLTAGE)).withLabel("Voltage " + ndx),
183 addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.CONTACT)
184 .withLabel("Contact " + ndx).withType(channelType), channels);
185 addIfChannelAbsent(ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-event"), null)
186 .withType(new ChannelTypeUID(BINDING_ID, TRIGGER_CONTACT + (portIndex < 8 ? "" : "Advanced")))
187 .withLabel("Contact " + ndx + " Event").withKind(ChannelKind.TRIGGER), channels);
190 addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.NUMBER)
191 .withLabel("Counter " + ndx).withType(channelType), channels);
194 addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.SWITCH)
195 .withLabel("Relay " + ndx).withType(channelType), channels);
199 addIfChannelAbsent(ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-duration"), "Number:Time")
200 .withType(new ChannelTypeUID(BINDING_ID, CHANNEL_LAST_STATE_DURATION))
201 .withLabel("Previous state duration " + ndx), channels);
203 return mainChannelUID;
207 public void errorOccurred(Exception e) {
208 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
211 private boolean ignoreCondition(double newValue, PortData portData, Configuration configuration,
212 PortDefinition portDefinition, ZonedDateTime now) {
213 if (!portData.isInitializing()) { // Always accept if portData is not initialized
214 double prevValue = portData.getValue();
215 if (newValue == prevValue) { // Always reject if the value did not change
218 if (portDefinition == PortDefinition.ANALOG) { // For analog values, check histeresis
219 AnalogInputConfiguration config = configuration.as(AnalogInputConfiguration.class);
220 long hysteresis = config.hysteresis / 2;
221 return (newValue <= prevValue + hysteresis && newValue >= prevValue - hysteresis);
222 } else if (portDefinition == PortDefinition.CONTACT) { // For contact values, check debounce
223 DigitalInputConfiguration config = configuration.as(DigitalInputConfiguration.class);
224 return (config.debouncePeriod != 0
225 && now.isBefore(portData.getTimestamp().plus(config.debouncePeriod, ChronoUnit.MILLIS)));
232 public void dataReceived(String port, double value) {
233 updateStatus(ThingStatus.ONLINE);
234 Channel channel = thing.getChannel(PortDefinition.asChannelId(port));
235 if (channel != null) {
236 String channelId = channel.getUID().getId();
237 String groupId = channel.getUID().getGroupId();
238 PortData portData = portDatas.get(channelId);
239 if (portData != null && groupId != null) {
240 ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault());
241 long sinceLastChange = Duration.between(portData.getTimestamp(), now).toMillis();
242 Configuration configuration = channel.getConfiguration();
243 PortDefinition portDefinition = PortDefinition.fromGroupId(groupId);
244 if (ignoreCondition(value, portData, configuration, portDefinition, now)) {
245 logger.debug("Ignore condition met for port '{}' with data '{}'", port, value);
248 logger.debug("About to update port '{}' with data '{}'", port, value);
249 State state = UnDefType.NULL;
250 switch (portDefinition) {
252 state = new DecimalType(value);
255 state = OnOffType.from(value == 1);
258 state = new DecimalType(value);
259 updateIfLinked(channelId + PROPERTY_SEPARATOR + CHANNEL_VOLTAGE,
260 new QuantityType<>(value * ANALOG_SAMPLING, Units.VOLT));
263 DigitalInputConfiguration config = configuration.as(DigitalInputConfiguration.class);
264 portData.cancelPulsing();
265 state = value == 1 ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
266 switch ((OpenClosedType) state) {
268 if (config.longPressTime != 0 && !portData.isInitializing()) {
269 scheduler.schedule(new LongPressEvaluator(channel, port, portData),
270 config.longPressTime, TimeUnit.MILLISECONDS);
271 } else if (config.pulsePeriod != 0) {
272 portData.setPulsing(scheduler.scheduleWithFixedDelay(() -> {
273 triggerPushButtonChannel(channel, EVENT_PULSE);
274 }, config.pulsePeriod, config.pulsePeriod, TimeUnit.MILLISECONDS));
275 if (config.pulseTimeout != 0) {
276 scheduler.schedule(portData::cancelPulsing, config.pulseTimeout,
277 TimeUnit.MILLISECONDS);
282 if (!portData.isInitializing() && config.longPressTime != 0
283 && sinceLastChange < config.longPressTime) {
284 triggerPushButtonChannel(channel, EVENT_SHORT_PRESS);
288 if (!portData.isInitializing()) {
289 triggerPushButtonChannel(channel, value == 1 ? EVENT_PRESSED : EVENT_RELEASED);
294 updateIfLinked(channelId, state);
295 if (!portData.isInitializing()) {
296 updateIfLinked(channelId + PROPERTY_SEPARATOR + CHANNEL_LAST_STATE_DURATION,
297 new QuantityType<>(sinceLastChange / 1000, Units.SECOND));
299 portData.setData(value, now);
301 logger.debug("Received data '{}' for not configured port '{}'", value, port);
304 logger.debug("Received data '{}' for not configured channel '{}'", value, port);
308 private void updateIfLinked(String channelId, State state) {
309 if (isLinked(channelId)) {
310 updateState(channelId, state);
314 protected void triggerPushButtonChannel(Channel channel, String event) {
315 logger.debug("Triggering event '{}' on channel '{}'", event, channel.getUID());
316 triggerChannel(channel.getUID().getId() + PROPERTY_SEPARATOR + TRIGGER_CONTACT, event);
320 public void handleCommand(ChannelUID channelUID, Command command) {
321 logger.debug("Received channel: {}, command: {}", channelUID, command);
323 Channel channel = thing.getChannel(channelUID.getId());
324 String groupId = channelUID.getGroupId();
326 if (channel == null || groupId == null) {
329 if (command instanceof OnOffType onOffCommand && isValidPortId(channelUID)
330 && PortDefinition.fromGroupId(groupId) == PortDefinition.RELAY) {
331 RelayOutputConfiguration config = channel.getConfiguration().as(RelayOutputConfiguration.class);
332 String id = channelUID.getIdWithoutGroup();
333 parser.ifPresent(p -> p.setOutput(id, onOffCommand == OnOffType.ON ? 1 : 0, config.pulse));
336 logger.debug("Can not handle command '{}' on channel '{}'", command, channelUID);
339 private boolean isValidPortId(ChannelUID channelUID) {
340 return channelUID.getIdWithoutGroup().chars().allMatch(Character::isDigit);
343 public void resetCounter(int counter) {
344 parser.ifPresent(p -> p.resetCounter(counter));
347 public void reset() {
348 parser.ifPresent(M2MMessageParser::resetPLC);
352 public Collection<Class<? extends ThingHandlerService>> getServices() {
353 return List.of(Ipx800Actions.class);