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.cbus.handler;
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;
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;
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;
47 * The {@link CBusCGateHandler} is responsible for handling commands, which are
48 * sent to one of the channels.
50 * @author Scott Linton - Initial contribution
54 public class CBusCGateHandler extends BaseBridgeHandler {
56 private final Logger logger = LoggerFactory.getLogger(CBusCGateHandler.class);
57 private @Nullable InetAddress ipAddress;
58 public @Nullable CGateSession cGateSession;
59 private @Nullable ScheduledFuture<?> keepAliveFuture;
61 public CBusCGateHandler(Bridge br) {
65 // This is abstract in base class so have to implement it.
67 public void handleCommand(ChannelUID channelUID, Command command) {
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);
77 this.ipAddress = InetAddress.getByName(configuration.ipAddress);
78 } catch (UnknownHostException e1) {
79 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
80 "IP Address not resolvable");
84 InetAddress address = this.ipAddress;
85 if (address != null) {
86 logger.debug("CGate IP {}.", address.getHostAddress());
89 keepAliveFuture = scheduler.scheduleWithFixedDelay(this::keepAlive, 0, 100, TimeUnit.SECONDS);
92 private void keepAlive() {
93 CGateSession session = cGateSession;
94 if (session == null || !session.isConnected()) {
95 if (!getThing().getStatus().equals(ThingStatus.ONLINE)) {
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;
111 if (cGateSession.isConnected()) {
112 logger.debug("CGate session reports online");
113 updateStatus(ThingStatus.ONLINE);
116 cGateSession.connect();
118 } catch (CGateConnectException e) {
120 logger.debug("Failed to connect to CGate:", e);
122 cGateSession.close();
123 } catch (CGateException ignore) {
124 // We dont really care if an exception is thrown when clossing the connection after a failure
131 private void updateStatus() {
132 ThingStatus lastStatus = getThing().getStatus();
133 CGateSession cGateSession = this.cGateSession;
134 if (cGateSession == null) {
137 if (cGateSession.isConnected()) {
138 updateStatus(ThingStatus.ONLINE);
140 if (lastStatus != ThingStatus.OFFLINE) {
141 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
144 if (!getThing().getStatus().equals(lastStatus)) {
145 boolean isOnline = getThing().getStatus().equals(ThingStatus.ONLINE);
146 updateChildThings(isOnline);
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) {
156 ((CBusNetworkHandler) handler).cgateStateChanged(isOnline);
162 private void updateGroup(@Nullable String address, @Nullable String value) {
163 if (address == null || value == null) {
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");
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);
178 updateGroup(Integer.parseInt(addressParts[1]), Integer.parseInt(addressParts[2]),
179 Integer.parseInt(addressParts[3]), value);
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) {
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) {
197 if (thingThingHandler instanceof CBusGroupHandler) {
198 ((CBusGroupHandler) thingThingHandler).updateGroup(application, group, value);
205 public @Nullable CGateSession getCGateSession() {
210 public void dispose() {
211 ScheduledFuture<?> keepAliveFuture = this.keepAliveFuture;
212 if (keepAliveFuture != null) {
213 keepAliveFuture.cancel(true);
215 CGateSession cGateSession = this.cGateSession;
216 if (cGateSession != null && cGateSession.isConnected()) {
218 cGateSession.close();
219 } catch (CGateException e) {
220 logger.warn("Cannot close CGate session", e);
223 logger.debug("no session or it is disconnected");
229 private class EventMonitor extends EventCallback {
232 public boolean acceptEvent(int eventCode) {
237 public void processEvent(@Nullable CGateSession cgate_session, int eventCode,
238 @Nullable GregorianCalendar event_time, @Nullable String event) {
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+")));
247 String address = tokenizer.poll();
250 String value = tokenizer.poll();
251 if (value != null && value.startsWith("level=")) {
252 String level = value.replace("level=", "");
253 updateGroup(address, level);
260 private class StatusChangeMonitor extends StatusChangeCallback {
263 public boolean isActive() {
268 public void processStatusChange(@Nullable CGateSession cGateSession, @Nullable String status) {
269 if (cGateSession == null || status == null) {
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()) {
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...
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+")));
289 String firstToken = tokenizer.poll();
290 if (firstToken == null) {
291 logger.debug("ProcessStateChange: Cant tokenize status {}", status);
294 switch (firstToken) {
297 String state = tokenizer.poll();
299 String address = tokenizer.poll();
300 if ("ramp".equals(state)) {
301 state = tokenizer.poll();
303 updateGroup(address, state);
306 case "temperature": {
307 // For temperature we ignore the state
310 String address = tokenizer.poll();
312 String temp = tokenizer.poll();
313 updateGroup(address, temp);
318 String command = tokenizer.poll();
320 String address = tokenizer.poll();
321 if ("event".equals(command)) {
323 String level = tokenizer.poll();
324 updateGroup(address, level);
325 } else if ("indicatorkill".equals(command)) {
326 updateGroup(address, "-1");
328 logger.warn("Unhandled trigger command {} - status {}", command, status);
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);
349 if (value != null && !value.isEmpty()) {
350 updateGroup(address, value);
355 LinkedList<String> commentTokenizer = new LinkedList<>(Arrays.asList(contents[1].split("\\s+")));
356 if ("lighting".equals(commentTokenizer.peek())) {
357 commentTokenizer.poll();
359 String commentToken = commentTokenizer.peek();
361 if ("SyncUpdate".equals(commentToken)) {
362 commentTokenizer.poll();
364 String address = commentTokenizer.poll();
367 String level = commentTokenizer.poll();
368 level = level.replace("level=", "");
369 updateGroup(address, level);
372 logger.debug("Received unparsed event: '{}'", status);