]> git.basschouten.com Git - openhab-addons.git/blob
eb2f89ce2b3211e7ff70ae01d0779460bed2eac7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.Collections;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Optional;
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 <tim@timwaterhouse.com> - Initial contribution
53  * @author Paul Smedley <paul@smedley.id.au> - Modifications to support Airbase Controllers
54  *
55  */
56 @Component(service = DiscoveryService.class)
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 final Runnable scanner;
66     private @Nullable ScheduledFuture<?> backgroundFuture;
67
68     public DaikinACUnitDiscoveryService() {
69         super(Collections.singleton(DaikinBindingConstants.THING_TYPE_AC_UNIT), 600, true);
70         scanner = createScanner();
71     }
72
73     @Override
74     protected void startScan() {
75         scheduler.execute(scanner);
76     }
77
78     @Override
79     protected void startBackgroundDiscovery() {
80         logger.trace("Starting background discovery");
81
82         if (backgroundFuture != null && !backgroundFuture.isDone()) {
83             backgroundFuture.cancel(true);
84             backgroundFuture = null;
85         }
86         backgroundFuture = scheduler.scheduleWithFixedDelay(scanner, 0, 60, TimeUnit.SECONDS);
87     }
88
89     @Override
90     protected void stopBackgroundDiscovery() {
91         if (backgroundFuture != null && !backgroundFuture.isDone()) {
92             backgroundFuture.cancel(true);
93             backgroundFuture = null;
94         }
95
96         super.stopBackgroundDiscovery();
97     }
98
99     private Runnable createScanner() {
100         return () -> {
101             long timestampOfLastScan = getTimestampOfLastScan();
102             for (InetAddress broadcastAddress : getBroadcastAddresses()) {
103                 logger.trace("Starting broadcast for {}", broadcastAddress.toString());
104
105                 try (DatagramSocket socket = new DatagramSocket()) {
106                     socket.setBroadcast(true);
107                     socket.setReuseAddress(true);
108                     byte[] packetContents = UDP_PACKET_CONTENTS.getBytes(StandardCharsets.UTF_8);
109                     DatagramPacket packet = new DatagramPacket(packetContents, packetContents.length, broadcastAddress,
110                             REMOTE_UDP_PORT);
111
112                     // Send before listening in case the port isn't bound until here.
113                     socket.send(packet);
114
115                     // receivePacketAndDiscover will return false if no packet is received after 1 second
116                     while (receivePacketAndDiscover(socket)) {
117                     }
118                 } catch (Exception e) {
119                     // Nothing to do here - the host couldn't be found, likely because it doesn't exist
120                 }
121             }
122
123             removeOlderResults(timestampOfLastScan);
124         };
125     }
126
127     private boolean receivePacketAndDiscover(DatagramSocket socket) {
128         try {
129             byte[] buffer = new byte[512];
130             DatagramPacket incomingPacket = new DatagramPacket(buffer, buffer.length);
131             socket.setSoTimeout(1000 /* one second */);
132             socket.receive(incomingPacket);
133
134             String host = incomingPacket.getAddress().toString().substring(1);
135             String data = new String(incomingPacket.getData(), 0, incomingPacket.getLength(), "US-ASCII");
136             logger.trace("Received packet from {}: {}", host, data);
137
138             Map<String, String> parsedData = InfoParser.parse(data);
139             Boolean secure = "1".equals(parsedData.get("en_secure"));
140             String thingId = Optional.ofNullable(parsedData.get("ssid")).orElse(host.replace(".", "_"));
141             String mac = Optional.ofNullable(parsedData.get("mac")).orElse("");
142             String uuid = mac.isEmpty() ? UUID.randomUUID().toString()
143                     : UUID.nameUUIDFromBytes(mac.getBytes()).toString();
144
145             DaikinWebTargets webTargets = new DaikinWebTargets(httpClient, host, secure, null);
146             boolean found = false;
147
148             // look for Daikin controller
149             try {
150                 found = "OK".equals(webTargets.getBasicInfo().ret);
151             } catch (DaikinCommunicationForbiddenException e) {
152                 // At this point, we don't have the adapter's key nor a uuid
153                 // so we're getting a Forbidden error
154                 // let's discover it and let the user configure the Key
155                 found = true;
156             }
157             if (found) {
158                 ThingUID thingUID = new ThingUID(DaikinBindingConstants.THING_TYPE_AC_UNIT, thingId);
159                 DiscoveryResultBuilder resultBuilder = DiscoveryResultBuilder.create(thingUID)
160                         .withProperty(DaikinConfiguration.HOST, host).withLabel("Daikin AC Unit (" + host + ")")
161                         .withProperty(DaikinConfiguration.SECURE, secure)
162                         .withRepresentationProperty(DaikinConfiguration.HOST);
163                 if (secure) {
164                     resultBuilder = resultBuilder.withProperty(DaikinConfiguration.UUID, uuid);
165                 }
166                 DiscoveryResult result = resultBuilder.build();
167
168                 logger.trace("Successfully discovered host {}", host);
169                 thingDiscovered(result);
170                 return true;
171             }
172             // look for Daikin Airbase controller
173             if ("OK".equals(webTargets.getAirbaseBasicInfo().ret)) {
174                 ThingUID thingUID = new ThingUID(DaikinBindingConstants.THING_TYPE_AIRBASE_AC_UNIT, thingId);
175                 DiscoveryResult result = DiscoveryResultBuilder.create(thingUID)
176                         .withProperty(DaikinConfiguration.HOST, host).withLabel("Daikin Airbase AC Unit (" + host + ")")
177                         .withRepresentationProperty(DaikinConfiguration.HOST).build();
178
179                 logger.trace("Successfully discovered host {}", host);
180                 thingDiscovered(result);
181                 return true;
182             }
183         } catch (Exception e) {
184             return false;
185         }
186         // Shouldn't get here unless we don't detect a controller.
187         // Return true to continue with the next packet, which comes from another adapter
188         return true;
189     }
190
191     private List<InetAddress> getBroadcastAddresses() {
192         ArrayList<InetAddress> addresses = new ArrayList<>();
193
194         for (String broadcastAddress : NetUtil.getAllBroadcastAddresses()) {
195             try {
196                 addresses.add(InetAddress.getByName(broadcastAddress));
197             } catch (UnknownHostException e) {
198                 logger.debug("Error broadcasting to {}", broadcastAddress, e);
199             }
200         }
201
202         return addresses;
203     }
204
205     @Reference
206     protected void setDaikinHttpClientFactory(final DaikinHttpClientFactory httpClientFactory) {
207         this.httpClient = httpClientFactory.getHttpClient();
208     }
209 }