]> git.basschouten.com Git - openhab-addons.git/blob
d983bc9f09cf57321406c870e7e5c55b75e1991b
[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.cbus.handler;
14
15 import java.net.InetAddress;
16 import java.net.UnknownHostException;
17 import java.util.Arrays;
18 import java.util.GregorianCalendar;
19 import java.util.LinkedList;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.cbus.CBusBindingConstants;
26 import org.openhab.binding.cbus.internal.CBusCGateConfiguration;
27 import org.openhab.binding.cbus.internal.CBusThreadPool;
28 import org.openhab.core.thing.Bridge;
29 import org.openhab.core.thing.ChannelUID;
30 import org.openhab.core.thing.Thing;
31 import org.openhab.core.thing.ThingStatus;
32 import org.openhab.core.thing.ThingStatusDetail;
33 import org.openhab.core.thing.binding.BaseBridgeHandler;
34 import org.openhab.core.thing.binding.ThingHandler;
35 import org.openhab.core.types.Command;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 import com.daveoxley.cbus.CGateConnectException;
40 import com.daveoxley.cbus.CGateException;
41 import com.daveoxley.cbus.CGateInterface;
42 import com.daveoxley.cbus.CGateSession;
43 import com.daveoxley.cbus.events.EventCallback;
44 import com.daveoxley.cbus.status.StatusChangeCallback;
45
46 /**
47  * The {@link CBusCGateHandler} is responsible for handling commands, which are
48  * sent to one of the channels.
49  *
50  * @author Scott Linton - Initial contribution
51  */
52
53 @NonNullByDefault
54 public class CBusCGateHandler extends BaseBridgeHandler {
55
56     private final Logger logger = LoggerFactory.getLogger(CBusCGateHandler.class);
57     private @Nullable InetAddress ipAddress;
58     public @Nullable CGateSession cGateSession;
59     private @Nullable ScheduledFuture<?> keepAliveFuture;
60
61     public CBusCGateHandler(Bridge br) {
62         super(br);
63     }
64
65     // This is abstract in base class so have to implement it.
66     @Override
67     public void handleCommand(ChannelUID channelUID, Command command) {
68     }
69
70     @Override
71     public void initialize() {
72         updateStatus(ThingStatus.OFFLINE);
73         logger.debug("Initializing CGate Bridge handler. {} {}", getThing().getThingTypeUID(), getThing().getUID());
74         CBusCGateConfiguration configuration = getConfigAs(CBusCGateConfiguration.class);
75         logger.debug("Using configuration {}", configuration);
76         try {
77             this.ipAddress = InetAddress.getByName(configuration.ipAddress);
78         } catch (UnknownHostException e1) {
79             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
80                     "IP Address not resolvable");
81             return;
82         }
83
84         InetAddress address = this.ipAddress;
85         if (address != null) {
86             logger.debug("CGate IP         {}.", address.getHostAddress());
87         }
88
89         keepAliveFuture = scheduler.scheduleWithFixedDelay(this::keepAlive, 0, 100, TimeUnit.SECONDS);
90     }
91
92     private void keepAlive() {
93         CGateSession session = cGateSession;
94         if (session == null || !session.isConnected()) {
95             if (!getThing().getStatus().equals(ThingStatus.ONLINE)) {
96                 connect();
97             } else {
98                 updateStatus();
99             }
100         }
101     }
102
103     private void connect() {
104         CGateSession cGateSession = this.cGateSession;
105         if (cGateSession == null) {
106             cGateSession = CGateInterface.connect(this.ipAddress, 20023, 20024, 20025, new CBusThreadPool());
107             cGateSession.registerEventCallback(new EventMonitor());
108             cGateSession.registerStatusChangeCallback(new StatusChangeMonitor());
109             this.cGateSession = cGateSession;
110         }
111         if (cGateSession.isConnected()) {
112             logger.debug("CGate session reports online");
113             updateStatus(ThingStatus.ONLINE);
114         } else {
115             try {
116                 cGateSession.connect();
117                 updateStatus();
118             } catch (CGateConnectException e) {
119                 updateStatus();
120                 logger.debug("Failed to connect to CGate:", e);
121                 try {
122                     cGateSession.close();
123                 } catch (CGateException ignore) {
124                     // We dont really care if an exception is thrown when clossing the connection after a failure
125                     // connecting.
126                 }
127             }
128         }
129     }
130
131     private void updateStatus() {
132         ThingStatus lastStatus = getThing().getStatus();
133         CGateSession cGateSession = this.cGateSession;
134         if (cGateSession == null) {
135             return;
136         }
137         if (cGateSession.isConnected()) {
138             updateStatus(ThingStatus.ONLINE);
139         } else {
140             if (lastStatus != ThingStatus.OFFLINE) {
141                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
142             }
143         }
144         if (!getThing().getStatus().equals(lastStatus)) {
145             boolean isOnline = getThing().getStatus().equals(ThingStatus.ONLINE);
146             updateChildThings(isOnline);
147         }
148     }
149
150     private void updateChildThings(boolean isOnline) {
151         scheduler.execute(() -> {
152             // now also re-initialize all network handlers
153             for (Thing thing : getThing().getThings()) {
154                 ThingHandler handler = thing.getHandler();
155                 if (handler instanceof CBusNetworkHandler networkHandler) {
156                     networkHandler.cgateStateChanged(isOnline);
157                 }
158             }
159         });
160     }
161
162     private void updateGroup(@Nullable String address, @Nullable String value) {
163         if (address == null || value == null) {
164             return;
165         }
166         logger.debug("updateGroup address {}", address);
167         // Address should be of the form //Project/network/application/group
168         if (!address.startsWith("//")) {
169             logger.debug("Address does not start with // so ignoring this update");
170             return;
171         }
172         String[] addressParts = address.substring(2).split("/");
173         if (addressParts.length != 4) {
174             logger.debug("Address is badly formed so ignoring this update length of parts is {} not 4",
175                     addressParts.length);
176             return;
177         }
178         updateGroup(Integer.parseInt(addressParts[1]), Integer.parseInt(addressParts[2]),
179                 Integer.parseInt(addressParts[3]), value);
180     }
181
182     private void updateGroup(int network, int application, int group, String value) {
183         for (Thing networkThing : getThing().getThings()) {
184             // Is this networkThing from the network we are looking for...
185             if (networkThing.getThingTypeUID().equals(CBusBindingConstants.BRIDGE_TYPE_NETWORK)) {
186                 CBusNetworkHandler netThingHandler = (CBusNetworkHandler) networkThing.getHandler();
187                 if (netThingHandler == null || netThingHandler.getNetworkId() != network) {
188                     continue;
189                 }
190                 // Loop through all the things on this network and see if they match the application / group
191                 for (Thing thing : netThingHandler.getThing().getThings()) {
192                     ThingHandler thingThingHandler = thing.getHandler();
193                     if (thingThingHandler == null) {
194                         continue;
195                     }
196
197                     if (thingThingHandler instanceof CBusGroupHandler groupHandler) {
198                         groupHandler.updateGroup(application, group, value);
199                     }
200                 }
201             }
202         }
203     }
204
205     public @Nullable CGateSession getCGateSession() {
206         return cGateSession;
207     }
208
209     @Override
210     public void dispose() {
211         ScheduledFuture<?> keepAliveFuture = this.keepAliveFuture;
212         if (keepAliveFuture != null) {
213             keepAliveFuture.cancel(true);
214         }
215         CGateSession cGateSession = this.cGateSession;
216         if (cGateSession != null && cGateSession.isConnected()) {
217             try {
218                 cGateSession.close();
219             } catch (CGateException e) {
220                 logger.warn("Cannot close CGate session", e);
221             }
222         } else {
223             logger.debug("no session or it is disconnected");
224         }
225         super.dispose();
226     }
227
228     @NonNullByDefault
229     private class EventMonitor extends EventCallback {
230
231         @Override
232         public boolean acceptEvent(int eventCode) {
233             return true;
234         }
235
236         @Override
237         public void processEvent(@Nullable CGateSession cgate_session, int eventCode,
238                 @Nullable GregorianCalendar event_time, @Nullable String event) {
239             if (event == null) {
240                 return;
241             }
242
243             if (eventCode == 701) {
244                 // By Marking this as Nullable it fools the static analyser into understanding that poll can return Null
245                 LinkedList<@Nullable String> tokenizer = new LinkedList<>(Arrays.asList(event.trim().split("\\s+")));
246                 @Nullable
247                 String address = tokenizer.poll();
248                 tokenizer.poll();
249                 @Nullable
250                 String value = tokenizer.poll();
251                 if (value != null && value.startsWith("level=")) {
252                     String level = value.replace("level=", "");
253                     updateGroup(address, level);
254                 }
255             }
256         }
257     }
258
259     @NonNullByDefault
260     private class StatusChangeMonitor extends StatusChangeCallback {
261
262         @Override
263         public boolean isActive() {
264             return true;
265         }
266
267         @Override
268         public void processStatusChange(@Nullable CGateSession cGateSession, @Nullable String status) {
269             if (cGateSession == null || status == null) {
270                 return;
271             }
272             if (status.startsWith("# ")) {
273                 status = status.substring(2);
274                 // Shouldnt need to check for null but this silences a warning
275                 if (status == null || status.isEmpty()) {
276                     return;
277                 }
278             }
279             logger.debug("ProcessStatusChange {}", status);
280             String[] contents = status.split("#");
281             if (cGateSession.getSessionID() != null
282                     && contents[1].contains("sessionId=" + cGateSession.getSessionID())) {
283                 // We created this event - don't worry about processing it again...
284                 return;
285             }
286             // By Marking this as Nullable it fools the static analyser into understanding that poll can return Null
287             LinkedList<@Nullable String> tokenizer = new LinkedList<>(Arrays.asList(contents[0].split("\\s+")));
288             @Nullable
289             String firstToken = tokenizer.poll();
290             if (firstToken == null) {
291                 logger.debug("ProcessStateChange: Cant tokenize status {}", status);
292                 return;
293             }
294             switch (firstToken) {
295                 case "lighting": {
296                     @Nullable
297                     String state = tokenizer.poll();
298                     @Nullable
299                     String address = tokenizer.poll();
300                     if ("ramp".equals(state)) {
301                         state = tokenizer.poll();
302                     }
303                     updateGroup(address, state);
304                     break;
305                 }
306                 case "temperature": {
307                     // For temperature we ignore the state
308                     tokenizer.poll();
309                     @Nullable
310                     String address = tokenizer.poll();
311                     @Nullable
312                     String temp = tokenizer.poll();
313                     updateGroup(address, temp);
314                     break;
315                 }
316                 case "trigger": {
317                     @Nullable
318                     String command = tokenizer.poll();
319                     @Nullable
320                     String address = tokenizer.poll();
321                     if ("event".equals(command)) {
322                         @Nullable
323                         String level = tokenizer.poll();
324                         updateGroup(address, level);
325                     } else if ("indicatorkill".equals(command)) {
326                         updateGroup(address, "-1");
327                     } else {
328                         logger.warn("Unhandled trigger command {} - status {}", command, status);
329                     }
330                     break;
331                 }
332                 case "clock": {
333                     @Nullable
334                     String address = "";
335                     @Nullable
336                     String value = "";
337                     @Nullable
338                     String type = tokenizer.poll();
339                     if ("date".equals(type)) {
340                         address = tokenizer.poll() + "/1";
341                         value = tokenizer.poll();
342                     } else if ("time".equals(type)) {
343                         address = tokenizer.poll() + "/0";
344                         value = tokenizer.poll();
345                     } else if (!"request_refresh".equals(type)) {
346                         // We dont handle request_refresh as we are not a clock master
347                         logger.debug("Received unknown clock event: {}", status);
348                     }
349                     if (value != null && !value.isEmpty()) {
350                         updateGroup(address, value);
351                     }
352                     break;
353                 }
354                 default: {
355                     LinkedList<String> commentTokenizer = new LinkedList<>(Arrays.asList(contents[1].split("\\s+")));
356                     if ("lighting".equals(commentTokenizer.peek())) {
357                         commentTokenizer.poll();
358                         @Nullable
359                         String commentToken = commentTokenizer.peek();
360
361                         if ("SyncUpdate".equals(commentToken)) {
362                             commentTokenizer.poll();
363                             @Nullable
364                             String address = commentTokenizer.poll();
365
366                             @Nullable
367                             String level = commentTokenizer.poll();
368                             level = level.replace("level=", "");
369                             updateGroup(address, level);
370                         }
371                     } else {
372                         logger.debug("Received unparsed event: '{}'", status);
373                     }
374                     break;
375                 }
376             }
377         }
378     }
379 }