]> git.basschouten.com Git - openhab-addons.git/blob
08f7d3c5414b132029b328d07be7e9d2d1cfc1eb
[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.daikin.internal.discovery;
14
15 import java.net.DatagramPacket;
16 import java.net.DatagramSocket;
17 import java.net.InetAddress;
18 import java.net.UnknownHostException;
19 import java.nio.charset.StandardCharsets;
20 import java.util.ArrayList;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Optional;
24 import java.util.Set;
25 import java.util.UUID;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.openhab.binding.daikin.internal.DaikinBindingConstants;
33 import org.openhab.binding.daikin.internal.DaikinCommunicationForbiddenException;
34 import org.openhab.binding.daikin.internal.DaikinHttpClientFactory;
35 import org.openhab.binding.daikin.internal.DaikinWebTargets;
36 import org.openhab.binding.daikin.internal.api.InfoParser;
37 import org.openhab.binding.daikin.internal.config.DaikinConfiguration;
38 import org.openhab.core.config.discovery.AbstractDiscoveryService;
39 import org.openhab.core.config.discovery.DiscoveryResult;
40 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
41 import org.openhab.core.config.discovery.DiscoveryService;
42 import org.openhab.core.net.NetUtil;
43 import org.openhab.core.thing.ThingUID;
44 import org.osgi.service.component.annotations.Component;
45 import org.osgi.service.component.annotations.Reference;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 /**
50  * Discovery service for Daikin AC units.
51  *
52  * @author Tim Waterhouse - Initial contribution
53  * @author Paul Smedley - Modifications to support Airbase Controllers
54  *
55  */
56 @Component(service = DiscoveryService.class, configurationPid = "discovery.daikin")
57 @NonNullByDefault
58 public class DaikinACUnitDiscoveryService extends AbstractDiscoveryService {
59     private static final String UDP_PACKET_CONTENTS = "DAIKIN_UDP/common/basic_info";
60     private static final int REMOTE_UDP_PORT = 30050;
61
62     private Logger logger = LoggerFactory.getLogger(DaikinACUnitDiscoveryService.class);
63
64     private @Nullable HttpClient httpClient;
65     private @Nullable ScheduledFuture<?> backgroundFuture;
66
67     public DaikinACUnitDiscoveryService() {
68         super(Set.of(DaikinBindingConstants.THING_TYPE_AC_UNIT), 600, true);
69     }
70
71     @Override
72     protected void startScan() {
73         scheduler.execute(this::scanner);
74     }
75
76     @Override
77     protected void startBackgroundDiscovery() {
78         logger.trace("Starting background discovery");
79
80         if (backgroundFuture != null && !backgroundFuture.isDone()) {
81             backgroundFuture.cancel(true);
82             backgroundFuture = null;
83         }
84         backgroundFuture = scheduler.scheduleWithFixedDelay(this::scanner, 0, 60, TimeUnit.SECONDS);
85     }
86
87     @Override
88     protected void stopBackgroundDiscovery() {
89         if (backgroundFuture != null && !backgroundFuture.isDone()) {
90             backgroundFuture.cancel(true);
91             backgroundFuture = null;
92         }
93
94         super.stopBackgroundDiscovery();
95     }
96
97     private void scanner() {
98         long timestampOfLastScan = getTimestampOfLastScan();
99         for (InetAddress broadcastAddress : getBroadcastAddresses()) {
100             logger.trace("Starting broadcast for {}", broadcastAddress.toString());
101
102             try (DatagramSocket socket = new DatagramSocket()) {
103                 socket.setBroadcast(true);
104                 socket.setReuseAddress(true);
105                 byte[] packetContents = UDP_PACKET_CONTENTS.getBytes(StandardCharsets.UTF_8);
106                 DatagramPacket packet = new DatagramPacket(packetContents, packetContents.length, broadcastAddress,
107                         REMOTE_UDP_PORT);
108
109                 // Send before listening in case the port isn't bound until here.
110                 socket.send(packet);
111
112                 // receivePacketAndDiscover will return false if no packet is received after 1 second
113                 while (receivePacketAndDiscover(socket)) {
114                 }
115             } catch (Exception e) {
116                 // Nothing to do here - the host couldn't be found, likely because it doesn't exist
117             }
118         }
119
120         removeOlderResults(timestampOfLastScan);
121     }
122
123     private boolean receivePacketAndDiscover(DatagramSocket socket) {
124         try {
125             byte[] buffer = new byte[512];
126             DatagramPacket incomingPacket = new DatagramPacket(buffer, buffer.length);
127             socket.setSoTimeout(1000 /* one second */);
128             socket.receive(incomingPacket);
129
130             String host = incomingPacket.getAddress().toString().substring(1);
131             String data = new String(incomingPacket.getData(), 0, incomingPacket.getLength(), "US-ASCII");
132             logger.trace("Received packet from {}: {}", host, data);
133
134             Map<String, String> parsedData = InfoParser.parse(data);
135             Boolean secure = "1".equals(parsedData.get("en_secure"));
136             String thingId = Optional.ofNullable(parsedData.get("ssid")).orElse(host.replace(".", "_"));
137             String mac = Optional.ofNullable(parsedData.get("mac")).orElse("");
138             String uuid = mac.isEmpty() ? UUID.randomUUID().toString()
139                     : UUID.nameUUIDFromBytes(mac.getBytes()).toString();
140
141             DaikinWebTargets webTargets = new DaikinWebTargets(httpClient, host, secure, null);
142             boolean found = false;
143
144             // look for Daikin controller
145             try {
146                 found = "OK".equals(webTargets.getBasicInfo().ret);
147             } catch (DaikinCommunicationForbiddenException e) {
148                 // At this point, we don't have the adapter's key nor a uuid
149                 // so we're getting a Forbidden error
150                 // let's discover it and let the user configure the Key
151                 found = true;
152             }
153             if (found) {
154                 ThingUID thingUID = new ThingUID(DaikinBindingConstants.THING_TYPE_AC_UNIT, thingId);
155                 DiscoveryResultBuilder resultBuilder = DiscoveryResultBuilder.create(thingUID)
156                         .withProperty(DaikinConfiguration.HOST, host).withLabel("Daikin AC Unit (" + host + ")")
157                         .withProperty(DaikinConfiguration.SECURE, secure)
158                         .withRepresentationProperty(DaikinConfiguration.HOST);
159                 if (secure) {
160                     resultBuilder = resultBuilder.withProperty(DaikinConfiguration.UUID, uuid);
161                 }
162                 DiscoveryResult result = resultBuilder.build();
163
164                 logger.trace("Successfully discovered host {}", host);
165                 thingDiscovered(result);
166                 return true;
167             }
168             // look for Daikin Airbase controller
169             if ("OK".equals(webTargets.getAirbaseBasicInfo().ret)) {
170                 ThingUID thingUID = new ThingUID(DaikinBindingConstants.THING_TYPE_AIRBASE_AC_UNIT, thingId);
171                 DiscoveryResult result = DiscoveryResultBuilder.create(thingUID)
172                         .withProperty(DaikinConfiguration.HOST, host).withLabel("Daikin Airbase AC Unit (" + host + ")")
173                         .withRepresentationProperty(DaikinConfiguration.HOST).build();
174
175                 logger.trace("Successfully discovered host {}", host);
176                 thingDiscovered(result);
177                 return true;
178             }
179         } catch (Exception e) {
180             return false;
181         }
182         // Shouldn't get here unless we don't detect a controller.
183         // Return true to continue with the next packet, which comes from another adapter
184         return true;
185     }
186
187     private List<InetAddress> getBroadcastAddresses() {
188         ArrayList<InetAddress> addresses = new ArrayList<>();
189
190         for (String broadcastAddress : NetUtil.getAllBroadcastAddresses()) {
191             try {
192                 addresses.add(InetAddress.getByName(broadcastAddress));
193             } catch (UnknownHostException e) {
194                 logger.debug("Error broadcasting to {}", broadcastAddress, e);
195             }
196         }
197
198         return addresses;
199     }
200
201     @Reference
202     protected void setDaikinHttpClientFactory(final DaikinHttpClientFactory httpClientFactory) {
203         this.httpClient = httpClientFactory.getHttpClient();
204     }
205 }