added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.adorne-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-adorne" description="Adorne Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.adorne/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link AdorneBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "adorne";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_HUB = new ThingTypeUID(BINDING_ID, "hub");
|
||||
public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
|
||||
public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_POWER = "power";
|
||||
public static final String CHANNEL_BRIGHTNESS = "brightness";
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link AdorneDeviceState} class defines a simple POJO representing the Adorne device state.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneDeviceState {
|
||||
public final int zoneId;
|
||||
public final String name;
|
||||
public final ThingTypeUID deviceType;
|
||||
public final boolean onOff;
|
||||
public final int brightness;
|
||||
|
||||
public AdorneDeviceState(int zoneId, String name, ThingTypeUID deviceType, boolean onOff, int brightness) {
|
||||
this.zoneId = zoneId;
|
||||
this.name = name;
|
||||
this.deviceType = deviceType;
|
||||
this.onOff = onOff;
|
||||
this.brightness = brightness;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.configuration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubConfiguration} class represents the hub configuration options.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneHubConfiguration {
|
||||
public String host = "LCM1.local";
|
||||
public Integer port = 2112;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.configuration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link AdorneSwitchConfiguration} class represents the switch configuration options.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneSwitchConfiguration {
|
||||
public @Nullable Integer zoneId;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.adorne.internal.configuration.AdorneHubConfiguration;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubChangeNotify;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.util.UIDUtils;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AdorneDiscoveryService} discovers things for the Adorne hub and Adorne devices.
|
||||
* Discovery is only supported if the hub is accessible via default host and port.
|
||||
*
|
||||
* @author Mark Theiding - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.adorne")
|
||||
public class AdorneDiscoveryService extends AbstractDiscoveryService implements AdorneHubChangeNotify {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneDiscoveryService.class);
|
||||
private static final int DISCOVERY_TIMEOUT_SECONDS = 10;
|
||||
private static final String DISCOVERY_HUB_LABEL = "Adorne Hub";
|
||||
private static final String DISCOVERY_ZONE_ID = "zoneId";
|
||||
private @Nullable AdorneHubController adorneHubController;
|
||||
|
||||
/**
|
||||
* Creates a AdorneDiscoveryService with disabled auto-discovery.
|
||||
*/
|
||||
public AdorneDiscoveryService() {
|
||||
// Passing false as last argument to super constructor turns off background discovery
|
||||
super(Collections.singleton(new ThingTypeUID(BINDING_ID, "-")), DISCOVERY_TIMEOUT_SECONDS, false);
|
||||
|
||||
// We create the hub controller with default host and port. In the future we could let users create hubs
|
||||
// manually with custom host and port settings and then perform discovery here for those hubs.
|
||||
adorneHubController = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick off discovery of all devices on the hub
|
||||
*/
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Discovery scan started");
|
||||
|
||||
AdorneHubController adorneHubController = new AdorneHubController(new AdorneHubConfiguration(), scheduler,
|
||||
this);
|
||||
this.adorneHubController = adorneHubController;
|
||||
|
||||
// Hack - we wrap the ThingUID in an array to make it appear effectively final to the compiler throughout the
|
||||
// chain of futures. Passing it through the chain as context would bloat the code.
|
||||
ThingUID[] bridgeUID = new ThingUID[1];
|
||||
|
||||
// Future enhancement: Need a timeout for each future execution to recover from bugs in the hub controller, but
|
||||
// Java8 doesn't yet offer that
|
||||
adorneHubController.start().thenCompose(Void -> {
|
||||
// We use the hub's MAC address as its unique identifier
|
||||
return adorneHubController.getMACAddress();
|
||||
}).thenCompose(macAddress -> {
|
||||
String macAddressNoColon = macAddress.replace(':', '-'); // Colons are not allowed in ThingUIDs
|
||||
bridgeUID[0] = new ThingUID(THING_TYPE_HUB, macAddressNoColon);
|
||||
// We have fully discovered the hub
|
||||
thingDiscovered(DiscoveryResultBuilder.create(bridgeUID[0]).withLabel(DISCOVERY_HUB_LABEL).build());
|
||||
return adorneHubController.getZones();
|
||||
}).thenAccept(zoneIds -> {
|
||||
zoneIds.forEach(zoneId -> {
|
||||
adorneHubController.getState(zoneId).thenAccept(state -> {
|
||||
String id = UIDUtils.encode(state.name); // Strip zone ID's name to become a valid ThingUID
|
||||
// We have fully discovered a new zone ID
|
||||
thingDiscovered(DiscoveryResultBuilder
|
||||
.create(new ThingUID(state.deviceType, bridgeUID[0], id.toLowerCase()))
|
||||
.withLabel(state.name).withBridge(bridgeUID[0])
|
||||
.withProperty(DISCOVERY_ZONE_ID, state.zoneId).build());
|
||||
}).exceptionally(e -> {
|
||||
logger.warn("Discovery of zone ID {} failed ({})", zoneId, e.getMessage());
|
||||
return null;
|
||||
});
|
||||
});
|
||||
adorneHubController.stopWhenCommandsServed(); // Shut down hub once all discovery requests have been served
|
||||
}).exceptionally(e -> {
|
||||
logger.warn("Discovery failed ({})", e.getMessage());
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification to stop scanning
|
||||
*/
|
||||
@Override
|
||||
protected void stopScan() {
|
||||
super.stopScan();
|
||||
|
||||
AdorneHubController adorneHubController = this.adorneHubController;
|
||||
if (adorneHubController != null) {
|
||||
adorneHubController.stop();
|
||||
this.adorneHubController = null;
|
||||
logger.debug("Discovery timed out. Scan stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to do on change notifications
|
||||
@Override
|
||||
public void stateChangeNotify(int zoneId, boolean onOff, int brightness) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectionChangeNotify(boolean connected) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.handler;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.CHANNEL_BRIGHTNESS;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AdorneDimmerHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels. It supports the brightness channel in addition to the inherited switch channel.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneDimmerHandler extends AdorneSwitchHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneDimmerHandler.class);
|
||||
|
||||
public AdorneDimmerHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles refresh and percent commands for channel
|
||||
* {@link org.openhab.binding.adorne.internal.AdorneBindingConstants#CHANNEL_BRIGHTNESS}
|
||||
* It delegates all other commands to its parent class.
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("handleCommand (channelUID:{} command:{}", channelUID, command);
|
||||
try {
|
||||
if (channelUID.getId().equals(CHANNEL_BRIGHTNESS)) {
|
||||
if (command instanceof RefreshType) {
|
||||
refreshBrightness();
|
||||
} else if (command instanceof PercentType) {
|
||||
// Change the brightness through the hub controller
|
||||
AdorneHubController adorneHubController = getAdorneHubController();
|
||||
int level = ((PercentType) command).intValue();
|
||||
if (level >= 1 && level <= 100) { // Ignore commands outside of the supported 1-100 range
|
||||
adorneHubController.setBrightness(zoneId, level);
|
||||
} else {
|
||||
logger.debug("Ignored command to set brightness to level {}", level);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.handleCommand(channelUID, command); // Parent can handle everything else
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
// Hub controller could't handle our commands. Unfortunately the framework has no mechanism to report
|
||||
// runtime errors. If we throw the exception up the framework logs it as an error - we don't want that - we
|
||||
// want the framework to handle it gracefully. No point to update the thing status, since the
|
||||
// AdorneHubController already does that. So we are forced to swallow the exception here.
|
||||
logger.debug("Failed to execute command {} for channel {} for thing {} ({})", command, channelUID,
|
||||
getThing().getLabel(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the brightness of our thing to the actual state of the device.
|
||||
*
|
||||
*/
|
||||
public void refreshBrightness() {
|
||||
// Asynchronously get our brightness from the hub controller and update our state accordingly
|
||||
AdorneHubController adorneHubController = getAdorneHubController();
|
||||
adorneHubController.getState(zoneId).thenAccept(state -> {
|
||||
updateState(CHANNEL_BRIGHTNESS, new PercentType(state.brightness));
|
||||
logger.debug("Refreshed dimmer {} with brightness {}", getThing().getLabel(), state.brightness);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes all supported channels.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void refresh() {
|
||||
super.refresh();
|
||||
refreshBrightness();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.handler;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHandlerFactory} is responsible for creating thing handlers.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.adorne", service = ThingHandlerFactory.class)
|
||||
public class AdorneHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneHandlerFactory.class);
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
|
||||
Stream.of(THING_TYPE_HUB, THING_TYPE_SWITCH, THING_TYPE_DIMMER).collect(Collectors.toSet()));
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates handlers for switches, dimmers and hubs.
|
||||
*/
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_SWITCH)) {
|
||||
logger.debug("Creating an AdorneSwitchHandler for thing '{}'", thing.getUID());
|
||||
|
||||
return new AdorneSwitchHandler(thing);
|
||||
} else if (thingTypeUID.equals(THING_TYPE_DIMMER)) {
|
||||
logger.debug("Creating an AdorneDimmerHandler for thing '{}'", thing.getUID());
|
||||
|
||||
return new AdorneDimmerHandler(thing);
|
||||
} else if (thingTypeUID.equals(THING_TYPE_HUB)) {
|
||||
logger.debug("Creating an AdorneHubHandler for bridge '{}'", thing.getUID());
|
||||
|
||||
return new AdorneHubHandler((Bridge) thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.handler;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.adorne.internal.configuration.AdorneHubConfiguration;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubChangeNotify;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubHandler} manages the state and status of the Adorne Hub's devices.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneHubHandler extends BaseBridgeHandler implements AdorneHubChangeNotify {
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneHubHandler.class);
|
||||
private @Nullable AdorneHubController adorneHubController = null;
|
||||
|
||||
public AdorneHubHandler(Bridge bridge) {
|
||||
super(bridge);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubHandler} does not support any commands itself. This method is a NOOP and only provided since
|
||||
* its implementation is required.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// Unfortunately BaseBridgeHandler doesn't provide a default implementation of handleCommand. However, hub
|
||||
// commands could be added as a future enhancement e.g. to support hub firmware upgrades.
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes the hub controller for communication with the hub.
|
||||
*/
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing hub {}", getThing().getLabel());
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
AdorneHubConfiguration config = getConfigAs(AdorneHubConfiguration.class);
|
||||
logger.debug("Configuration host:{} port:{}", config.host, config.port);
|
||||
|
||||
AdorneHubController adorneHubController = new AdorneHubController(config, scheduler, this);
|
||||
this.adorneHubController = adorneHubController;
|
||||
// Kick off the hub controller that handles all interactions with the hub for us
|
||||
adorneHubController.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes resources by stopping the hub controller.
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
AdorneHubController adorneHubController = this.adorneHubController;
|
||||
if (adorneHubController != null) {
|
||||
adorneHubController.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hub controller. Returns <code>null</code> if hub controller has not been created yet.
|
||||
*
|
||||
* @return hub controller
|
||||
*/
|
||||
public @Nullable AdorneHubController getAdorneHubController() {
|
||||
return adorneHubController;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubHandler} is notified that the state of one of its physical devices has changed. The
|
||||
* {@link AdorneHubHandler} then asks the appropriate thing handler to update the thing to match the new state.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void stateChangeNotify(int zoneId, boolean onOff, int brightness) {
|
||||
logger.debug("State changed (zoneId:{} onOff:{} brightness:{})", zoneId, onOff, brightness);
|
||||
getThing().getThings().forEach(thing -> {
|
||||
AdorneSwitchHandler thingHandler = (AdorneSwitchHandler) thing.getHandler();
|
||||
if (thingHandler != null && thingHandler.getZoneId() == zoneId) {
|
||||
thingHandler.updateState(CHANNEL_POWER, OnOffType.from(onOff));
|
||||
if (thing.getThingTypeUID().equals(THING_TYPE_DIMMER)) {
|
||||
thingHandler.updateState(CHANNEL_BRIGHTNESS, new PercentType(brightness));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubHandler} is notified that its connectivity has changed.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void connectionChangeNotify(boolean connected) {
|
||||
logger.debug("Status changed (connected:{})", connected);
|
||||
|
||||
if (connected) {
|
||||
// Refresh all of our things in case thing states changed while we were disconnected
|
||||
getThing().getThings().forEach(thing -> {
|
||||
AdorneSwitchHandler thingHandler = (AdorneSwitchHandler) thing.getHandler();
|
||||
if (thingHandler != null) {
|
||||
thingHandler.refresh();
|
||||
}
|
||||
});
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.handler;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.CHANNEL_POWER;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.adorne.internal.configuration.AdorneSwitchConfiguration;
|
||||
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AdorneSwitchHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneSwitchHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneSwitchHandler.class);
|
||||
|
||||
/**
|
||||
* The zone ID that represents this {@link AdorneSwitchHandler}'s thing
|
||||
*/
|
||||
protected int zoneId;
|
||||
|
||||
public AdorneSwitchHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles refresh and on/off commands for channel
|
||||
* {@link org.openhab.binding.adorne.internal.AdorneBindingConstants#CHANNEL_POWER}
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("handleCommand (channelUID:{} command:{}", channelUID, command);
|
||||
try {
|
||||
if (channelUID.getId().equals(CHANNEL_POWER)) {
|
||||
if (command instanceof OnOffType) {
|
||||
AdorneHubController adorneHubController = getAdorneHubController();
|
||||
adorneHubController.setOnOff(zoneId, command.equals(OnOffType.ON));
|
||||
} else if (command instanceof RefreshType) {
|
||||
refreshOnOff();
|
||||
}
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
// Hub controller could't handle our commands. Unfortunately the framework has no mechanism to report
|
||||
// runtime errors. If we throw the exception up the framework logs it as an error - we don't want that - we
|
||||
// want the framework to handle it gracefully. No point to update the thing status, since the
|
||||
// AdorneHubController already does that. So we are forced to swallow the exception here.
|
||||
logger.debug("Failed to execute command {} for channel {} for thing {} ({})", command, channelUID,
|
||||
getThing().getLabel(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the handled thing to online.
|
||||
*/
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing switch {}", getThing().getLabel());
|
||||
|
||||
AdorneSwitchConfiguration config = getConfigAs(AdorneSwitchConfiguration.class);
|
||||
Integer configZoneId = config.zoneId;
|
||||
if (configZoneId != null) {
|
||||
zoneId = configZoneId;
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
|
||||
return;
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates thing status in response to bridge status changes.
|
||||
*/
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
logger.trace("bridgeStatusChanged bridgeStatusInfo:{}", bridgeStatusInfo.getStatus());
|
||||
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
} else {
|
||||
updateStatus(bridgeStatusInfo.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hub controller.
|
||||
*
|
||||
* @throws IllegalStateException if hub controller is not available yet.
|
||||
*/
|
||||
protected AdorneHubController getAdorneHubController() {
|
||||
Bridge bridge;
|
||||
AdorneHubHandler hubHandler;
|
||||
AdorneHubController adorneHubController = null;
|
||||
|
||||
bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
hubHandler = (AdorneHubHandler) bridge.getHandler();
|
||||
if (hubHandler != null) {
|
||||
adorneHubController = hubHandler.getAdorneHubController();
|
||||
}
|
||||
}
|
||||
if (adorneHubController == null) {
|
||||
throw new IllegalStateException("Hub Controller not available yet.");
|
||||
}
|
||||
return adorneHubController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the zone ID that represents this {@link AdorneSwitchHandler}'s thing
|
||||
*
|
||||
* @return zone ID
|
||||
*/
|
||||
public int getZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the on/off state of our thing to the actual state of the device.
|
||||
*
|
||||
*/
|
||||
public void refreshOnOff() {
|
||||
// Asynchronously get our onOff state from the hub controller and update our state accordingly
|
||||
AdorneHubController adorneHubController = getAdorneHubController();
|
||||
adorneHubController.getState(zoneId).thenAccept(state -> {
|
||||
OnOffType onOffState = OnOffType.from(state.onOff);
|
||||
updateState(CHANNEL_POWER, onOffState);
|
||||
logger.debug("Refreshed switch {} with switch state {}", getThing().getLabel(), onOffState);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes all supported channels.
|
||||
*
|
||||
*/
|
||||
public void refresh() {
|
||||
refreshOnOff();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a public version of updateState.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void updateState(String channelID, State state) {
|
||||
super.updateState(channelID, state);// Leverage our base class' protected method
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.hub;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubChangeNotify} interface is used by the {@link AdorneHubController} to notify listeners about
|
||||
* Adorne device status and hub connection changes.
|
||||
*
|
||||
* @author Mark Theiding - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface AdorneHubChangeNotify {
|
||||
/**
|
||||
* Notify listener about state change of on/off and brightness state
|
||||
*
|
||||
* @param zoneID zone ID for which change occurred
|
||||
* @param onOff new on/off state
|
||||
* @param brightness new brightness
|
||||
*/
|
||||
public void stateChangeNotify(int zoneId, boolean onOff, int brightness);
|
||||
|
||||
/**
|
||||
* Notify listener about hub connection change
|
||||
*
|
||||
* @param connected new connection state
|
||||
*/
|
||||
public void connectionChangeNotify(boolean connected);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.hub;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonStreamParser;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubConnection} manages basic connectivity with the Adorne hub.
|
||||
*
|
||||
* @author Mark Theiding - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneHubConnection {
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneHubConnection.class);
|
||||
|
||||
private final Socket hubSocket;
|
||||
private final PrintStream hubOut;
|
||||
private final InputStreamReader hubInReader;
|
||||
private final JsonStreamParser hubIn;
|
||||
|
||||
public AdorneHubConnection(String hubHost, int hubPort, int timeout) throws IOException {
|
||||
hubSocket = new Socket(hubHost, hubPort);
|
||||
hubSocket.setSoTimeout(timeout);
|
||||
hubOut = new PrintStream(hubSocket.getOutputStream());
|
||||
hubInReader = new InputStreamReader(hubSocket.getInputStream());
|
||||
hubIn = new JsonStreamParser(hubInReader);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
hubInReader.close(); // Closes underlying input stream as well
|
||||
} catch (IOException e) {
|
||||
logger.warn("Closing hub input reader failed ({})", e.getMessage());
|
||||
}
|
||||
hubOut.close(); // Closes underlying output stream as well
|
||||
try {
|
||||
hubSocket.close();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Closing hub controller socket failed ({})", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
try {
|
||||
hubSocket.shutdownInput();
|
||||
} catch (IOException e) {
|
||||
logger.debug("Couldn't shutdown hub socket");
|
||||
}
|
||||
}
|
||||
|
||||
public void putMsg(String cmd) {
|
||||
hubOut.print(cmd);
|
||||
}
|
||||
|
||||
public @Nullable JsonObject getMsg() throws JsonParseException {
|
||||
JsonElement msg = null;
|
||||
JsonObject msgJsonObject = null;
|
||||
|
||||
msg = hubIn.next();
|
||||
|
||||
if (msg == null || (msg instanceof JsonPrimitive && msg.getAsCharacter() == 0)) {
|
||||
return null; // Eat empty messages
|
||||
}
|
||||
logger.debug("Received message {}", msg);
|
||||
if (msg instanceof JsonObject) {
|
||||
msgJsonObject = (JsonObject) msg;
|
||||
}
|
||||
return msgJsonObject;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.adorne.internal.hub;
|
||||
|
||||
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.IntUnaryOperator;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.adorne.internal.AdorneDeviceState;
|
||||
import org.openhab.binding.adorne.internal.configuration.AdorneHubConfiguration;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
|
||||
/**
|
||||
* The {@link AdorneHubController} manages the interaction with the Adorne hub. The controller maintains a connection
|
||||
* with the Adorne Hub and listens to device changes and issues device commands. Interaction with the hub is performed
|
||||
* asynchronously through REST messages.
|
||||
*
|
||||
* @author Mark Theiding - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AdorneHubController {
|
||||
private final Logger logger = LoggerFactory.getLogger(AdorneHubController.class);
|
||||
|
||||
private static final int HUB_CONNECT_TIMEOUT = 10000;
|
||||
private static final int HUB_RECONNECT_SLEEP_MINIMUM = 1;
|
||||
private static final int HUB_RECONNECT_SLEEP_MAXIMUM = 15 * 60;
|
||||
|
||||
// Hub rest commands
|
||||
private static final String HUB_REST_SET_ONOFF = "{\"ID\":%d,\"Service\":\"SetZoneProperties\",\"ZID\":%d,\"PropertyList\":{\"Power\":%b}}\0";
|
||||
private static final String HUB_REST_SET_BRIGHTNESS = "{\"ID\":%d,\"Service\":\"SetZoneProperties\",\"ZID\":%d,\"PropertyList\":{\"PowerLevel\":%d}}\0";
|
||||
private static final String HUB_REST_REQUEST_STATE = "{\"ID\":%d,\"Service\":\"ReportZoneProperties\",\"ZID\":%d}\0";
|
||||
private static final String HUB_REST_REQUEST_ZONES = "{\"ID\":%d,\"Service\":\"ListZones\"}\0";
|
||||
private static final String HUB_REST_REQUEST_MACADDRESS = "{\"ID\":%d,\"Service\":\"SystemInfo\"}\0";
|
||||
private static final String HUB_TOKEN_SERVICE = "Service";
|
||||
private static final String HUB_TOKEN_ZID = "ZID";
|
||||
private static final String HUB_TOKEN_PROPERTY_LIST = "PropertyList";
|
||||
private static final String HUB_TOKEN_DEVICE_TYPE = "DeviceType";
|
||||
private static final String HUB_TOKEN_SWITCH = "Switch";
|
||||
private static final String HUB_TOKEN_DIMMER = "Dimmer";
|
||||
private static final String HUB_TOKEN_NAME = "Name";
|
||||
private static final String HUB_TOKEN_POWER = "Power";
|
||||
private static final String HUB_TOKEN_POWER_LEVEL = "PowerLevel";
|
||||
private static final String HUB_TOKEN_MAC_ADDRESS = "MACAddress";
|
||||
private static final String HUB_TOKEN_ZONE_LIST = "ZoneList";
|
||||
private static final String HUB_SERVICE_REPORT_ZONE_PROPERTIES = "ReportZoneProperties";
|
||||
private static final String HUB_SERVICE_ZONE_PROPERTIES_CHANGED = "ZonePropertiesChanged";
|
||||
private static final String HUB_SERVICE_LIST_ZONE = "ListZones";
|
||||
private static final String HUB_SERVICE_SYSTEM_INFO = "SystemInfo";
|
||||
|
||||
private @Nullable Future<?> hubController;
|
||||
private final String hubHost;
|
||||
private int hubPort;
|
||||
private @Nullable AdorneHubConnection hubConnection;
|
||||
private final CompletableFuture<@Nullable Void> hubControllerConnected;
|
||||
private int hubReconnectSleep; // Sleep time before we attempt re-connect
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
private volatile boolean stopWhenCommandsServed; // Stop the controller once all pending commands have been served
|
||||
|
||||
// When we submit commmands to the hub we don't correlate commands and responses. We simply use the first available
|
||||
// response that answers our question. For that we store all pending commands.
|
||||
// Note that for optimal resiliency we send a new request for each command even if a request is already pending
|
||||
private final Map<Integer, CompletableFuture<AdorneDeviceState>> stateCommands;
|
||||
private @Nullable CompletableFuture<List<Integer>> zoneCommand;
|
||||
private @Nullable CompletableFuture<String> macAddressCommand;
|
||||
private final AtomicInteger commandId; // We assign increasing command ids to all REST commands to the hub for
|
||||
// easier troubleshooting
|
||||
|
||||
private final AdorneHubChangeNotify changeListener;
|
||||
|
||||
private final Object stopLock;
|
||||
private final Object hubConnectionLock;
|
||||
private final Object macAddressCommandLock;
|
||||
private final Object zoneCommandLock;
|
||||
|
||||
public AdorneHubController(AdorneHubConfiguration config, ScheduledExecutorService scheduler,
|
||||
AdorneHubChangeNotify changeListener) {
|
||||
hubHost = config.host;
|
||||
hubPort = config.port;
|
||||
this.scheduler = scheduler;
|
||||
this.changeListener = changeListener;
|
||||
hubController = null;
|
||||
hubConnection = null;
|
||||
hubControllerConnected = new CompletableFuture<>();
|
||||
hubReconnectSleep = HUB_RECONNECT_SLEEP_MINIMUM;
|
||||
|
||||
stopWhenCommandsServed = false;
|
||||
|
||||
stopLock = new Object();
|
||||
hubConnectionLock = new Object();
|
||||
macAddressCommandLock = new Object();
|
||||
zoneCommandLock = new Object();
|
||||
|
||||
stateCommands = new HashMap<>();
|
||||
zoneCommand = null;
|
||||
macAddressCommand = null;
|
||||
commandId = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the hub controller. Call only once.
|
||||
*
|
||||
* @return Future to inform the caller that the hub controller is ready for receiving commands
|
||||
*/
|
||||
public CompletableFuture<@Nullable Void> start() {
|
||||
logger.info("Starting hub controller");
|
||||
hubController = scheduler.submit(this::msgLoop);
|
||||
return hubControllerConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the hub controller. Can't restart afterwards. If called before start nothing happens.
|
||||
*/
|
||||
public void stop() {
|
||||
logger.info("Stopping hub controller");
|
||||
synchronized (stopLock) {
|
||||
// Canceling the controller tells the message loop to stop and also cancels recreation of the message loop
|
||||
// if that is pending after a disconnect.
|
||||
Future<?> hubController = this.hubController;
|
||||
if (hubController != null) {
|
||||
hubController.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the input stream in case controller is waiting on input
|
||||
// Note this is best effort. If we are unlucky the hub can still enter waiting on input just after our stop
|
||||
// here. Because waiting on input is long-running we can't just synchronize it with the stop check as case 2
|
||||
// above. But that is ok as waiting on input has a timeout and will honor stop after that.
|
||||
synchronized (hubConnectionLock) {
|
||||
AdorneHubConnection hubConnection = this.hubConnection;
|
||||
if (hubConnection != null) {
|
||||
hubConnection.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
cancelCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the hub controller once all in-flight commands have been executed.
|
||||
*/
|
||||
public void stopWhenCommandsServed() {
|
||||
stopWhenCommandsServed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns device on or off.
|
||||
*
|
||||
* @param zoneId the device's zone ID
|
||||
* @param on true to turn on the device
|
||||
*/
|
||||
public void setOnOff(int zoneId, boolean on) {
|
||||
sendRestCmd(String.format(HUB_REST_SET_ONOFF, getNextCommandId(), zoneId, on));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the brightness for a device. Applies only to dimmer devices.
|
||||
*
|
||||
* @param zoneId the device's zone ID
|
||||
* @param level A value from 1-100. Note that in particular value 0 is not supported, which means this method can't
|
||||
* be used to turn off a dimmer.
|
||||
*/
|
||||
public void setBrightness(int zoneId, int level) {
|
||||
if (level < 1 || level > 100) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
sendRestCmd(String.format(HUB_REST_SET_BRIGHTNESS, getNextCommandId(), zoneId, level));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets asynchronously the state for a device.
|
||||
*
|
||||
* @param zoneId the device's zone ID
|
||||
* @return a future for the {@link AdorneDeviceState}
|
||||
*/
|
||||
public CompletableFuture<AdorneDeviceState> getState(int zoneId) {
|
||||
// Note that we send the REST command for resiliency even if there is a pending command
|
||||
sendRestCmd(String.format(HUB_REST_REQUEST_STATE, getNextCommandId(), zoneId));
|
||||
|
||||
CompletableFuture<AdorneDeviceState> stateCommand;
|
||||
synchronized (stateCommands) {
|
||||
stateCommand = stateCommands.get(zoneId);
|
||||
if (stateCommand == null) {
|
||||
stateCommand = new CompletableFuture<>();
|
||||
stateCommands.put(zoneId, stateCommand);
|
||||
}
|
||||
}
|
||||
return stateCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets asynchronously all zone IDs that are in use on the hub.
|
||||
*
|
||||
* @return a future for the list of zone IDs
|
||||
*/
|
||||
public CompletableFuture<List<Integer>> getZones() {
|
||||
// Note that we send the REST command for resiliency even if there is a pending command
|
||||
sendRestCmd(String.format(HUB_REST_REQUEST_ZONES, getNextCommandId()));
|
||||
|
||||
CompletableFuture<List<Integer>> zoneCommand;
|
||||
synchronized (zoneCommandLock) {
|
||||
zoneCommand = this.zoneCommand;
|
||||
if (zoneCommand == null) {
|
||||
this.zoneCommand = zoneCommand = new CompletableFuture<>();
|
||||
}
|
||||
}
|
||||
return zoneCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets asynchronously the MAC address of the hub.
|
||||
*
|
||||
* @return a future for the MAC address
|
||||
*/
|
||||
public CompletableFuture<String> getMACAddress() {
|
||||
// Note that we send the REST command for resiliency even if there is a pending command
|
||||
sendRestCmd(String.format(HUB_REST_REQUEST_MACADDRESS, getNextCommandId()));
|
||||
|
||||
CompletableFuture<String> macAddressCommand;
|
||||
synchronized (macAddressCommandLock) {
|
||||
macAddressCommand = this.macAddressCommand;
|
||||
if (macAddressCommand == null) {
|
||||
this.macAddressCommand = macAddressCommand = new CompletableFuture<>();
|
||||
}
|
||||
}
|
||||
return macAddressCommand;
|
||||
}
|
||||
|
||||
private void sendRestCmd(String cmd) {
|
||||
logger.debug("Sending command {}", cmd);
|
||||
synchronized (hubConnectionLock) {
|
||||
AdorneHubConnection hubConnection = this.hubConnection;
|
||||
if (hubConnection != null) {
|
||||
hubConnection.putMsg(cmd);
|
||||
} else {
|
||||
throw new IllegalStateException("Can't send command. Adorne Hub connection is not available.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the controller message loop that is interacting with the Adorne Hub by sending commands and listening for
|
||||
* updates
|
||||
*/
|
||||
private void msgLoop() {
|
||||
try {
|
||||
JsonObject hubMsg;
|
||||
JsonPrimitive jsonService;
|
||||
String service;
|
||||
|
||||
// Main message loop listening for updates from the hub
|
||||
logger.debug("Starting message loop");
|
||||
while (!shouldStop()) {
|
||||
if (!connect()) {
|
||||
int sleep = hubReconnectSleep;
|
||||
logger.debug("Waiting {} seconds before re-attempting to connect.", sleep);
|
||||
if (hubReconnectSleep < HUB_RECONNECT_SLEEP_MAXIMUM) {
|
||||
hubReconnectSleep = hubReconnectSleep * 2; // Increase sleep time exponentially
|
||||
}
|
||||
restartMsgLoop(sleep);
|
||||
return;
|
||||
} else {
|
||||
hubReconnectSleep = HUB_RECONNECT_SLEEP_MINIMUM; // Reset
|
||||
}
|
||||
|
||||
hubMsg = null;
|
||||
try {
|
||||
AdorneHubConnection hubConnection = this.hubConnection;
|
||||
if (hubConnection != null) {
|
||||
hubMsg = hubConnection.getMsg();
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
logger.debug("Failed to read valid message {}", e.getMessage());
|
||||
disconnect(); // Disconnect so we can recover
|
||||
}
|
||||
if (hubMsg == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process message based on service type
|
||||
if ((jsonService = hubMsg.getAsJsonPrimitive(HUB_TOKEN_SERVICE)) != null) {
|
||||
service = jsonService.getAsString();
|
||||
} else {
|
||||
continue; // Ignore messages that don't have a service specified
|
||||
}
|
||||
|
||||
if (service.equals(HUB_SERVICE_REPORT_ZONE_PROPERTIES)) {
|
||||
processMsgReportZoneProperties(hubMsg);
|
||||
} else if (service.equals(HUB_SERVICE_ZONE_PROPERTIES_CHANGED)) {
|
||||
processMsgZonePropertiesChanged(hubMsg);
|
||||
} else if (service.equals(HUB_SERVICE_LIST_ZONE)) {
|
||||
processMsgListZone(hubMsg);
|
||||
} else if (service.equals(HUB_SERVICE_SYSTEM_INFO)) {
|
||||
processMsgSystemInfo(hubMsg);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Hub controller failed", e);
|
||||
}
|
||||
|
||||
// Shut down
|
||||
disconnect();
|
||||
|
||||
cancelCommands();
|
||||
hubControllerConnected.cancel(false);
|
||||
logger.info("Exiting hub controller");
|
||||
}
|
||||
|
||||
private boolean shouldStop() {
|
||||
boolean stateCommandsIsEmpty;
|
||||
synchronized (stateCommands) {
|
||||
stateCommandsIsEmpty = stateCommands.isEmpty();
|
||||
}
|
||||
boolean commandsServed = stopWhenCommandsServed && stateCommandsIsEmpty && (zoneCommand == null)
|
||||
&& (macAddressCommand == null);
|
||||
|
||||
return isCancelled() || commandsServed;
|
||||
}
|
||||
|
||||
private boolean isCancelled() {
|
||||
Future<?> hubController = this.hubController;
|
||||
return hubController == null || hubController.isCancelled();
|
||||
}
|
||||
|
||||
private boolean connect() {
|
||||
try {
|
||||
if (hubConnection == null) {
|
||||
hubConnection = new AdorneHubConnection(hubHost, hubPort, HUB_CONNECT_TIMEOUT);
|
||||
logger.debug("Hub connection established");
|
||||
|
||||
// Working around an Adorne Hub bug: the first command sent from a new connection intermittently
|
||||
// gets lost in the hub. We are requesting the MAC address here simply to get this fragile first
|
||||
// command out of the way. Requesting the MAC address and ignoring the result doesn't do any harm.
|
||||
getMACAddress();
|
||||
|
||||
hubControllerConnected.complete(null);
|
||||
|
||||
changeListener.connectionChangeNotify(true);
|
||||
}
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
logger.debug("Couldn't establish hub connection ({}).", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
hubReconnectSleep = HUB_RECONNECT_SLEEP_MINIMUM; // Reset our reconnect sleep time
|
||||
synchronized (hubConnectionLock) {
|
||||
AdorneHubConnection hubConnection = this.hubConnection;
|
||||
if (hubConnection != null) {
|
||||
hubConnection.close();
|
||||
this.hubConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
changeListener.connectionChangeNotify(false);
|
||||
}
|
||||
|
||||
private void cancelCommands() {
|
||||
// If there are still pending commands we need to cancel them
|
||||
synchronized (stateCommands) {
|
||||
stateCommands.forEach((zoneId, stateCommand) -> stateCommand.cancel(false));
|
||||
stateCommands.clear();
|
||||
}
|
||||
synchronized (zoneCommandLock) {
|
||||
CompletableFuture<List<Integer>> zoneCommand = this.zoneCommand;
|
||||
if (zoneCommand != null) {
|
||||
zoneCommand.cancel(false);
|
||||
this.zoneCommand = null;
|
||||
}
|
||||
}
|
||||
synchronized (macAddressCommandLock) {
|
||||
CompletableFuture<String> macAddressCommand = this.macAddressCommand;
|
||||
if (macAddressCommand != null) {
|
||||
macAddressCommand.cancel(false);
|
||||
this.macAddressCommand = null;
|
||||
}
|
||||
}
|
||||
logger.debug("Cancelled commands");
|
||||
}
|
||||
|
||||
private void restartMsgLoop(int sleep) {
|
||||
synchronized (stopLock) {
|
||||
if (!isCancelled()) {
|
||||
this.hubController = scheduler.schedule(this::msgLoop, sleep, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The hub sent zone properties in response to a command.
|
||||
*/
|
||||
private void processMsgReportZoneProperties(JsonObject hubMsg) {
|
||||
int zoneId = hubMsg.getAsJsonPrimitive(HUB_TOKEN_ZID).getAsInt();
|
||||
logger.debug("Reporting zone properties for zone ID {} ", zoneId);
|
||||
|
||||
JsonObject jsonPropertyList = hubMsg.getAsJsonObject(HUB_TOKEN_PROPERTY_LIST);
|
||||
String deviceTypeStr = jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_DEVICE_TYPE).getAsString();
|
||||
ThingTypeUID deviceType;
|
||||
if (deviceTypeStr.equals(HUB_TOKEN_SWITCH)) {
|
||||
deviceType = THING_TYPE_SWITCH;
|
||||
} else if (deviceTypeStr.equals(HUB_TOKEN_DIMMER)) {
|
||||
deviceType = THING_TYPE_DIMMER;
|
||||
} else {
|
||||
logger.debug("Unsupported device type {}", deviceTypeStr);
|
||||
return;
|
||||
}
|
||||
AdorneDeviceState state = new AdorneDeviceState(zoneId,
|
||||
jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_NAME).getAsString(), deviceType,
|
||||
jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER).getAsBoolean(),
|
||||
jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER_LEVEL).getAsInt());
|
||||
|
||||
synchronized (stateCommands) {
|
||||
CompletableFuture<AdorneDeviceState> stateCommand = stateCommands.get(zoneId);
|
||||
if (stateCommand != null) {
|
||||
stateCommand.complete(state);
|
||||
stateCommands.remove(zoneId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The hub informs us about a zone's change in properties.
|
||||
*/
|
||||
private void processMsgZonePropertiesChanged(JsonObject hubMsg) {
|
||||
int zoneId = hubMsg.getAsJsonPrimitive(HUB_TOKEN_ZID).getAsInt();
|
||||
logger.debug("Zone properties changed for zone ID {} ", zoneId);
|
||||
|
||||
JsonObject jsonPropertyList = hubMsg.getAsJsonObject(HUB_TOKEN_PROPERTY_LIST);
|
||||
boolean onOff = jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER).getAsBoolean();
|
||||
int brightness = jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER_LEVEL).getAsInt();
|
||||
changeListener.stateChangeNotify(zoneId, onOff, brightness);
|
||||
}
|
||||
|
||||
/**
|
||||
* The hub sent a list of zones in response to a command.
|
||||
*/
|
||||
private void processMsgListZone(JsonObject hubMsg) {
|
||||
List<Integer> zones = new ArrayList<>();
|
||||
JsonArray jsonZoneList;
|
||||
|
||||
jsonZoneList = hubMsg.getAsJsonArray(HUB_TOKEN_ZONE_LIST);
|
||||
jsonZoneList.forEach(jsonZoneId -> {
|
||||
JsonPrimitive jsonZoneIdValue = ((JsonObject) jsonZoneId).getAsJsonPrimitive(HUB_TOKEN_ZID);
|
||||
zones.add(jsonZoneIdValue.getAsInt());
|
||||
});
|
||||
|
||||
synchronized (zoneCommandLock) {
|
||||
CompletableFuture<List<Integer>> zoneCommand = this.zoneCommand;
|
||||
if (zoneCommand != null) {
|
||||
zoneCommand.complete(zones);
|
||||
this.zoneCommand = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The hub sent system info in response to a command.
|
||||
*/
|
||||
private void processMsgSystemInfo(JsonObject hubMsg) {
|
||||
synchronized (macAddressCommandLock) {
|
||||
CompletableFuture<String> macAddressCommand = this.macAddressCommand;
|
||||
if (macAddressCommand != null) {
|
||||
macAddressCommand.complete(hubMsg.getAsJsonPrimitive(HUB_TOKEN_MAC_ADDRESS).getAsString());
|
||||
this.macAddressCommand = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getNextCommandId() {
|
||||
IntUnaryOperator op = commandId -> {
|
||||
int newCommandId = commandId;
|
||||
if (commandId == Integer.MAX_VALUE) {
|
||||
newCommandId = 0;
|
||||
}
|
||||
return ++newCommandId;
|
||||
};
|
||||
|
||||
return commandId.updateAndGet(op);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="adorne" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Adorne Binding</name>
|
||||
<description>The Adorne Binding controls Legrand's Adorne Wi-Fi ready switches and outlets.</description>
|
||||
<author>Mark Theiding</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="adorne"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="http://openhab.org/schemas/thing-description/v1.0.0 http://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="hub">
|
||||
<label>Adorne Hub</label>
|
||||
<description>The Adorne Hub serves as the bridge to control Adorne switches, dimmer switches and outlets.</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="host" type="text">
|
||||
<default>LCM1.local</default>
|
||||
<label>Host</label>
|
||||
<description>
|
||||
Host name or IP address.
|
||||
</description>
|
||||
<context>network_address</context>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer">
|
||||
<default>2112</default>
|
||||
<label>Port</label>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="adorne"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="switch">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="hub"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Adorne Switch</label>
|
||||
<description>Controls an Adorne switch or outlet.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="power" typeId="system.power"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="zoneId" type="integer" required="true">
|
||||
<label>Zone ID</label>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="dimmer">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="hub"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Adorne Dimmer Switch</label>
|
||||
<description>Controls an Adorne dimmer switch.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="power" typeId="system.power"/>
|
||||
<channel id="brightness" typeId="system.brightness"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="zoneId" type="integer" required="true">
|
||||
<label>Zone ID</label>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,5 @@
|
||||
Switch LightBathroom {channel="adorne:switch:home:bathroom:power"}
|
||||
Switch LightBedroomSwitch1 {channel="adorne:dimmer:home:bedroom1:power"}
|
||||
Dimmer LightBedroomDimmer1 {channel="adorne:dimmer:home:bedroom1:brightness"}
|
||||
Switch LightBedroomSwitch2 {channel="adorne:dimmer:home:bedroom2:power"}
|
||||
Dimmer LightBedroomDimmer2 {channel="adorne:dimmer:home:bedroom2:brightness"}
|
||||
@@ -0,0 +1,14 @@
|
||||
sitemap demo label="Adorne Binding Demo"
|
||||
{
|
||||
Frame label="Adorne Switch" {
|
||||
Switch item=LightBathroom label="Bathroom" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
|
||||
}
|
||||
Frame label="Adorne Dimmer using Slider" {
|
||||
Switch item=LightBedroomSwitch1 label="Bedroom 1" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
|
||||
Slider item=LightBedroomDimmer1 label="Bedroom 1" icon="light-on" minValue=1 maxValue=100 step=1 // Requires OpenHAB 2.5
|
||||
}
|
||||
Frame label="Adorne Dimmer using Setpoint" {
|
||||
Switch item=LightBedroomSwitch2 label="Bedroom 2" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
|
||||
Setpoint item=LightBedroomDimmer2 label="Bedroom 2" icon="light-on" minValue=1 maxValue=100 step=5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
Bridge adorne:hub:home "Adorne Hub" {
|
||||
switch bathroom "Bathroom" [zoneId=0]
|
||||
dimmer bedroom1 "Bedroom1" [zoneId=1]
|
||||
dimmer bedroom2 "Bedroom2" [zoneId=2]
|
||||
}
|
||||
Reference in New Issue
Block a user