]> git.basschouten.com Git - openhab-addons.git/blob
10d7780fbb7b3e7b72675adc64e0656da91f25f1
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.network.internal.handler;
14
15 import static org.openhab.binding.network.internal.NetworkBindingConstants.*;
16
17 import java.time.Instant;
18 import java.time.ZonedDateTime;
19 import java.util.Collection;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.TimeZone;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.openhab.binding.network.internal.NetworkBindingConfiguration;
27 import org.openhab.binding.network.internal.NetworkBindingConfigurationListener;
28 import org.openhab.binding.network.internal.NetworkBindingConstants;
29 import org.openhab.binding.network.internal.NetworkHandlerConfiguration;
30 import org.openhab.binding.network.internal.PresenceDetection;
31 import org.openhab.binding.network.internal.PresenceDetectionListener;
32 import org.openhab.binding.network.internal.PresenceDetectionValue;
33 import org.openhab.binding.network.internal.WakeOnLanPacketSender;
34 import org.openhab.binding.network.internal.action.NetworkActions;
35 import org.openhab.core.library.types.DateTimeType;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.library.unit.MetricPrefix;
40 import org.openhab.core.library.unit.Units;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.thing.binding.ThingHandlerService;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.UnDefType;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 /**
54  * This handler is handling the CHANNEL_ONLINE (boolean) and CHANNEL_TIME (time in ms)
55  * commands and is starting a {@see NetworkService} instance for the configured device.
56  *
57  * @author Marc Mettke - Initial contribution
58  * @author David Graeff - Rewritten
59  * @author Wouter Born - Add Wake-on-LAN thing action support
60  */
61 @NonNullByDefault
62 public class NetworkHandler extends BaseThingHandler
63         implements PresenceDetectionListener, NetworkBindingConfigurationListener {
64     private final Logger logger = LoggerFactory.getLogger(NetworkHandler.class);
65     private @NonNullByDefault({}) PresenceDetection presenceDetection;
66     private @NonNullByDefault({}) WakeOnLanPacketSender wakeOnLanPacketSender;
67
68     private boolean isTCPServiceDevice;
69     private NetworkBindingConfiguration configuration;
70
71     // How many retries before a device is deemed offline
72     int retries;
73     // Retry counter. Will be reset as soon as a device presence detection succeed.
74     private int retryCounter = 0;
75     private NetworkHandlerConfiguration handlerConfiguration = new NetworkHandlerConfiguration();
76
77     /**
78      * Do not call this directly, but use the {@see NetworkHandlerBuilder} instead.
79      */
80     public NetworkHandler(Thing thing, boolean isTCPServiceDevice, NetworkBindingConfiguration configuration) {
81         super(thing);
82         this.isTCPServiceDevice = isTCPServiceDevice;
83         this.configuration = configuration;
84         this.configuration.addNetworkBindingConfigurationListener(this);
85     }
86
87     private void refreshValue(ChannelUID channelUID) {
88         // We are not yet even initialized, don't do anything
89         if (presenceDetection == null || !presenceDetection.isAutomaticRefreshing()) {
90             return;
91         }
92
93         switch (channelUID.getId()) {
94             case CHANNEL_ONLINE:
95                 presenceDetection.getValue(value -> updateState(CHANNEL_ONLINE, OnOffType.from(value.isReachable())));
96                 break;
97             case CHANNEL_LATENCY:
98             case CHANNEL_DEPRECATED_TIME:
99                 presenceDetection.getValue(value -> {
100                     updateState(CHANNEL_LATENCY,
101                             new QuantityType<>(value.getLowestLatency(), MetricPrefix.MILLI(Units.SECOND)));
102                     updateState(CHANNEL_DEPRECATED_TIME, new DecimalType(value.getLowestLatency()));
103                 });
104                 break;
105             case CHANNEL_LASTSEEN:
106                 if (presenceDetection.getLastSeen() > 0) {
107                     Instant instant = Instant.ofEpochMilli(presenceDetection.getLastSeen());
108                     updateState(CHANNEL_LASTSEEN, new DateTimeType(
109                             ZonedDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
110                 } else {
111                     updateState(CHANNEL_LASTSEEN, UnDefType.UNDEF);
112                 }
113                 break;
114             default:
115                 logger.debug("Command received for an unknown channel: {}", channelUID.getId());
116                 break;
117         }
118     }
119
120     @Override
121     public void handleCommand(ChannelUID channelUID, Command command) {
122         if (command instanceof RefreshType) {
123             refreshValue(channelUID);
124         } else {
125             logger.debug("Command {} is not supported for channel: {}", command, channelUID.getId());
126         }
127     }
128
129     @Override
130     public void partialDetectionResult(PresenceDetectionValue value) {
131         updateState(CHANNEL_ONLINE, OnOffType.ON);
132         updateState(CHANNEL_LATENCY, new QuantityType<>(value.getLowestLatency(), MetricPrefix.MILLI(Units.SECOND)));
133         updateState(CHANNEL_DEPRECATED_TIME, new DecimalType(value.getLowestLatency()));
134     }
135
136     @Override
137     public void finalDetectionResult(PresenceDetectionValue value) {
138         // We do not notify the framework immediately if a device presence detection failed and
139         // the user configured retries to be > 1.
140         retryCounter = !value.isReachable() ? retryCounter + 1 : 0;
141
142         if (retryCounter >= this.retries) {
143             updateState(CHANNEL_ONLINE, OnOffType.OFF);
144             updateState(CHANNEL_LATENCY, UnDefType.UNDEF);
145             updateState(CHANNEL_DEPRECATED_TIME, UnDefType.UNDEF);
146             retryCounter = 0;
147         }
148
149         if (value.isReachable()) {
150             Instant instant = Instant.ofEpochMilli(presenceDetection.getLastSeen());
151             updateState(CHANNEL_LASTSEEN, new DateTimeType(
152                     ZonedDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
153         }
154
155         updateNetworkProperties();
156     }
157
158     @Override
159     public void dispose() {
160         PresenceDetection detection = presenceDetection;
161         if (detection != null) {
162             detection.stopAutomaticRefresh();
163         }
164         presenceDetection = null;
165     }
166
167     /**
168      * Initialize with a presenceDetection object.
169      * Used by testing for injecting.
170      */
171     void initialize(PresenceDetection presenceDetection) {
172         handlerConfiguration = getConfigAs(NetworkHandlerConfiguration.class);
173
174         this.presenceDetection = presenceDetection;
175         presenceDetection.setHostname(handlerConfiguration.hostname);
176         presenceDetection.setNetworkInterfaceNames(handlerConfiguration.networkInterfaceNames);
177         presenceDetection.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency);
178
179         if (isTCPServiceDevice) {
180             Integer port = handlerConfiguration.port;
181             if (port == null) {
182                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No port configured!");
183                 return;
184             }
185             presenceDetection.setServicePorts(Set.of(port));
186         } else {
187             // It does not harm to send an additional UDP packet to a device,
188             // therefore we assume all ping devices are iOS devices. If this
189             // does not work for all users for some obscure reason, we can make
190             // this a thing configuration variable.
191             presenceDetection.setIOSDevice(true);
192             // Hand over binding configurations to the network service
193             presenceDetection.setUseDhcpSniffing(configuration.allowDHCPlisten);
194             presenceDetection.setUseIcmpPing(configuration.allowSystemPings);
195             presenceDetection.setUseArpPing(true, configuration.arpPingToolPath, configuration.arpPingUtilMethod);
196         }
197
198         this.retries = handlerConfiguration.retry.intValue();
199         presenceDetection.setRefreshInterval(handlerConfiguration.refreshInterval.longValue());
200         presenceDetection.setTimeout(handlerConfiguration.timeout.intValue());
201
202         wakeOnLanPacketSender = new WakeOnLanPacketSender(handlerConfiguration.macAddress,
203                 handlerConfiguration.hostname, handlerConfiguration.port, handlerConfiguration.networkInterfaceNames);
204
205         updateStatus(ThingStatus.ONLINE);
206         presenceDetection.startAutomaticRefresh(scheduler);
207
208         updateNetworkProperties();
209     }
210
211     private void updateNetworkProperties() {
212         // Update properties (after startAutomaticRefresh, to get the correct dhcp state)
213         Map<String, String> properties = editProperties();
214         properties.put(NetworkBindingConstants.PROPERTY_ARP_STATE, presenceDetection.getArpPingState());
215         properties.put(NetworkBindingConstants.PROPERTY_ICMP_STATE, presenceDetection.getIPPingState());
216         properties.put(NetworkBindingConstants.PROPERTY_PRESENCE_DETECTION_TYPE, "");
217         properties.put(NetworkBindingConstants.PROPERTY_IOS_WAKEUP, presenceDetection.isIOSdevice() ? "Yes" : "No");
218         properties.put(NetworkBindingConstants.PROPERTY_DHCP_STATE, presenceDetection.getDhcpState());
219         updateProperties(properties);
220     }
221
222     // Create a new network service and apply all configurations.
223     @Override
224     public void initialize() {
225         initialize(new PresenceDetection(this, configuration.cacheDeviceStateTimeInMS.intValue()));
226     }
227
228     /**
229      * Returns true if this handler is for a TCP service device.
230      */
231     public boolean isTCPServiceDevice() {
232         return isTCPServiceDevice;
233     }
234
235     @Override
236     public void bindingConfigurationChanged() {
237         // Make sure that changed binding configuration is reflected
238         presenceDetection.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency);
239     }
240
241     @Override
242     public Collection<Class<? extends ThingHandlerService>> getServices() {
243         return List.of(NetworkActions.class);
244     }
245
246     public void sendWakeOnLanPacketViaIp() {
247         // Hostname can't be null
248         wakeOnLanPacketSender.sendWakeOnLanPacketViaIp();
249     }
250
251     public void sendWakeOnLanPacketViaMac() {
252         if (handlerConfiguration.macAddress.isEmpty()) {
253             throw new IllegalStateException(
254                     "Cannot send WoL packet because the 'macAddress' is not configured for " + thing.getUID());
255         }
256         wakeOnLanPacketSender.sendWakeOnLanPacketViaMac();
257     }
258 }