added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.satel-${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-satel" description="Satel Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.satel/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,29 @@
/**
* 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.satel.action;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Automation action handler interface for reading Satel event log.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public interface ISatelEventLogActions {
public Map<String, Object> readEvent(@Nullable Number index);
}

View File

@@ -0,0 +1,95 @@
/**
* 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.satel.action;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.satel.internal.handler.SatelEventLogHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.ActionOutput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Automation action handler service for reading Satel event log.
*
* @author Krzysztof Goworek - Initial contribution
* @see SatelEventLogHandler
*/
@ThingActionsScope(name = "satel")
@NonNullByDefault
public class SatelEventLogActions implements ThingActions, ISatelEventLogActions {
private final Logger logger = LoggerFactory.getLogger(getClass());
private @Nullable SatelEventLogHandler handler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof SatelEventLogHandler) {
this.handler = (SatelEventLogHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@Override
@RuleAction(label = "@text/actionReadEventLabel", description = "@text/actionReadEventDesc")
public @ActionOutput(name = "index", type = "java.lang.Integer", label = "@text/actionOutputIndexLabel", description = "@text/actionOutputIndexDesc") @ActionOutput(name = "prev_index", type = "java.lang.Integer", label = "@text/actionOutputPrevIndexLabel", description = "@text/actionOutputPrevIndexDesc") @ActionOutput(name = "timestamp", type = "java.time.ZonedDateTime", label = "@text/actionOutputTimestampLabel", description = "@text/actionOutputTimestampDesc") @ActionOutput(name = "description", type = "java.lang.String", label = "@text/actionOutputDescriptionLabel", description = "@text/actionOutputDescriptionDesc") @ActionOutput(name = "details", type = "java.lang.String", label = "@text/actionOutputDetailsLabel", description = "@text/actionOutputDetailsDesc") Map<String, Object> readEvent(
@ActionInput(name = "index", label = "@text/actionInputIndexLabel", description = "@text/actionInputIndexDesc") @Nullable Number index) {
logger.debug("satel.readEvent called with input: index={}", index);
Map<String, Object> result = new HashMap<>();
if (handler != null) {
handler.readEvent(index == null ? -1 : index.intValue()).ifPresent(event -> {
result.put("index", event.getIndex());
result.put("prev_index", event.getPrevIndex());
result.put("timestamp", event.getTimestamp());
result.put("description", event.getDescription());
result.put("details", event.getDetails());
});
}
return result;
}
public static Map<String, Object> readEvent(@Nullable ThingActions actions, @Nullable Number index) {
return invokeMethodOf(actions).readEvent(index);
}
private static ISatelEventLogActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
} else if (actions instanceof ISatelEventLogActions) {
return (ISatelEventLogActions) actions;
} else if (actions.getClass().getName().equals(SatelEventLogActions.class.getName())) {
return (ISatelEventLogActions) Proxy.newProxyInstance(ISatelEventLogActions.class.getClassLoader(),
new Class[] { ISatelEventLogActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
return m.invoke(actions, args);
});
}
throw new IllegalArgumentException("actions is not an instance of ISatelEventLogActions");
}
}

View File

@@ -0,0 +1,77 @@
/**
* 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.satel.internal;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link SatelBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class SatelBindingConstants {
public static final String BINDING_ID = "satel";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_ETHM1 = new ThingTypeUID(BINDING_ID, "ethm-1");
public static final ThingTypeUID THING_TYPE_INTRS = new ThingTypeUID(BINDING_ID, "int-rs");
public static final ThingTypeUID THING_TYPE_OUTPUT = new ThingTypeUID(BINDING_ID, "output");
public static final ThingTypeUID THING_TYPE_PARTITION = new ThingTypeUID(BINDING_ID, "partition");
public static final ThingTypeUID THING_TYPE_SHUTTER = new ThingTypeUID(BINDING_ID, "shutter");
public static final ThingTypeUID THING_TYPE_ZONE = new ThingTypeUID(BINDING_ID, "zone");
public static final ThingTypeUID THING_TYPE_SYSTEM = new ThingTypeUID(BINDING_ID, "system");
public static final ThingTypeUID THING_TYPE_EVENTLOG = new ThingTypeUID(BINDING_ID, "event-log");
public static final ThingTypeUID THING_TYPE_ATD100 = new ThingTypeUID(BINDING_ID, "atd-100");
public static final Set<ThingTypeUID> BRIDGE_THING_TYPES_UIDS = Stream.of(THING_TYPE_ETHM1, THING_TYPE_INTRS)
.collect(Collectors.toSet());
// Physical devices
public static final Set<ThingTypeUID> DEVICE_THING_TYPES_UIDS = Stream
.of(THING_TYPE_OUTPUT, THING_TYPE_PARTITION, THING_TYPE_SHUTTER, THING_TYPE_ZONE, THING_TYPE_ATD100)
.collect(Collectors.toSet());
// Virtual devices
public static final Set<ThingTypeUID> VIRTUAL_THING_TYPES_UIDS = Stream.of(THING_TYPE_SYSTEM, THING_TYPE_EVENTLOG)
.collect(Collectors.toSet());
// Channel types
public static final ChannelTypeUID CHANNEL_TYPE_LOBATT = new ChannelTypeUID("system", "low-battery");
public static final ChannelTypeUID CHANNEL_TYPE_NOCOMM = new ChannelTypeUID(BINDING_ID, "device_nocomm");
// List of all Channel ids except those covered by state enums
public static final String CHANNEL_SHUTTER_STATE = "shutter_state";
public static final String CHANNEL_DATE_TIME = "date_time";
public static final String CHANNEL_SERVICE_MODE = "service_mode";
public static final String CHANNEL_TROUBLES = "troubles";
public static final String CHANNEL_TROUBLES_MEMORY = "troubles_memory";
public static final String CHANNEL_ACU100_PRESENT = "acu100_present";
public static final String CHANNEL_INTRX_PRESENT = "intrx_present";
public static final String CHANNEL_GRADE23_SET = "grade23_set";
public static final String CHANNEL_USER_CODE = "user_code";
public static final String CHANNEL_INDEX = "index";
public static final String CHANNEL_PREV_INDEX = "prev_index";
public static final String CHANNEL_TIMESTAMP = "timestamp";
public static final String CHANNEL_DESCRIPTION = "description";
public static final String CHANNEL_DETAILS = "details";
public static final String CHANNEL_TEMPERATURE = "temperature";
}

View File

@@ -0,0 +1,176 @@
/**
* 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.satel.internal;
import static org.openhab.binding.satel.internal.SatelBindingConstants.*;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
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.binding.satel.internal.config.SatelThingConfig;
import org.openhab.binding.satel.internal.discovery.SatelDeviceDiscoveryService;
import org.openhab.binding.satel.internal.handler.Atd100Handler;
import org.openhab.binding.satel.internal.handler.Ethm1BridgeHandler;
import org.openhab.binding.satel.internal.handler.IntRSBridgeHandler;
import org.openhab.binding.satel.internal.handler.SatelBridgeHandler;
import org.openhab.binding.satel.internal.handler.SatelEventLogHandler;
import org.openhab.binding.satel.internal.handler.SatelOutputHandler;
import org.openhab.binding.satel.internal.handler.SatelPartitionHandler;
import org.openhab.binding.satel.internal.handler.SatelShutterHandler;
import org.openhab.binding.satel.internal.handler.SatelSystemHandler;
import org.openhab.binding.satel.internal.handler.SatelZoneHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.openhab.core.thing.type.ThingType;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link SatelHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Krzysztof Goworek - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.satel")
@NonNullByDefault
public class SatelHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(BRIDGE_THING_TYPES_UIDS, DEVICE_THING_TYPES_UIDS, VIRTUAL_THING_TYPES_UIDS)
.flatMap(uids -> uids.stream()).collect(Collectors.toSet());
private Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegistrations = new ConcurrentHashMap<>();
private @Nullable SerialPortManager serialPortManager;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration,
@Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
ThingUID effectiveUID = thingUID;
if (effectiveUID == null) {
if (DEVICE_THING_TYPES_UIDS.contains(thingTypeUID)) {
effectiveUID = getDeviceUID(thingTypeUID, thingUID, configuration, bridgeUID);
} else if (VIRTUAL_THING_TYPES_UIDS.contains(thingTypeUID) && bridgeUID != null) {
effectiveUID = new ThingUID(thingTypeUID, bridgeUID.getId());
}
}
return super.createThing(thingTypeUID, configuration, effectiveUID, bridgeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (Ethm1BridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
SatelBridgeHandler bridgeHandler = new Ethm1BridgeHandler((Bridge) thing);
registerDiscoveryService(bridgeHandler);
return bridgeHandler;
} else if (IntRSBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
final SerialPortManager serialPortManager = this.serialPortManager;
if (serialPortManager != null) {
SatelBridgeHandler bridgeHandler = new IntRSBridgeHandler((Bridge) thing, serialPortManager);
registerDiscoveryService(bridgeHandler);
return bridgeHandler;
} else {
throw new IllegalStateException(
"Unable to create INT-RS bridge thing. The serial port manager is missing.");
}
} else if (SatelZoneHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SatelZoneHandler(thing);
} else if (SatelOutputHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SatelOutputHandler(thing);
} else if (SatelPartitionHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SatelPartitionHandler(thing);
} else if (SatelShutterHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SatelShutterHandler(thing);
} else if (SatelSystemHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SatelSystemHandler(thing);
} else if (SatelEventLogHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SatelEventLogHandler(thing);
} else if (Atd100Handler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new Atd100Handler(thing);
}
return null;
}
@SuppressWarnings("null")
@Override
protected void removeHandler(ThingHandler thingHandler) {
super.removeHandler(thingHandler);
ServiceRegistration<?> discoveryServiceRegistration = discoveryServiceRegistrations
.remove(thingHandler.getThing().getUID());
if (discoveryServiceRegistration != null) {
discoveryServiceRegistration.unregister();
}
}
@Reference
protected void setSerialPortManager(final SerialPortManager serialPortManager) {
this.serialPortManager = serialPortManager;
}
protected void unsetSerialPortManager(final SerialPortManager serialPortManager) {
this.serialPortManager = null;
}
private void registerDiscoveryService(SatelBridgeHandler bridgeHandler) {
SatelDeviceDiscoveryService discoveryService = new SatelDeviceDiscoveryService(bridgeHandler,
this::resolveThingType);
ServiceRegistration<?> discoveryServiceRegistration = bundleContext
.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>());
discoveryServiceRegistrations.put(bridgeHandler.getThing().getUID(), discoveryServiceRegistration);
}
private ThingUID getDeviceUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID, Configuration configuration,
@Nullable ThingUID bridgeUID) {
String deviceId;
if (THING_TYPE_SHUTTER.equals(thingTypeUID)) {
deviceId = String.format("%s-%s", configuration.get(SatelThingConfig.UP_ID),
configuration.get(SatelThingConfig.DOWN_ID));
} else {
deviceId = String.valueOf(configuration.get(SatelThingConfig.ID));
}
return bridgeUID != null ? new ThingUID(thingTypeUID, deviceId, bridgeUID.getId())
: new ThingUID(thingTypeUID, deviceId);
}
private ThingType resolveThingType(ThingTypeUID thingTypeUID) {
final ThingType result = super.getThingTypeByUID(thingTypeUID);
if (result != null) {
return result;
}
throw new IllegalArgumentException("Invalid thing type provided: " + thingTypeUID);
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.satel.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Command class for command that clear troubles memory.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class ClearTroublesCommand extends ControlCommand {
public static final byte COMMAND_CODE = (byte) 0x8b;
/**
* Creates new command class instance.
*
* @param userCode code of the user on behalf the control is made
*/
public ClearTroublesCommand(String userCode) {
super(COMMAND_CODE, userCodeToBytes(userCode));
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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.satel.internal.command;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
/**
* Base class for all commands that return result code in the response.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public abstract class ControlCommand extends SatelCommandBase {
/**
* Creates new command class instance.
*
* @param commandCode command code
* @param payload command bytes
*/
public ControlCommand(byte commandCode, byte[] payload) {
super(commandCode, payload);
}
@Override
protected boolean isResponseValid(SatelMessage response) {
return true;
}
protected static byte[] userCodeToBytes(String userCode) {
if (StringUtils.isEmpty(userCode)) {
throw new IllegalArgumentException("User code is empty");
}
if (userCode.length() > 8) {
throw new IllegalArgumentException("User code too long");
}
byte[] bytes = new byte[8];
int digitsNbr = 2 * bytes.length;
for (int i = 0; i < digitsNbr; ++i) {
if (i < userCode.length()) {
char digit = userCode.charAt(i);
if (!Character.isDigit(digit)) {
throw new IllegalArgumentException("User code must contain digits only");
}
if (i % 2 == 0) {
bytes[i / 2] = (byte) ((digit - '0') << 4);
} else {
bytes[i / 2] |= (byte) (digit - '0');
}
} else if (i % 2 == 0) {
bytes[i / 2] = (byte) 0xff;
} else if (i == userCode.length()) {
bytes[i / 2] |= 0x0f;
}
}
return bytes;
}
}

View File

@@ -0,0 +1,65 @@
/**
* 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.satel.internal.command;
import java.util.BitSet;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.ArrayUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.event.NewStatesEvent;
import org.openhab.binding.satel.internal.types.ControlType;
/**
* Command class for commands that control (change) state of Integra objects,
* like partitions (arm, disarm), zones (bypass, unbypass) outputs (on, off,
* switch), etc.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class ControlObjectCommand extends ControlCommand {
private static final long REFRESH_DELAY = 1000;
private ControlType controlType;
private ScheduledExecutorService scheduler;
/**
* Creates new command class instance for specified type of control.
*
* @param controlType type of controlled objects
* @param objects bits that represents objects to control
* @param userCode code of the user on behalf the control is made
* @param scheduler scheduler object for scheduling refreshes
*/
public ControlObjectCommand(ControlType controlType, byte[] objects, String userCode,
ScheduledExecutorService scheduler) {
super(controlType.getControlCommand(), ArrayUtils.addAll(userCodeToBytes(userCode), objects));
this.controlType = controlType;
this.scheduler = scheduler;
}
@Override
protected void handleResponseInternal(final EventDispatcher eventDispatcher) {
// force refresh states that might have changed
final BitSet newStates = controlType.getControlledStates();
if (!newStates.isEmpty()) {
// add delay to give a chance to process sent command
scheduler.schedule(() -> eventDispatcher.dispatchEvent(new NewStatesEvent(newStates)), REFRESH_DELAY,
TimeUnit.MILLISECONDS);
}
}
}

View File

@@ -0,0 +1,73 @@
/**
* 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.satel.internal.command;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.event.IntegraStateEvent;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
import org.openhab.binding.satel.internal.types.StateType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Command class for commands that return state of Integra objects, like
* partitions (armed, alarm, entry time), zones (violation, tamper, alarm),
* outputs and doors (opened, opened long).
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class IntegraStateCommand extends SatelCommandBase {
private final Logger logger = LoggerFactory.getLogger(IntegraStateCommand.class);
private StateType stateType;
/**
* Constructs new command instance for specified type of state.
*
* @param stateType type of state
* @param extended if <code>true</code> command will be sent as extended (256 zones or outputs)
*/
public IntegraStateCommand(StateType stateType, boolean extended) {
super(stateType.getRefreshCommand(), extended);
this.stateType = stateType;
}
/**
* @return <code>true</code> if current command is extended (256 zones/outputs)
*/
public boolean isExtended() {
return Arrays.equals(EXTENDED_CMD_PAYLOAD, this.getPayload());
}
@Override
protected boolean isResponseValid(SatelMessage response) {
// validate response
if (response.getPayload().length != this.stateType.getPayloadLength(isExtended())) {
logger.debug("Invalid payload length for state type {}: {}", this.stateType, response.getPayload().length);
return false;
}
return true;
}
@Override
protected void handleResponseInternal(final EventDispatcher eventDispatcher) {
// dispatch event
eventDispatcher.dispatchEvent(
new IntegraStateEvent(getResponse().getCommand(), getResponse().getPayload(), isExtended()));
}
}

View File

@@ -0,0 +1,89 @@
/**
* 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.satel.internal.command;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.event.IntegraStatusEvent;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Command class for command that returns Integra RTC and basic status.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class IntegraStatusCommand extends SatelCommandBase {
private final Logger logger = LoggerFactory.getLogger(IntegraStatusCommand.class);
public static final byte COMMAND_CODE = 0x1a;
/**
* Creates new command class instance.
*/
public IntegraStatusCommand() {
super(COMMAND_CODE, false);
}
/**
* @return date and time
*/
public Optional<LocalDateTime> getIntegraTime() {
// parse current date and time
try {
final byte[] payload = getResponse().getPayload();
return Optional
.of(LocalDateTime.of(bcdToInt(payload, 0, 2), bcdToInt(payload, 2, 1), bcdToInt(payload, 3, 1),
bcdToInt(payload, 4, 1), bcdToInt(payload, 5, 1), bcdToInt(payload, 6, 1)));
} catch (DateTimeException e) {
logger.debug("Invalid date/time set in the system", e);
return Optional.empty();
}
}
/**
* @return first status byte
*/
public byte getStatusByte1() {
return getResponse().getPayload()[7];
}
/**
* @return second status byte
*/
public byte getStatusByte2() {
return getResponse().getPayload()[8];
}
@Override
protected boolean isResponseValid(SatelMessage response) {
if (response.getPayload().length != 9) {
logger.debug("Invalid payload length: {}", response.getPayload().length);
return false;
}
return true;
}
@Override
protected void handleResponseInternal(final EventDispatcher eventDispatcher) {
// dispatch version event
eventDispatcher.dispatchEvent(new IntegraStatusEvent(getIntegraTime(), getStatusByte1(), getStatusByte2()));
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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.satel.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.event.IntegraVersionEvent;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Command class for command that returns Integra version and type.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class IntegraVersionCommand extends SatelCommandBase {
private final Logger logger = LoggerFactory.getLogger(IntegraVersionCommand.class);
public static final byte COMMAND_CODE = 0x7e;
/**
* Creates new command class instance.
*/
public IntegraVersionCommand() {
super(COMMAND_CODE, false);
}
/**
* @return Integra firmware version and release date
*/
public String getVersion() {
return getVersion(1);
}
/**
* @return Integra type
*/
public byte getType() {
return getResponse().getPayload()[0];
}
/**
* @return firmware language
*/
public byte getLanguage() {
return getResponse().getPayload()[12];
}
/**
* @return <code>true</code> if alarm settings are stored in flash memory
*/
public boolean areSettingsInFlash() {
return getResponse().getPayload()[13] == (byte) 0xFF;
}
@Override
protected boolean isResponseValid(SatelMessage response) {
// validate response
if (response.getPayload().length != 14) {
logger.debug("Invalid payload length: {}", response.getPayload().length);
return false;
}
return true;
}
@Override
protected void handleResponseInternal(final EventDispatcher eventDispatcher) {
// dispatch version event
eventDispatcher
.dispatchEvent(new IntegraVersionEvent(getType(), getVersion(), getLanguage(), areSettingsInFlash()));
}
}

View File

@@ -0,0 +1,70 @@
/**
* 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.satel.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.event.ModuleVersionEvent;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Command class for command that returns communication module version.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class ModuleVersionCommand extends SatelCommandBase {
private final Logger logger = LoggerFactory.getLogger(ModuleVersionCommand.class);
public static final byte COMMAND_CODE = 0x7c;
/**
* Creates new command class instance.
*/
public ModuleVersionCommand() {
super(COMMAND_CODE, false);
}
/**
* @return communication module firmware version and release date
*/
public String getVersion() {
return getVersion(0);
}
/**
* @return <code>true</code> if the module supports extended (32-bit) payload for zones/outputs
*/
public boolean hasExtPayloadSupport() {
return (getResponse().getPayload()[11] & 0x01) != 0;
}
@Override
protected boolean isResponseValid(SatelMessage response) {
// validate response
if (response.getPayload().length != 12) {
logger.debug("Invalid payload length: {}", response.getPayload().length);
return false;
}
return true;
}
@Override
protected void handleResponseInternal(final EventDispatcher eventDispatcher) {
// dispatch version event
eventDispatcher.dispatchEvent(new ModuleVersionEvent(getVersion(), hasExtPayloadSupport()));
}
}

View File

@@ -0,0 +1,58 @@
/**
* 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.satel.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.event.NewStatesEvent;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Command class for command that returns list of states changed since last state read.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class NewStatesCommand extends SatelCommandBase {
private final Logger logger = LoggerFactory.getLogger(NewStatesCommand.class);
public static final byte COMMAND_CODE = 0x7f;
/**
* Creates new command class instance.
*
* @param extended if <code>true</code> command will be sent as extended (256 zones or outputs)
*/
public NewStatesCommand(boolean extended) {
super(COMMAND_CODE, extended);
}
@Override
protected boolean isResponseValid(SatelMessage response) {
// validate response
if (response.getPayload().length < 5 || response.getPayload().length > 6) {
logger.debug("Invalid payload length: {}", response.getPayload().length);
return false;
}
return true;
}
@Override
protected void handleResponseInternal(final EventDispatcher eventDispatcher) {
// dispatch event
eventDispatcher.dispatchEvent(new NewStatesEvent(getResponse().getPayload()));
}
}

View File

@@ -0,0 +1,156 @@
/**
* 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.satel.internal.command;
import java.nio.charset.Charset;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Command class for command that reads information about specific device
* (partition, zone, user, etc).
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class ReadDeviceInfoCommand extends SatelCommandBase {
private final Logger logger = LoggerFactory.getLogger(ReadDeviceInfoCommand.class);
public static final byte COMMAND_CODE = (byte) 0xee;
/**
* Device type: partition, zone, expander, etc.
*
* @author Krzysztof Goworek - Initial contribution
*
*/
public enum DeviceType {
PARTITION(0),
ZONE(1),
USER(2),
EXPANDER(3),
KEYPAD(3),
OUTPUT(4),
ZONE_WITH_PARTITION(5, true),
TIMER(6),
TELEPHONE(7),
OBJECT(15),
PARTITION_WITH_OBJECT(16, true);
int code;
boolean additionalInfo;
DeviceType(int code) {
this(code, false);
}
DeviceType(int code, boolean additionalInfo) {
this.code = code;
this.additionalInfo = additionalInfo;
}
int getCode() {
return code;
}
boolean hasAdditionalInfo() {
return additionalInfo;
}
}
/**
* Creates new command class instance to read description for given
* parameters.
*
* @param deviceType type of the device
* @param deviceNumber device number
*/
public ReadDeviceInfoCommand(DeviceType deviceType, int deviceNumber) {
super(COMMAND_CODE, new byte[] { (byte) deviceType.getCode(), getDeviceNumber(deviceType, deviceNumber) });
}
private static byte getDeviceNumber(DeviceType deviceType, int deviceNumber) {
switch (deviceType) {
case EXPANDER:
if (deviceNumber < 128) {
return (byte) (deviceNumber + 128);
}
break;
case KEYPAD:
if (deviceNumber < 128) {
return (byte) (deviceNumber + 192);
}
break;
case ZONE:
case ZONE_WITH_PARTITION:
return (byte) (deviceNumber == 256 ? 0 : deviceNumber);
default:
break;
}
return (byte) deviceNumber;
}
/**
* Returns device model or function depending on device type:
* <ul>
* <li>partition - partition type</li>
* <li>zone - zone reaction</li>
* <li>user - 0</li>
* <li>object - 0</li>
* <li>expander - expander model, CA-64 PP, CA-64 E, etc</li>
* <li>LCD - LCD model, INT-KLCD, INT-KLCDR, etc</li>
* <li>output - output function</li>
* </ul>
*
* @return kind of the device
*/
public int getDeviceKind() {
return getResponse().getPayload()[2] & 0xff;
}
/**
* Returns name of the device decoded using given encoding. Encoding
* depends on firmware language and must be specified in the binding
* configuration.
*
* @param encoding encoding for the text
* @return device name
*/
public String getName(Charset encoding) {
return new String(getResponse().getPayload(), 3, 16, encoding).trim();
}
/**
* Returns additional info for some types of device.
*
* @return additional info
*/
public int getAdditionalInfo() {
final byte[] payload = getResponse().getPayload();
return (payload.length == 20) ? (payload[19] & 0xff) : 0;
}
@Override
protected boolean isResponseValid(SatelMessage response) {
// validate response
if (response.getPayload().length < 19 || response.getPayload().length > 20) {
logger.debug("Invalid payload length: {}", response.getPayload().length);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,221 @@
/**
* 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.satel.internal.command;
import java.time.LocalDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Command class for command that reads one record from the event log.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class ReadEventCommand extends SatelCommandBase {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public static final byte COMMAND_CODE = (byte) 0x8c;
/**
* Event class: zone alarms, partition alarms, arming, troubles, etc.
*
* @author Krzysztof Goworek - Initial contribution
*
*/
public enum EventClass {
ZONE_ALARMS("zone and tamper alarms"),
PARTITION_ALARMS("partition and expander alarms"),
ARMING("arming, disarming, alarm clearing"),
BYPASSES("zone bypasses and unbypasses"),
ACCESS_CONTROL("access control"),
TROUBLES("troubles"),
USER_FUNCTIONS("user functions"),
SYSTEM_EVENTS("system events");
private String description;
EventClass(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* Creates new command class instance to read a record under given index.
*
* @param eventIndex index of event record to retrieve, -1 for the most recent one
*/
public ReadEventCommand(int eventIndex) {
super(COMMAND_CODE, getIndexBytes(eventIndex));
}
private static byte[] getIndexBytes(int index) {
return new byte[] { (byte) ((index >> 16) & 0xff), (byte) ((index >> 8) & 0xff), (byte) (index & 0xff) };
}
/**
* Checks whether response data contains valid event record.
*
* @return <code>true</code> if returned record is empty (likely the last
* record in the log)
*/
public boolean isEmpty() {
return (getResponse().getPayload()[0] & 0x20) == 0;
}
/**
* Checks whether event record is present in the response data.
*
* @return <code>true</code> if event data is present in the response
*/
public boolean isEventPresent() {
return (getResponse().getPayload()[0] & 0x10) != 0;
}
/**
* Returns date and time of the event.
*
* @return date and time of the event
*/
public LocalDateTime getTimestamp() {
final byte[] payload = getResponse().getPayload();
final int currentYear = LocalDateTime.now().getYear();
final int yearBase = currentYear / 4;
final int yearMarker = (payload[0] >> 6) & 0x03;
int year = 4 * yearBase + yearMarker;
final int minutes = ((payload[2] & 0x0f) << 8) + (payload[3] & 0xff);
if (year > currentYear) {
year -= 4;
}
LocalDateTime result = LocalDateTime.of(year, (payload[2] >> 4) & 0x0f, payload[1] & 0x1f, minutes / 60,
minutes % 60);
return result;
}
/**
* Returns class of the event.
*
* @return event class of the event
* @see EventClass
*/
public EventClass getEventClass() {
final int eventClassIdx = (getResponse().getPayload()[1] >> 5) & 0x07;
return EventClass.values()[eventClassIdx];
}
/**
* Returns number of partion the event is about.
*
* @return partition number
*/
public int getPartition() {
return ((getResponse().getPayload()[4] >> 3) & 0x1f) + 1;
}
/**
* Returns number of partition keypad related to the event.
*
* @return partition keypad number
*/
public int getPartitionKeypad() {
return ((getResponse().getPayload()[4] >> 2) & 0x3f) + 1;
}
/**
* Returns event code the describes the event. It can be used to retrieve description text for this event.
*
* @return event code
* @see ReadEventDescCommand
*/
public int getEventCode() {
final byte[] payload = getResponse().getPayload();
return ((payload[4] & 0x03) << 8) + (payload[5] & 0xff);
}
/**
* Returns state restoration flag.
*
* @return <code>true</code> if this is restoration of some state (i.e.
* arming and disarming have the same code but different restoration
* flag)
*/
public boolean isRestore() {
return (getResponse().getPayload()[4] & 0x04) != 0;
}
/**
* Return source of the event.
*
* @return event source (zone number, user number, etc depending on event)
*/
public int getSource() {
return getResponse().getPayload()[6] & 0xff;
}
/**
* Returns object number for the event.
*
* @return object number (0..7)
*/
public int getObject() {
return (getResponse().getPayload()[7] >> 5) & 0x07;
}
/**
* Returns user control number for the event.
*
* @return user control number
*/
public int getUserControlNumber() {
return getResponse().getPayload()[7] & 0x1f;
}
/**
* Return index of previous event in the log. Can be used to iterate over tha event log.
*
* @return index of previous event record in the log
*/
public int getNextIndex() {
final byte[] payload = getResponse().getPayload();
return (payload[8] << 16) + ((payload[9] & 0xff) << 8) + (payload[10] & 0xff);
}
/**
* Returns current event index.
*
* @return index of current record echoed by communication module
*/
public int getCurrentIndex() {
final byte[] payload = getResponse().getPayload();
return (payload[11] << 16) + ((payload[12] & 0xff) << 8) + (payload[13] & 0xff);
}
@Override
protected boolean isResponseValid(SatelMessage response) {
// validate response
if (response.getPayload().length != 14) {
logger.debug("Invalid payload length: {}", response.getPayload().length);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,102 @@
/**
* 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.satel.internal.command;
import java.nio.charset.Charset;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Command class for command that reads description for specific event code.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class ReadEventDescCommand extends SatelCommandBase {
private final Logger logger = LoggerFactory.getLogger(ReadEventDescCommand.class);
public static final byte COMMAND_CODE = (byte) 0x8f;
/**
* Creates new command class instance to read description for given parameters.
*
* @param eventCode event code
* @param restore <code>true</code> if this is restoration
* @param longDescription <code>true</code> for long description, <code>false</code> for short one
*/
public ReadEventDescCommand(int eventCode, boolean restore, boolean longDescription) {
super(COMMAND_CODE, buildPayload(eventCode, restore, longDescription));
}
private static byte[] buildPayload(int eventCode, boolean restore, boolean longDescription) {
int firstByte = 0;
if (restore) {
firstByte |= 0x04;
}
if (longDescription) {
firstByte |= 0x80;
}
firstByte |= ((eventCode >> 8) & 0x03);
return new byte[] { (byte) firstByte, (byte) (eventCode & 0xff) };
}
/**
* Returns type of requested description, either long or short.
*
* @return <code>true</code> if long description has been requested
*/
public boolean isLongDescription() {
return (getRequest().getPayload()[0] & 0x80) != 0;
}
/**
* Returns text of the description decoded using given encoding.
* Encoding depends on firmware language and must be specified in the binding configuration.
*
* @param encoding encoding for the text
* @return text of the description
*/
public String getText(Charset encoding) {
final int length = isLongDescription() ? 46 : 16;
return new String(getResponse().getPayload(), 5, length, encoding).trim();
}
/**
* Returns kind of description, either short or long, depending on the request.
*
* @return kind of description
*/
public int getKind() {
final byte[] payload = getResponse().getPayload();
if (isLongDescription()) {
return payload[2] & 0xff;
} else {
return ((payload[3] & 0xff) << 8) + (payload[4] & 0xff);
}
}
@Override
protected boolean isResponseValid(SatelMessage response) {
// validate response
int properLength = isLongDescription() ? 51 : 21;
if (response.getPayload().length != properLength) {
logger.debug("Invalid payload length: {}", response.getPayload().length);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,70 @@
/**
* 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.satel.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.event.ZoneTemperatureEvent;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Command class for command that reads temperature in a zone.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class ReadZoneTemperature extends SatelCommandBase {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public static final byte COMMAND_CODE = (byte) 0x7d;
/**
* Creates new command class instance to read temperature in given zone.
*
* @param zoneNbr zone number, 1 ... 256
*/
public ReadZoneTemperature(int zoneNbr) {
super(COMMAND_CODE, new byte[] { (byte) (zoneNbr == 256 ? 0 : zoneNbr) });
}
/**
* Returns zone temperature (Celsius degrees).
*
* @return zone temperature
*/
public float getTemperature() {
final byte[] payload = getResponse().getPayload();
int temp = ((payload[1] & 0xff) << 8) + (payload[2] & 0xff);
return (temp - 110) / 2.0f;
}
@Override
protected boolean isResponseValid(SatelMessage response) {
// validate response
if (response.getPayload().length != 3) {
logger.debug("Invalid payload length: {}", response.getPayload().length);
return false;
}
return true;
}
@Override
protected void handleResponseInternal(final EventDispatcher eventDispatcher) {
// dispatch temperature event
int zoneNbr = getResponse().getPayload()[0];
eventDispatcher.dispatchEvent(new ZoneTemperatureEvent(zoneNbr == 0 ? 256 : zoneNbr, getTemperature()));
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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.satel.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
/**
* Interface for commands sent to communication module.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public interface SatelCommand {
/**
* State of command.
* <ul>
* <li>NEW - just created</li>
* <li>ENQUEUED - currently waiting in the queue</li>
* <li>SENT - sent to communication module</li>
* <li>SUCCEEDED - response received and successfully handled</li>
* <li>FAILED - either send failed or response is invalid</li>
* </ul>
*
* @author Krzysztof Goworek - Initial contribution
*
*/
public enum State {
NEW,
ENQUEUED,
SENT,
SUCCEEDED,
FAILED
}
/**
* Returns current state of the command.
*
* @return current state
*/
State getState();
/**
* Sets state of the command.
*
* @param state new state
*/
void setState(State state);
/**
* Returns request's message object.
*
* @return {@link SatelMessage} request object
*/
SatelMessage getRequest();
/**
* Checks whether a response message matches request enclosed in this object.
*
* @param response response message
* @return <code>true</code> if given response matches the request
*/
boolean matches(SatelMessage response);
/**
* Handles response received for the command. Usually generates an event with received data.
*
* @param eventDispatcher event dispatcher
* @param response response to handle
* @return <code>true</code> if response has been successfully handled
*/
boolean handleResponse(EventDispatcher eventDispatcher, SatelMessage response);
}

View File

@@ -0,0 +1,232 @@
/**
* 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.satel.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for all command classes.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public abstract class SatelCommandBase extends SatelMessage implements SatelCommand {
private final Logger logger = LoggerFactory.getLogger(SatelCommandBase.class);
/**
* Used in extended (INT-RS v2.xx) command version.
*/
protected static final byte[] EXTENDED_CMD_PAYLOAD = { 0x00 };
private static final byte COMMAND_RESULT_CODE = (byte) 0xef;
private volatile State state = State.NEW;
private boolean logResponseError = true;
private @Nullable SatelMessage response;
/**
* Creates new command basing on command code and extended command flag.
*
* @param commandCode command code
* @param extended if <code>true</code> command will be sent as extended (256 zones or outputs)
*/
public SatelCommandBase(byte commandCode, boolean extended) {
this(commandCode, extended ? EXTENDED_CMD_PAYLOAD : EMPTY_PAYLOAD);
}
/**
* Creates new instance with specified command code and payload.
*
* @param command command code
* @param payload command payload
*/
public SatelCommandBase(byte commandCode, byte[] payload) {
super(commandCode, payload);
}
@Override
public State getState() {
return state;
}
@Override
public void setState(State state) {
synchronized (this) {
this.state = state;
this.notifyAll();
}
}
@Override
public SatelMessage getRequest() {
return this;
}
@Override
public final boolean matches(@Nullable SatelMessage response) {
return response != null
&& (response.getCommand() == getCommand() || response.getCommand() == COMMAND_RESULT_CODE);
}
@Override
public final boolean handleResponse(EventDispatcher eventDispatcher, SatelMessage response) {
// if response is valid, store it for future use
if (response.getCommand() == COMMAND_RESULT_CODE) {
if (!hasCommandSucceeded(response)) {
return false;
}
} else if (response.getCommand() != getCommand()) {
logger.debug("Response code does not match command {}: {}", String.format("%02X", getCommand()), response);
return false;
} else if (!isResponseValid(response)) {
return false;
}
this.response = response;
handleResponseInternal(eventDispatcher);
return true;
}
public void ignoreResponseError() {
this.logResponseError = false;
}
protected SatelMessage getResponse() {
final SatelMessage response = this.response;
if (response != null) {
return response;
} else {
throw new IllegalStateException("Response not yet received for command. " + this.toString());
}
}
/**
* Checks whether given response is valid for the command.
*
* @param response message to check
* @return <code>true</code> if given message is valid response for the command
*/
protected abstract boolean isResponseValid(SatelMessage response);
/**
* Overriden in subclasses allows to execute action specific to given command (i.e. dispatch an event).
*
* @param eventDispatcher event dispatcher
*/
protected void handleResponseInternal(final EventDispatcher eventDispatcher) {
}
protected static int bcdToInt(byte[] bytes, int offset, int size) {
int result = 0, digit;
int byteIdx = offset;
int digits = size * 2;
for (int i = 0; i < digits; ++i) {
if (i % 2 == 0) {
digit = (bytes[byteIdx] >> 4) & 0x0f;
} else {
digit = (bytes[byteIdx]) & 0x0f;
byteIdx += 1;
}
result = result * 10 + digit;
}
return result;
}
protected boolean hasCommandSucceeded(SatelMessage response) {
// validate response message
if (response.getCommand() != COMMAND_RESULT_CODE) {
logger.debug("Invalid response code: {}. {}", String.format("%02X", response.getCommand()), getRequest());
return false;
}
if (response.getPayload().length != 1) {
logger.debug("Invalid payload length: {}. {}", response.getPayload().length, getRequest());
return false;
}
// check result code
byte responseCode = response.getPayload()[0];
String errorMsg;
switch (responseCode) {
case 0:
// success
return true;
case 0x01:
errorMsg = "Requesting user code not found";
break;
case 0x02:
errorMsg = "No access";
break;
case 0x03:
errorMsg = "Selected user does not exist";
break;
case 0x04:
errorMsg = "Selected user already exists";
break;
case 0x05:
errorMsg = "Wrong code or code already exists";
break;
case 0x06:
errorMsg = "Telephone code already exists";
break;
case 0x07:
errorMsg = "Changed code is the same";
break;
case 0x08:
errorMsg = "Other error";
break;
case 0x11:
errorMsg = "Can not arm, but can use force arm";
break;
case 0x12:
errorMsg = "Can not arm";
break;
case (byte) 0xff:
logger.trace("Command accepted");
return true;
default:
if (responseCode >= 0x80 && responseCode <= 0x8f) {
errorMsg = String.format("Other error: %02X", responseCode);
} else {
errorMsg = String.format("Unknown result code: %02X", responseCode);
}
}
if (logResponseError) {
logger.info("{}. {}", errorMsg, getRequest());
}
return false;
}
/**
* Decodes firmware version and release date from command payload
*
* @param offset starting offset in payload
* @return decoded firmware version and release date as string
*/
public String getVersion(int offset) {
// build version string
final byte[] payload = getResponse().getPayload();
String verStr = new String(payload, offset, 1) + "." + new String(payload, offset + 1, 2) + " "
+ new String(payload, offset + 3, 4) + "-" + new String(payload, offset + 7, 2) + "-"
+ new String(payload, offset + 9, 2);
return verStr;
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.satel.internal.command;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.apache.commons.lang.ArrayUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Command class for command to set RTC clock.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class SetClockCommand extends ControlCommand {
public static final byte COMMAND_CODE = (byte) 0x8e;
private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
/**
* Creates new command class instance.
*
* @param dateTime date and time to set
* @param userCode code of the user on behalf the control is made
*/
public SetClockCommand(LocalDateTime dateTime, String userCode) {
super(COMMAND_CODE, ArrayUtils.addAll(userCodeToBytes(userCode), getDateTimeBytes(dateTime)));
}
private static byte[] getDateTimeBytes(LocalDateTime dateTime) {
return DATETIME_FORMAT.format(dateTime).getBytes();
}
}

View File

@@ -0,0 +1,41 @@
/**
* 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.satel.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Atd100Config} contains configuration values for ATD-100 things.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class Atd100Config {
private int id;
private int refresh;
/**
* @return zone number
*/
public int getId() {
return id;
}
/**
* @return polling interval in minutes
*/
public int getRefresh() {
return refresh;
}
}

View File

@@ -0,0 +1,54 @@
/**
* 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.satel.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Ethm1Config} contains configuration values for Satel ETHM-1 bridge.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class Ethm1Config extends SatelBridgeConfig {
public static final String HOST = "host";
private @Nullable String host;
private int port;
private @Nullable String encryptionKey;
/**
* @return IP or hostname of the bridge
*/
public String getHost() {
final String host = this.host;
return host == null ? "" : host;
}
/**
* @return IP port the bridge listens to
*/
public int getPort() {
return port;
}
/**
* @return key used to encrypt messages
*/
public String getEncryptionKey() {
final String encryptionKey = this.encryptionKey;
return encryptionKey == null ? "" : encryptionKey;
}
}

View File

@@ -0,0 +1,36 @@
/**
* 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.satel.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link IntRSConfig} contains configuration values for Satel INT-RS bridge.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class IntRSConfig extends SatelBridgeConfig {
public static final String PORT = "port";
private @Nullable String port;
/**
* @return serial port to which the module is connected
*/
public @Nullable String getPort() {
return port;
}
}

View File

@@ -0,0 +1,70 @@
/**
* 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.satel.internal.config;
import java.nio.charset.Charset;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link SatelBridgeConfig} contains common configuration values for Satel bridge things.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class SatelBridgeConfig {
private int timeout;
private int refresh;
private @Nullable String userCode;
private @Nullable String encoding;
private boolean extCommands;
/**
* @return value of timeout in milliseconds
*/
public int getTimeout() {
return timeout;
}
/**
* @return polling interval in milliseconds
*/
public int getRefresh() {
return refresh;
}
/**
* @return user code in behalf of all the commands will be executed
*/
public String getUserCode() {
final String userCode = this.userCode;
return userCode == null ? "" : userCode;
}
/**
* @return encoding for texts received from the bridge (names, descriptions)
*/
public Charset getEncoding() {
final String encoding = this.encoding;
return encoding == null ? Charset.defaultCharset() : Charset.forName(encoding);
}
/**
* @return <code>true</code> if the module supports extended commands
*/
public boolean hasExtCommandsSupport() {
return extCommands;
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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.satel.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SatelThingConfig} contains common configuration values for Satel devices.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class SatelThingConfig {
public static final String ID = "id";
public static final String UP_ID = "upId";
public static final String DOWN_ID = "downId";
private int id;
private int upId;
private int downId;
private boolean invertState;
private boolean forceArming;
private boolean commandOnly;
private boolean wireless;
/**
* @return device identifier
*/
public int getId() {
return id;
}
/**
* @return for a shutter: output number to control "up" direction
*/
public int getUpId() {
return upId;
}
/**
* @return for a shutter: output number to control "down" direction
*/
public int getDownId() {
return downId;
}
/**
* @return if <code>true</code>, device's state should be inverted
*/
public boolean isStateInverted() {
return invertState;
}
/**
* @return if <code>true</code>, forces arming a partition
*/
public boolean isForceArmingEnabled() {
return forceArming;
}
/**
* @return if <code>true</code> the thing should accept only commands, it does not update its state
*/
public boolean isCommandOnly() {
return commandOnly;
}
/**
* @return if <code>true</code> the thing is a wireless device
*/
public boolean isWireless() {
return wireless;
}
}

View File

@@ -0,0 +1,196 @@
/**
* 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.satel.internal.discovery;
import static org.openhab.binding.satel.internal.SatelBindingConstants.*;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
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.binding.satel.internal.command.ReadDeviceInfoCommand;
import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand.DeviceType;
import org.openhab.binding.satel.internal.command.SatelCommand;
import org.openhab.binding.satel.internal.config.SatelThingConfig;
import org.openhab.binding.satel.internal.handler.SatelBridgeHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.type.ThingType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SatelDeviceDiscoveryService} searches for available Satel
* devices and modules connected to the API console
*
* @author Krzysztof Goworek - Initial contribution
*
*/
@NonNullByDefault
public class SatelDeviceDiscoveryService extends AbstractDiscoveryService {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Stream
.of(DEVICE_THING_TYPES_UIDS, VIRTUAL_THING_TYPES_UIDS).flatMap(uids -> uids.stream())
.collect(Collectors.toSet());
private static final int OUTPUT_FUNCTION_SHUTTER = 105;
private final Logger logger = LoggerFactory.getLogger(SatelDeviceDiscoveryService.class);
private SatelBridgeHandler bridgeHandler;
private Function<ThingTypeUID, ThingType> thingTypeProvider;
private volatile boolean scanStopped;
public SatelDeviceDiscoveryService(SatelBridgeHandler bridgeHandler,
Function<ThingTypeUID, ThingType> thingTypeProvider) {
super(SUPPORTED_THING_TYPES, 60, true);
this.bridgeHandler = bridgeHandler;
this.thingTypeProvider = thingTypeProvider;
}
@Override
protected void startScan() {
scanStopped = false;
if (bridgeHandler.isInitialized()) {
// add virtual things by default
for (ThingTypeUID thingTypeUID : VIRTUAL_THING_TYPES_UIDS) {
ThingType thingType = thingTypeProvider.apply(thingTypeUID);
addThing(thingTypeUID, null, thingType.getLabel(), Collections.emptyMap());
}
}
if (!scanStopped) {
scanForDevices(DeviceType.KEYPAD, 8);
}
if (!scanStopped) {
scanForDevices(DeviceType.EXPANDER, 64);
}
if (!scanStopped) {
scanForDevices(DeviceType.PARTITION_WITH_OBJECT, bridgeHandler.getIntegraType().getPartitions());
}
if (!scanStopped) {
scanForDevices(DeviceType.ZONE_WITH_PARTITION, bridgeHandler.getIntegraType().getZones());
}
if (!scanStopped) {
scanForDevices(DeviceType.OUTPUT, bridgeHandler.getIntegraType().getZones());
}
}
@Override
protected synchronized void stopScan() {
scanStopped = true;
super.stopScan();
}
private void scanForDevices(DeviceType deviceType, int maxNumber) {
logger.debug("Scanning for {} started", deviceType.name());
final Charset encoding = bridgeHandler.getEncoding();
for (int i = 1; i <= maxNumber && !scanStopped; ++i) {
ReadDeviceInfoCommand cmd = new ReadDeviceInfoCommand(deviceType, i);
cmd.ignoreResponseError();
if (bridgeHandler.sendCommand(cmd, false)) {
String name = cmd.getName(encoding);
int deviceKind = cmd.getDeviceKind();
int info = cmd.getAdditionalInfo();
logger.debug("Found device: type={}, id={}, name={}, kind/function={}, info={}", deviceType.name(), i,
name, deviceKind, info);
if (isDeviceAvailable(deviceType, deviceKind)) {
addDevice(deviceType, deviceKind, i, name);
}
} else if (cmd.getState() != SatelCommand.State.FAILED) {
// serious failure, disconnection or so
scanStopped = true;
logger.error("Unexpected failure during scan for {} using {}", deviceType.name(),
bridgeHandler.getThing().getUID().toString());
}
}
if (scanStopped) {
logger.debug("Scanning stopped");
} else {
logger.debug("Scanning for {} finished", deviceType.name());
}
}
private void addDevice(DeviceType deviceType, int deviceKind, int deviceId, String deviceName) {
ThingTypeUID thingTypeUID = getThingTypeUID(deviceType, deviceKind);
if (thingTypeUID == null) {
logger.warn("Unknown device found: type={}, kind={}, name={}", deviceType.name(), deviceKind, deviceName);
} else if (!getSupportedThingTypes().contains(thingTypeUID)) {
logger.warn("Unsupported device: {}", thingTypeUID.toString());
} else {
Map<String, Object> properties = new HashMap<>();
if (THING_TYPE_SHUTTER.equals(thingTypeUID)) {
properties.put(SatelThingConfig.UP_ID, deviceId);
properties.put(SatelThingConfig.DOWN_ID, deviceId + 1);
} else {
properties.put(SatelThingConfig.ID, deviceId);
}
addThing(thingTypeUID, String.valueOf(deviceId), deviceName, properties);
}
}
private void addThing(ThingTypeUID thingTypeUID, @Nullable String deviceId, String label,
Map<String, Object> properties) {
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
ThingUID thingUID;
if (deviceId == null) {
thingUID = new ThingUID(thingTypeUID, bridgeUID.getId());
} else {
thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceId);
}
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
.withBridge(bridgeUID).withLabel(label).withProperties(properties).build();
thingDiscovered(discoveryResult);
}
private static @Nullable ThingTypeUID getThingTypeUID(DeviceType deviceType, int deviceKind) {
switch (deviceType) {
case OUTPUT:
return (deviceKind == OUTPUT_FUNCTION_SHUTTER) ? THING_TYPE_SHUTTER : THING_TYPE_OUTPUT;
case PARTITION:
case PARTITION_WITH_OBJECT:
return THING_TYPE_PARTITION;
case ZONE:
case ZONE_WITH_PARTITION:
return THING_TYPE_ZONE;
default:
return null;
}
}
private static boolean isDeviceAvailable(DeviceType deviceType, int deviceKind) {
switch (deviceType) {
case OUTPUT:
return deviceKind != 0 && deviceKind != OUTPUT_FUNCTION_SHUTTER
&& (deviceKind != OUTPUT_FUNCTION_SHUTTER + 1);
case PARTITION:
case PARTITION_WITH_OBJECT:
case ZONE:
case ZONE_WITH_PARTITION:
return true;
default:
return false;
}
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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.satel.internal.event;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Event class describing connection status to Satel module.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class ConnectionStatusEvent implements SatelEvent {
private boolean connected;
private @Nullable String reason;
/**
* Constructs event class with given connection status.
*
* @param connected value describing connection status
*/
public ConnectionStatusEvent(boolean connected) {
this(connected, null);
}
/**
* Constructs event class with given connection status and disconnection reason.
*
* @param connected value describing connection status
* @param reason disconnection reason
*/
public ConnectionStatusEvent(boolean connected, @Nullable String reason) {
this.connected = connected;
this.reason = reason;
}
/**
* Returns status of connection.
*
* @return a boolean value describing connection status
*/
public boolean isConnected() {
return this.connected;
}
/**
* Returns disconnection reason.
*
* @return optional text description in case of disconnection
*/
public @Nullable String getReason() {
return reason;
}
@Override
public String toString() {
return String.format("%s: connected = %b, reason = %s", this.getClass().getName(), this.connected, this.reason);
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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.satel.internal.event;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Allows distributing incoming event to all registered listeners. Listeners
* must implement {@link SatelEventListener} interface.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class EventDispatcher {
private final Logger logger = LoggerFactory.getLogger(EventDispatcher.class);
private final Set<SatelEventListener> eventListeners = new CopyOnWriteArraySet<>();
@SuppressWarnings("unchecked")
private final Map<Class<? extends SatelEvent>, @Nullable Method> eventHandlers = Stream
.of(SatelEventListener.class.getDeclaredMethods())
.filter(m -> m.getParameterCount() == 1 && SatelEvent.class.isAssignableFrom(m.getParameterTypes()[0]))
.collect(Collectors.toMap(m -> (Class<SatelEvent>) m.getParameterTypes()[0], m -> m));
/**
* Add a listener for Satel events.
*
* @param eventListener the event listener to add.
*/
public void addEventListener(SatelEventListener eventListener) {
this.eventListeners.add(eventListener);
}
/**
* Remove a listener for Satel events.
*
* @param eventListener the event listener to remove.
*/
public void removeEventListener(SatelEventListener eventListener) {
this.eventListeners.remove(eventListener);
}
/**
* Dispatch incoming event to all listeners.
*
* @param event the event to distribute.
*/
public void dispatchEvent(SatelEvent event) {
final Method m = eventHandlers.get(event.getClass());
if (m == null) {
logger.warn("Missing event handler for event {}. Event discarded.", event.getClass().getName());
} else {
logger.debug("Distributing event: {}", event);
eventListeners.forEach(listener -> {
logger.trace("Distributing to {}", listener);
try {
m.invoke(listener, event);
} catch (ReflectiveOperationException e) {
logger.warn("Unable to distribute {} to {}", event.getClass().getName(), listener, e);
}
});
}
}
}

View File

@@ -0,0 +1,106 @@
/**
* 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.satel.internal.event;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.types.StateType;
/**
* Event class describing current state of zones/partitions/outputs/doors/troubles.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class IntegraStateEvent implements SatelEvent {
private byte command;
private BitSet stateBits;
private boolean extendedData;
private Map<StateType, BitSet> stateBitsMap = new HashMap<>();
/**
* Constructs new event instance from given state type and state bits.
*
* @param command the command byte
* @param stateBits state bits as byte array
* @param extendedData whether state bits are for extended command
*/
public IntegraStateEvent(byte command, byte[] stateBits, boolean extendedData) {
this.command = command;
this.stateBits = BitSet.valueOf(stateBits);
this.extendedData = extendedData;
}
/**
* Checks whether data in the event is valid for given type of state.
*
* @param stateType state type
* @return <code>true</code> if this event is valid for given stae
*/
public boolean hasDataForState(StateType stateType) {
return stateType.getRefreshCommand() == this.command;
}
/**
* Returns state bits as {@link BitSet}.
*
* @param stateType type of state
* @return bits for given state
* @throws IllegalArgumentException when wrong state type given
*/
public BitSet getStateBits(StateType stateType) {
if (!hasDataForState(stateType)) {
throw new IllegalArgumentException("Event does not have data for " + stateType);
}
int bitsCount = stateType.getBytesCount(extendedData) * 8;
// whole payload is a single state
if (stateType.getStartByte() == 0 && bitsCount == stateBits.size()) {
return stateBits;
}
return stateBitsMap.computeIfAbsent(stateType, k -> {
int startBit = k.getStartByte() * 8;
return stateBits.get(startBit, startBit + bitsCount);
});
}
/**
* Returns <code>true</code> if specified state bit is set for given state.
*
* @param stateType type of state
* @param nbr state bit number
* @return <code>true</code> if state bit is set
*/
public boolean isSet(StateType stateType, int nbr) {
return getStateBits(stateType).get(nbr);
}
/**
* Returns number of state bits that are active for given state.
*
* @param stateType type of state
* @return number of active states
*/
public int statesSet(StateType stateType) {
return getStateBits(stateType).cardinality();
}
@Override
public String toString() {
return String.format("IntegraStateEvent: command = %02X, extended = %b, active = %s", command, extendedData,
stateBits);
}
}

View File

@@ -0,0 +1,119 @@
/**
* 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.satel.internal.event;
import java.time.LocalDateTime;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Event class describing basic status bits and current time.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class IntegraStatusEvent implements SatelEvent {
private Optional<LocalDateTime> integraTime;
private boolean serviceMode;
private boolean troubles;
private boolean acu100Present;
private boolean intRxPresent;
private boolean troublesMemory;
private boolean grade23Set;
private byte integraType;
/**
* Constructs new event.
*
* @param integraTime current Integra date and time
* @param statusByte1 status bits, byte #1
* @param statusByte2 status bits, byte #2
*/
public IntegraStatusEvent(Optional<LocalDateTime> integraTime, byte statusByte1, byte statusByte2) {
this.integraTime = integraTime;
this.serviceMode = (statusByte1 & 0x80) != 0;
this.troubles = (statusByte1 & 0x40) != 0;
this.acu100Present = (statusByte2 & 0x80) != 0;
this.intRxPresent = (statusByte2 & 0x40) != 0;
this.troublesMemory = (statusByte2 & 0x20) != 0;
this.grade23Set = (statusByte2 & 0x10) != 0;
this.integraType = (byte) (statusByte2 & 0x0f);
}
/**
* @return current date and time on connected Integra or <code>Optional.empty()</code> if date/time set in the
* system is incorrect
*/
public Optional<LocalDateTime> getIntegraTime() {
return integraTime;
}
/**
* @return <code>true</code> if service mode is enabled
*/
public boolean inServiceMode() {
return serviceMode;
}
/**
* @return <code>true</code> if there are troubles in the system
*/
public boolean troublesPresent() {
return troubles;
}
/**
* @return <code>true</code> if the are troubles in the memory
*/
public boolean troublesMemory() {
return troublesMemory;
}
/**
* @return <code>true</code> if ACU-100 module is present in the system
*/
public boolean isAcu100Present() {
return acu100Present;
}
/**
* @return <code>true</code> if INT-RX module is present in the system
*/
public boolean isIntRxPresent() {
return intRxPresent;
}
/**
* @return <code>true</code> if Grade 2 or Grade 3 is enabled in Integra configuration
*/
public boolean isGrade23Set() {
return grade23Set;
}
/**
* @return Integra board type
*/
public int getIntegraType() {
return integraType;
}
@Override
public String toString() {
return String.format(
"IntegraStatusEvent: type = %d, time = %s, service mode = %b, troubles = %b, troubles memory = %b, ACU-100 = %b, INT-RX = %b, grade 2/3 = %b",
this.integraType, this.integraTime.map(LocalDateTime::toString).orElse("N/A"), this.serviceMode,
this.troubles, this.troublesMemory, this.acu100Present, this.intRxPresent, this.grade23Set);
}
}

View File

@@ -0,0 +1,80 @@
/**
* 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.satel.internal.event;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.types.IntegraType;
/**
* Event class describing type and version of connected Integra system.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class IntegraVersionEvent implements SatelEvent {
private byte type;
private String version;
private byte language;
private boolean settingsInFlash;
/**
* Constructs new event class.
*
* @param type Integra type
* @param version string describing version number and firmware revision
* @param language firmware language: 1 - english
* @param settingsInFlash settings stored in flash memory
*/
public IntegraVersionEvent(byte type, String version, byte language, boolean settingsInFlash) {
this.type = type;
this.version = version;
this.language = language;
this.settingsInFlash = settingsInFlash;
}
/**
* @return Integra type
* @see IntegraType
*/
public byte getType() {
return type;
}
/**
* @return firmware version and date
*/
public String getVersion() {
return version;
}
/**
* @return firmware language
*/
public byte getLanguage() {
return language;
}
/**
* @return <code>true</code> if configuration is stored in flash memory
*/
public boolean getSettingsInflash() {
return this.settingsInFlash;
}
@Override
public String toString() {
return String.format("IntegraVersionEvent: type = %d, version = %s, language = %d, settingsInFlash = %b",
this.type & 0xFF, this.version, this.language, this.settingsInFlash);
}
}

View File

@@ -0,0 +1,58 @@
/**
* 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.satel.internal.event;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Event class describing version of communication module.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class ModuleVersionEvent implements SatelEvent {
private String version;
private boolean extPayloadSupport;
/**
* Constructs new event class.
*
* @param version string describing version number and firmware revision
* @param extPayloadSupport the module supports extended (32-bit) payload for zones/outputs
*/
public ModuleVersionEvent(String version, boolean extPayloadSupport) {
this.version = version;
this.extPayloadSupport = extPayloadSupport;
}
/**
* @return firmware version and date
*/
public String getVersion() {
return version;
}
/**
* @return <code>true</code> if the module supports extended (32-bit) payload for zones/outputs
*/
public boolean hasExtPayloadSupport() {
return this.extPayloadSupport;
}
@Override
public String toString() {
return String.format("ModuleVersionEvent: version = %s, extPayloadSupport = %b", this.version,
this.extPayloadSupport);
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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.satel.internal.event;
import java.util.BitSet;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Event class describing changes in Integra state since last state read.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class NewStatesEvent implements SatelEvent {
private BitSet newStates;
/**
* Constructs event class from given {@link BitSet}.
*
* @param newStates changed states as {@link BitSet}
*/
public NewStatesEvent(BitSet newStates) {
this.newStates = newStates;
}
/**
* Constructs event class from given byte array.
*
* @param newStates changed states as byte array
*/
public NewStatesEvent(byte[] newStates) {
this(BitSet.valueOf(newStates));
}
/**
* Checks if specified state has changed since last read.
*
* @param nbr state number to check
* @return <code>true</code> if state has changed
*/
public boolean isNew(int nbr) {
return newStates.get(nbr);
}
@Override
public String toString() {
StringBuilder newStatesStr = new StringBuilder();
for (int i = this.newStates.nextSetBit(0); i >= 0; i = this.newStates.nextSetBit(i + 1)) {
if (newStatesStr.length() > 0) {
newStatesStr.append(",");
}
newStatesStr.append(String.format("%02X", i));
}
return String.format("NewStatesEvent: changed = [%s]", newStatesStr);
}
}

View File

@@ -0,0 +1,24 @@
/**
* 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.satel.internal.event;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Simple interface that all event classes must implement.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public interface SatelEvent {
}

View File

@@ -0,0 +1,81 @@
/**
* 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.satel.internal.event;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Event listener interface. All classes that want to receive Satel events must
* implement this interface.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public interface SatelEventListener {
/**
* Event handler for connection status events.
*
* @param event incoming event to handle
*/
default void incomingEvent(ConnectionStatusEvent event) {
}
/**
* Event handler for state events.
*
* @param event incoming event to handle
*/
default void incomingEvent(IntegraStateEvent event) {
}
/**
* Event handler for status events.
*
* @param event incoming event to handle
*/
default void incomingEvent(IntegraStatusEvent event) {
}
/**
* Event handler for Integra version events.
*
* @param event incoming event to handle
*/
default void incomingEvent(IntegraVersionEvent event) {
}
/**
* Event handler for communication module version events.
*
* @param event incoming event to handle
*/
default void incomingEvent(ModuleVersionEvent event) {
}
/**
* Event handler for events with list of new states.
*
* @param event incoming event to handle
*/
default void incomingEvent(NewStatesEvent event) {
}
/**
* Event handler for zone temperature events.
*
* @param event incoming event to handle
*/
default void incomingEvent(ZoneTemperatureEvent event) {
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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.satel.internal.event;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Event class describing current temperature in a zone.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class ZoneTemperatureEvent implements SatelEvent {
private int zoneNbr;
private float temperature;
/**
* Constructs new event class.
*
* @param zoneNbr zone number
* @param temperature current temperature in the zone
*/
public ZoneTemperatureEvent(int zoneNbr, float temperature) {
this.zoneNbr = zoneNbr;
this.temperature = temperature;
}
/**
* @return zone number
*/
public int getZoneNbr() {
return zoneNbr;
}
/**
* @return temperature in the zone
*/
public float getTemperature() {
return temperature;
}
@Override
public String toString() {
return String.format("ZoneTemperatureEvent: zoneNbr = %d, temperature = %.1f", this.zoneNbr, this.temperature);
}
}

View File

@@ -0,0 +1,122 @@
/**
* 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.satel.internal.handler;
import static org.openhab.binding.satel.internal.SatelBindingConstants.*;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.satel.internal.command.ReadZoneTemperature;
import org.openhab.binding.satel.internal.command.SatelCommand;
import org.openhab.binding.satel.internal.config.Atd100Config;
import org.openhab.binding.satel.internal.event.ZoneTemperatureEvent;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Atd100Handler} is responsible for handling commands, which are
* sent to one of the channels of a ATD-100 device.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class Atd100Handler extends WirelessChannelsHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_ATD100);
private final Logger logger = LoggerFactory.getLogger(getClass());
private @Nullable ScheduledFuture<?> pollingJob;
public Atd100Handler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
withBridgeHandlerPresent(bridgeHandler -> {
final ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob == null || pollingJob.isCancelled()) {
Atd100Config config = getConfigAs(Atd100Config.class);
Runnable pollingCommand = () -> {
if (bridgeHandler.getThing().getStatus() == ThingStatus.ONLINE) {
bridgeHandler.sendCommand(new ReadZoneTemperature(getThingConfig().getId()), true);
}
};
this.pollingJob = scheduler.scheduleWithFixedDelay(pollingCommand, 0, config.getRefresh(),
TimeUnit.MINUTES);
}
});
}
@Override
public void dispose() {
logger.debug("Disposing thing handler.");
final ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null && !pollingJob.isCancelled()) {
pollingJob.cancel(true);
}
this.pollingJob = null;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (CHANNEL_TEMPERATURE.equals(channelUID.getId())) {
logger.debug("New command for {}: {}", channelUID, command);
if (command == RefreshType.REFRESH) {
withBridgeHandlerPresent(bridgeHandler -> {
bridgeHandler.sendCommand(new ReadZoneTemperature(getThingConfig().getId()), true);
});
}
} else {
super.handleCommand(channelUID, command);
}
}
@Override
public void incomingEvent(ZoneTemperatureEvent event) {
logger.trace("Handling incoming event: {}", event);
if (event.getZoneNbr() == getThingConfig().getId()) {
updateState(CHANNEL_TEMPERATURE, new QuantityType<>(event.getTemperature(), SIUnits.CELSIUS));
}
}
@Override
protected boolean isWirelessDevice() {
return true;
}
@Override
protected Optional<SatelCommand> convertCommand(@Nullable ChannelUID channel, @Nullable Command command) {
// no commands supported
return Optional.empty();
}
}

View File

@@ -0,0 +1,83 @@
/**
* 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.satel.internal.handler;
import static org.openhab.binding.satel.internal.SatelBindingConstants.THING_TYPE_ETHM1;
import static org.openhab.binding.satel.internal.config.Ethm1Config.HOST;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.config.Ethm1Config;
import org.openhab.binding.satel.internal.protocol.Ethm1Module;
import org.openhab.binding.satel.internal.protocol.SatelModule;
import org.openhab.core.config.core.status.ConfigStatusMessage;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link Ethm1BridgeHandler} is a bridge handler for ETHM-1 communication module.
* All {@link SatelThingHandler}s use it to receive events and execute commands.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class Ethm1BridgeHandler extends SatelBridgeHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_ETHM1);
private final Logger logger = LoggerFactory.getLogger(Ethm1BridgeHandler.class);
public Ethm1BridgeHandler(Bridge bridge) {
super(bridge);
}
@Override
public void initialize() {
logger.debug("Initializing handler");
Ethm1Config config = getConfigAs(Ethm1Config.class);
if (StringUtils.isNotBlank(config.getHost())) {
SatelModule satelModule = new Ethm1Module(config.getHost(), config.getPort(), config.getTimeout(),
config.getEncryptionKey(), config.hasExtCommandsSupport());
super.initialize(satelModule);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"Cannot connect to Satel ETHM-1 module. IP address or host name not set.");
}
}
@Override
public Collection<ConfigStatusMessage> getConfigStatus() {
// The bridge IP address to be used for checks
String host = (String) getThing().getConfiguration().get(HOST);
Collection<ConfigStatusMessage> configStatusMessages;
// Check whether an IP address is provided
if (StringUtils.isBlank(host)) {
configStatusMessages = Collections.singletonList(ConfigStatusMessage.Builder.error(HOST)
.withMessageKeySuffix("hostEmpty").withArguments(HOST).build());
} else {
configStatusMessages = Collections.emptyList();
}
return configStatusMessages;
}
}

View File

@@ -0,0 +1,88 @@
/**
* 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.satel.internal.handler;
import static org.openhab.binding.satel.internal.SatelBindingConstants.THING_TYPE_INTRS;
import static org.openhab.binding.satel.internal.config.IntRSConfig.PORT;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.config.IntRSConfig;
import org.openhab.binding.satel.internal.protocol.IntRSModule;
import org.openhab.binding.satel.internal.protocol.SatelModule;
import org.openhab.core.config.core.status.ConfigStatusMessage;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link IntRSBridgeHandler} is a bridge handler for INT-RS communication module.
* All {@link SatelThingHandler}s use it to receive events and execute commands.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class IntRSBridgeHandler extends SatelBridgeHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_INTRS);
private final Logger logger = LoggerFactory.getLogger(IntRSBridgeHandler.class);
private final SerialPortManager serialPortManager;
public IntRSBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) {
super(bridge);
this.serialPortManager = serialPortManager;
}
@Override
public void initialize() {
logger.debug("Initializing handler");
final IntRSConfig config = getConfigAs(IntRSConfig.class);
final String port = config.getPort();
if (port != null && StringUtils.isNotBlank(port)) {
SatelModule satelModule = new IntRSModule(port, serialPortManager, config.getTimeout(),
config.hasExtCommandsSupport());
super.initialize(satelModule);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"Cannot connect to Satel INT-RS module. Serial port is not set.");
}
}
@Override
public Collection<ConfigStatusMessage> getConfigStatus() {
// The bridge serial port to be used for checks
final String port = (String) getThing().getConfiguration().get(PORT);
Collection<ConfigStatusMessage> configStatusMessages;
// Check whether a serial port is provided
if (StringUtils.isBlank(port)) {
configStatusMessages = Collections.singletonList(ConfigStatusMessage.Builder.error(PORT)
.withMessageKeySuffix("portEmpty").withArguments(PORT).build());
} else {
configStatusMessages = Collections.emptyList();
}
return configStatusMessages;
}
}

View File

@@ -0,0 +1,241 @@
/**
* 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.satel.internal.handler;
import java.nio.charset.Charset;
import java.time.ZoneId;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.satel.internal.command.NewStatesCommand;
import org.openhab.binding.satel.internal.command.SatelCommand;
import org.openhab.binding.satel.internal.config.SatelBridgeConfig;
import org.openhab.binding.satel.internal.event.ConnectionStatusEvent;
import org.openhab.binding.satel.internal.event.SatelEventListener;
import org.openhab.binding.satel.internal.protocol.SatelModule;
import org.openhab.binding.satel.internal.types.IntegraType;
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.ConfigStatusBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SatelBridgeHandler} is base class for all bridge handlers.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public abstract class SatelBridgeHandler extends ConfigStatusBridgeHandler implements SatelEventListener {
private final Logger logger = LoggerFactory.getLogger(SatelBridgeHandler.class);
private SatelBridgeConfig config = new SatelBridgeConfig();
private @Nullable SatelModule satelModule;
private @Nullable ScheduledFuture<?> pollingJob;
private String userCodeOverride = "";
private final ZoneId integraZone = ZoneId.systemDefault();
public SatelBridgeHandler(Bridge bridge) {
super(bridge);
}
@Override
public void incomingEvent(ConnectionStatusEvent event) {
final SatelModule satelModule = this.satelModule;
if (satelModule != null) {
// update bridge status and get new states from the system
if (event.isConnected()) {
updateStatus(ThingStatus.ONLINE);
satelModule.sendCommand(new NewStatesCommand(satelModule.hasExtPayloadSupport()));
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, event.getReason());
}
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// the bridge does not support any command at the moment
logger.debug("New command for {}: {}", channelUID, command);
}
protected void initialize(final SatelModule satelModule) {
logger.debug("Initializing bridge handler");
final SatelBridgeConfig config = getConfigAs(SatelBridgeConfig.class);
this.config = config;
this.satelModule = satelModule;
satelModule.addEventListener(this);
satelModule.open();
logger.debug("Satel module opened");
final ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob == null || pollingJob.isCancelled()) {
Runnable pollingCommand = () -> {
if (!satelModule.isInitialized()) {
logger.debug("Module not initialized yet, skipping refresh");
return;
}
// get list of states that have changed
logger.trace("Sending 'get new states' command");
satelModule.sendCommand(new NewStatesCommand(satelModule.hasExtPayloadSupport()));
};
this.pollingJob = scheduler.scheduleWithFixedDelay(pollingCommand, 0, config.getRefresh(),
TimeUnit.MILLISECONDS);
}
}
@Override
public void dispose() {
logger.debug("Disposing bridge handler.");
final ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null && !pollingJob.isCancelled()) {
pollingJob.cancel(true);
this.pollingJob = null;
}
final SatelModule satelModule = this.satelModule;
if (satelModule != null) {
satelModule.close();
this.satelModule = null;
logger.debug("Satel module closed.");
}
}
/**
* Adds given listener to list of event receivers.
*
* @param listener listener object to add
*/
public void addEventListener(SatelEventListener listener) {
final SatelModule satelModule = this.satelModule;
if (satelModule != null) {
satelModule.addEventListener(listener);
}
}
/**
* Removes given listener from list of event receivers.
*
* @param listener listener object to remove
*/
public void removeEventListener(SatelEventListener listener) {
final SatelModule satelModule = this.satelModule;
if (satelModule != null) {
satelModule.removeEventListener(listener);
}
}
@Override
public boolean isInitialized() {
final SatelModule satelModule = this.satelModule;
return satelModule != null && satelModule.isInitialized();
}
/**
* @return type of Integra system
* @see IntegraType
*/
public IntegraType getIntegraType() {
final SatelModule satelModule = this.satelModule;
return satelModule != null ? satelModule.getIntegraType() : IntegraType.UNKNOWN;
}
/**
* @return current user code, either from the configuration or set later using {@link #setUserCode(String)}
*/
public String getUserCode() {
if (StringUtils.isNotEmpty(userCodeOverride)) {
return userCodeOverride;
} else {
return config.getUserCode();
}
}
/**
* @param userCode new use code that overrides the one in the configuration
*/
public void setUserCode(String userCode) {
this.userCodeOverride = userCode;
}
/**
* @return encoding for texts
*/
public Charset getEncoding() {
try {
return config.getEncoding();
} catch (Exception e) {
logger.info("Invalid or unsupported encoding configured for {}", getThing().getUID());
return Charset.defaultCharset();
}
}
/**
* @return zone for Integra date and time values
*/
public ZoneId getZoneId() {
return integraZone;
}
/**
* Sends given command to communication module.
*
* @param command a command to send
* @param async if <code>false</code> method waits for the response
* @return <code>true</code> if send succeeded
*/
public boolean sendCommand(SatelCommand command, boolean async) {
final SatelModule satelModule = this.satelModule;
if (satelModule == null) {
return false;
}
if (async) {
return satelModule.sendCommand(command);
} else if (!satelModule.sendCommand(command, true)) {
return false;
}
boolean interrupted = false;
while (!interrupted) {
// wait for command state change
try {
synchronized (command) {
command.wait(satelModule.getTimeout());
}
} catch (InterruptedException e) {
// ignore, we will leave the loop on next interruption state check
interrupted = true;
}
// check current state
switch (command.getState()) {
case SUCCEEDED:
return true;
case FAILED:
return false;
default:
// wait for next change unless interrupted
}
}
return false;
}
}

View File

@@ -0,0 +1,422 @@
/**
* 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.satel.internal.handler;
import static org.openhab.binding.satel.internal.SatelBindingConstants.*;
import java.nio.charset.Charset;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.satel.action.SatelEventLogActions;
import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand;
import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand.DeviceType;
import org.openhab.binding.satel.internal.command.ReadEventCommand;
import org.openhab.binding.satel.internal.command.ReadEventDescCommand;
import org.openhab.binding.satel.internal.event.ConnectionStatusEvent;
import org.openhab.binding.satel.internal.types.IntegraType;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SatelEventLogHandler} is responsible for handling commands, which are
* sent to one of the event log channels.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class SatelEventLogHandler extends SatelThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_EVENTLOG);
private static final String NOT_AVAILABLE_TEXT = "N/A";
private static final String DETAILS_SEPARATOR = ", ";
private static final long CACHE_CLEAR_INTERVAL = TimeUnit.MINUTES.toMillis(30);
private final Logger logger = LoggerFactory.getLogger(SatelEventLogHandler.class);
private final Map<String, @Nullable EventDescriptionCacheEntry> eventDescriptions = new ConcurrentHashMap<>();
private final Map<String, @Nullable String> deviceNameCache = new ConcurrentHashMap<>();
private @Nullable ScheduledFuture<?> cacheExpirationJob;
private Charset encoding = Charset.defaultCharset();
/**
* Represents single record of the event log.
*
* @author Krzysztof Goworek
*
*/
public static class EventLogEntry {
private final int index;
private final int prevIndex;
private final ZonedDateTime timestamp;
private final String description;
private final String details;
private EventLogEntry(int index, int prevIndex, ZonedDateTime timestamp, String description, String details) {
this.index = index;
this.prevIndex = prevIndex;
this.timestamp = timestamp;
this.description = description;
this.details = details;
}
/**
* @return index of this record entry
*/
public int getIndex() {
return index;
}
/**
* @return index of the previous record entry in the log
*/
public int getPrevIndex() {
return prevIndex;
}
/**
* @return date and time when the event occurred
*/
public ZonedDateTime getTimestamp() {
return timestamp;
}
/**
* @return description of the event
*/
public String getDescription() {
return description;
}
/**
* @return details about zones, partitions, users, etc
*/
public String getDetails() {
return details;
}
@Override
public String toString() {
return "EventLogEntry [index=" + index + ", prevIndex=" + prevIndex + ", timestamp=" + timestamp
+ ", description=" + description + ", details=" + details + "]";
}
}
public SatelEventLogHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
withBridgeHandlerPresent(bridgeHandler -> {
this.encoding = bridgeHandler.getEncoding();
});
final ScheduledFuture<?> cacheExpirationJob = this.cacheExpirationJob;
if (cacheExpirationJob == null || cacheExpirationJob.isCancelled()) {
// for simplicity all cache entries are cleared every 30 minutes
this.cacheExpirationJob = scheduler.scheduleWithFixedDelay(deviceNameCache::clear, CACHE_CLEAR_INTERVAL,
CACHE_CLEAR_INTERVAL, TimeUnit.MILLISECONDS);
}
}
@Override
public void dispose() {
super.dispose();
final ScheduledFuture<?> cacheExpirationJob = this.cacheExpirationJob;
if (cacheExpirationJob != null && !cacheExpirationJob.isCancelled()) {
cacheExpirationJob.cancel(true);
}
this.cacheExpirationJob = null;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("New command for {}: {}", channelUID, command);
if (CHANNEL_INDEX.equals(channelUID.getId()) && command instanceof DecimalType) {
int eventIndex = ((DecimalType) command).intValue();
withBridgeHandlerPresent(bridgeHandler -> readEvent(eventIndex).ifPresent(entry -> {
// update items
updateState(CHANNEL_INDEX, new DecimalType(entry.getIndex()));
updateState(CHANNEL_PREV_INDEX, new DecimalType(entry.getPrevIndex()));
updateState(CHANNEL_TIMESTAMP, new DateTimeType(entry.getTimestamp()));
updateState(CHANNEL_DESCRIPTION, new StringType(entry.getDescription()));
updateState(CHANNEL_DETAILS, new StringType(entry.getDetails()));
}));
}
}
@Override
public void incomingEvent(ConnectionStatusEvent event) {
logger.trace("Handling incoming event: {}", event);
// we have just connected, change thing's status
if (event.isConnected()) {
updateStatus(ThingStatus.ONLINE);
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(SatelEventLogActions.class);
}
/**
* Reads one record from the event log.
*
* @param eventIndex record index
* @return record data or {@linkplain Optional#empty()} if there is no record under given index
*/
public Optional<EventLogEntry> readEvent(int eventIndex) {
return getEventDescription(eventIndex).flatMap(eventDesc -> {
ReadEventCommand readEventCmd = eventDesc.readEventCmd;
int currentIndex = readEventCmd.getCurrentIndex();
String eventText = eventDesc.getText();
boolean upperZone = getBridgeHandler().getIntegraType() == IntegraType.I256_PLUS
&& readEventCmd.getUserControlNumber() > 0;
String eventDetails;
switch (eventDesc.getKind()) {
case 0:
eventDetails = "";
break;
case 1:
eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
+ DETAILS_SEPARATOR + getZoneExpanderKeypadDescription(readEventCmd.getSource(), upperZone);
break;
case 2:
eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
+ DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
break;
case 3:
eventDetails = getDeviceDescription(DeviceType.EXPANDER, readEventCmd.getPartitionKeypad())
+ DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
break;
case 4:
eventDetails = getZoneExpanderKeypadDescription(readEventCmd.getSource(), upperZone);
break;
case 5:
eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition());
break;
case 6:
eventDetails = getDeviceDescription(DeviceType.KEYPAD, readEventCmd.getPartition())
+ DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
break;
case 7:
eventDetails = getUserDescription(readEventCmd.getSource());
break;
case 8:
eventDetails = getDeviceDescription(DeviceType.EXPANDER, readEventCmd.getSource());
break;
case 9:
eventDetails = getTelephoneDescription(readEventCmd.getSource());
break;
case 11:
eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
+ DETAILS_SEPARATOR + getDataBusDescription(readEventCmd.getSource());
break;
case 12:
if (readEventCmd.getSource() <= getBridgeHandler().getIntegraType().getOnMainboard()) {
eventDetails = getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
} else {
eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
+ DETAILS_SEPARATOR + getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
}
break;
case 13:
if (readEventCmd.getSource() <= 128) {
eventDetails = getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
} else {
eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
+ DETAILS_SEPARATOR + getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
}
break;
case 14:
eventDetails = getTelephoneDescription(readEventCmd.getPartition()) + DETAILS_SEPARATOR
+ getUserDescription(readEventCmd.getSource());
break;
case 15:
eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
+ DETAILS_SEPARATOR + getDeviceDescription(DeviceType.TIMER, readEventCmd.getSource());
break;
case 31:
// this description consists of two records, so we must read additional record from the log
eventDetails = "." + readEventCmd.getSource() + "."
+ (readEventCmd.getObject() * 32 + readEventCmd.getUserControlNumber());
Optional<EventDescription> eventDescNext = getEventDescription(readEventCmd.getNextIndex());
if (!eventDescNext.isPresent()) {
return Optional.empty();
}
final EventDescription eventDescNextItem = eventDescNext.get();
if (eventDescNextItem.getKind() != 30) {
logger.info("Unexpected event record kind {} at index {}", eventDescNextItem.getKind(),
readEventCmd.getNextIndex());
return Optional.empty();
}
readEventCmd = eventDescNextItem.readEventCmd;
eventText = eventDescNextItem.getText();
eventDetails = getDeviceDescription(DeviceType.KEYPAD, readEventCmd.getPartition())
+ DETAILS_SEPARATOR + "ip: " + readEventCmd.getSource() + "."
+ (readEventCmd.getObject() * 32 + readEventCmd.getUserControlNumber()) + eventDetails;
break;
case 32:
eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
+ DETAILS_SEPARATOR + getDeviceDescription(DeviceType.ZONE, readEventCmd.getSource());
break;
default:
logger.info("Unsupported device kind code {} at index {}", eventDesc.getKind(),
readEventCmd.getCurrentIndex());
eventDetails = String.join(DETAILS_SEPARATOR, "kind=" + eventDesc.getKind(),
"partition=" + readEventCmd.getPartition(), "source=" + readEventCmd.getSource(),
"object=" + readEventCmd.getObject(), "ucn=" + readEventCmd.getUserControlNumber());
}
return Optional.of(new EventLogEntry(currentIndex, readEventCmd.getNextIndex(),
readEventCmd.getTimestamp().atZone(getBridgeHandler().getZoneId()), eventText, eventDetails));
});
}
private Optional<EventDescription> getEventDescription(int eventIndex) {
ReadEventCommand readEventCmd = new ReadEventCommand(eventIndex);
if (!getBridgeHandler().sendCommand(readEventCmd, false)) {
logger.info("Unable to read event record for given index: {}", eventIndex);
return Optional.empty();
} else if (readEventCmd.isEmpty()) {
logger.info("No record under given index: {}", eventIndex);
return Optional.empty();
} else {
return Optional.of(readEventDescription(readEventCmd));
}
}
private static class EventDescriptionCacheEntry {
private final String eventText;
private final int descKind;
EventDescriptionCacheEntry(String eventText, int descKind) {
this.eventText = eventText;
this.descKind = descKind;
}
String getText() {
return eventText;
}
int getKind() {
return descKind;
}
}
private static class EventDescription extends EventDescriptionCacheEntry {
private final ReadEventCommand readEventCmd;
EventDescription(ReadEventCommand readEventCmd, String eventText, int descKind) {
super(eventText, descKind);
this.readEventCmd = readEventCmd;
}
}
private EventDescription readEventDescription(ReadEventCommand readEventCmd) {
int eventCode = readEventCmd.getEventCode();
boolean restore = readEventCmd.isRestore();
String mapKey = String.format("%d_%b", eventCode, restore);
EventDescriptionCacheEntry mapValue = eventDescriptions.computeIfAbsent(mapKey, k -> {
ReadEventDescCommand cmd = new ReadEventDescCommand(eventCode, restore, true);
if (!getBridgeHandler().sendCommand(cmd, false)) {
logger.info("Unable to read event description: {}, {}", eventCode, restore);
return null;
}
return new EventDescriptionCacheEntry(cmd.getText(encoding), cmd.getKind());
});
if (mapValue == null) {
return new EventDescription(readEventCmd, NOT_AVAILABLE_TEXT, 0);
} else {
return new EventDescription(readEventCmd, mapValue.getText(), mapValue.getKind());
}
}
private String getOutputExpanderDescription(int deviceNumber, boolean upperOutput) {
if (deviceNumber == 0) {
return "mainboard";
} else if (deviceNumber <= 128) {
return getDeviceDescription(DeviceType.OUTPUT, upperOutput ? 128 + deviceNumber : deviceNumber);
} else if (deviceNumber <= 192) {
return getDeviceDescription(DeviceType.EXPANDER, deviceNumber);
} else {
return "invalid output|expander device: " + deviceNumber;
}
}
private String getZoneExpanderKeypadDescription(int deviceNumber, boolean upperZone) {
if (deviceNumber == 0) {
return "mainboard";
} else if (deviceNumber <= 128) {
return getDeviceDescription(DeviceType.ZONE, upperZone ? 128 + deviceNumber : deviceNumber);
} else if (deviceNumber <= 192) {
return getDeviceDescription(DeviceType.EXPANDER, deviceNumber);
} else {
return getDeviceDescription(DeviceType.KEYPAD, deviceNumber);
}
}
private String getUserDescription(int deviceNumber) {
return deviceNumber == 0 ? "user: unknown" : getDeviceDescription(DeviceType.USER, deviceNumber);
}
private String getDataBusDescription(int deviceNumber) {
return "data bus: " + deviceNumber;
}
private String getTelephoneDescription(int deviceNumber) {
return deviceNumber == 0 ? "telephone: unknown" : getDeviceDescription(DeviceType.TELEPHONE, deviceNumber);
}
private String getDeviceDescription(DeviceType deviceType, int deviceNumber) {
return String.format("%s: %s", deviceType.name().toLowerCase(), readDeviceName(deviceType, deviceNumber));
}
private String readDeviceName(DeviceType deviceType, int deviceNumber) {
String cacheKey = String.format("%s_%d", deviceType, deviceNumber);
String result = deviceNameCache.computeIfAbsent(cacheKey, k -> {
ReadDeviceInfoCommand cmd = new ReadDeviceInfoCommand(deviceType, deviceNumber);
if (!getBridgeHandler().sendCommand(cmd, false)) {
logger.info("Unable to read device info: {}, {}", deviceType, deviceNumber);
return null;
}
return cmd.getName(encoding);
});
return result == null ? NOT_AVAILABLE_TEXT : result;
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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.satel.internal.handler;
import static org.openhab.binding.satel.internal.SatelBindingConstants.THING_TYPE_OUTPUT;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.command.ControlObjectCommand;
import org.openhab.binding.satel.internal.command.SatelCommand;
import org.openhab.binding.satel.internal.types.OutputControl;
import org.openhab.binding.satel.internal.types.OutputState;
import org.openhab.binding.satel.internal.types.StateType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
/**
* The {@link SatelOutputHandler} is responsible for handling commands, which are
* sent to one of the channels of an output device.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class SatelOutputHandler extends WirelessChannelsHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_OUTPUT);
public SatelOutputHandler(Thing thing) {
super(thing);
}
@Override
protected Optional<SatelCommand> convertCommand(ChannelUID channel, Command command) {
if (command instanceof OnOffType && getStateType(channel.getId()) == OutputState.STATE) {
final SatelBridgeHandler bridgeHandler = getBridgeHandler();
boolean switchOn = (command == OnOffType.ON);
int size = bridgeHandler.getIntegraType().hasExtPayload() ? 32 : 16;
byte[] outputs = getObjectBitset(size, getThingConfig().getId());
boolean newState = switchOn ^ getThingConfig().isStateInverted();
return Optional.of(new ControlObjectCommand(newState ? OutputControl.ON : OutputControl.OFF, outputs,
bridgeHandler.getUserCode(), scheduler));
}
return Optional.empty();
}
@Override
protected StateType getStateType(String channelId) {
StateType result = super.getStateType(channelId);
if (result == StateType.NONE) {
result = OutputState.valueOf(channelId.toUpperCase());
}
return result;
}
}

View File

@@ -0,0 +1,104 @@
/**
* 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.satel.internal.handler;
import static org.openhab.binding.satel.internal.SatelBindingConstants.THING_TYPE_PARTITION;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.command.ControlObjectCommand;
import org.openhab.binding.satel.internal.command.SatelCommand;
import org.openhab.binding.satel.internal.types.PartitionControl;
import org.openhab.binding.satel.internal.types.PartitionState;
import org.openhab.binding.satel.internal.types.StateType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
/**
* The {@link SatelPartitionHandler} is responsible for handling commands, which are
* sent to one of the channels of a partition device.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class SatelPartitionHandler extends SatelStateThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_PARTITION);
public SatelPartitionHandler(Thing thing) {
super(thing);
}
@Override
protected StateType getStateType(String channelId) {
return PartitionState.valueOf(channelId.toUpperCase());
}
@Override
protected Optional<SatelCommand> convertCommand(ChannelUID channel, Command command) {
if (command instanceof OnOffType) {
boolean switchOn = (command == OnOffType.ON);
StateType stateType = getStateType(channel.getId());
byte[] partitions = getObjectBitset(4, getThingConfig().getId());
boolean forceArm = getThingConfig().isForceArmingEnabled();
PartitionControl action = null;
switch ((PartitionState) stateType) {
// clear alarms on OFF command
case ALARM:
case ALARM_MEMORY:
case FIRE_ALARM:
case FIRE_ALARM_MEMORY:
case VERIFIED_ALARMS:
case WARNING_ALARMS:
action = switchOn ? null : PartitionControl.CLEAR_ALARM;
break;
// arm or disarm, depending on command
case ARMED:
case REALLY_ARMED:
action = switchOn ? (forceArm ? PartitionControl.FORCE_ARM_MODE_0 : PartitionControl.ARM_MODE_0)
: PartitionControl.DISARM;
break;
case ARMED_MODE_1:
action = switchOn ? (forceArm ? PartitionControl.FORCE_ARM_MODE_1 : PartitionControl.ARM_MODE_1)
: PartitionControl.DISARM;
break;
case ARMED_MODE_2:
action = switchOn ? (forceArm ? PartitionControl.FORCE_ARM_MODE_2 : PartitionControl.ARM_MODE_2)
: PartitionControl.DISARM;
break;
case ARMED_MODE_3:
action = switchOn ? (forceArm ? PartitionControl.FORCE_ARM_MODE_3 : PartitionControl.ARM_MODE_3)
: PartitionControl.DISARM;
break;
// do nothing for other types of state
default:
break;
}
if (action != null) {
return Optional
.of(new ControlObjectCommand(action, partitions, getBridgeHandler().getUserCode(), scheduler));
}
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,100 @@
/**
* 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.satel.internal.handler;
import static org.openhab.binding.satel.internal.SatelBindingConstants.*;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.command.ControlObjectCommand;
import org.openhab.binding.satel.internal.command.SatelCommand;
import org.openhab.binding.satel.internal.event.IntegraStateEvent;
import org.openhab.binding.satel.internal.types.OutputControl;
import org.openhab.binding.satel.internal.types.OutputState;
import org.openhab.binding.satel.internal.types.StateType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SatelShutterHandler} is responsible for handling commands, which are
* sent to one of the channels of a shutter device.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class SatelShutterHandler extends SatelStateThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_SHUTTER);
private final Logger logger = LoggerFactory.getLogger(SatelShutterHandler.class);
public SatelShutterHandler(Thing thing) {
super(thing);
}
@Override
public void incomingEvent(IntegraStateEvent event) {
logger.trace("Handling incoming event: {}", event);
if (getThingConfig().isCommandOnly() || !event.hasDataForState(OutputState.STATE)) {
return;
}
Channel channel = getThing().getChannel(CHANNEL_SHUTTER_STATE);
if (channel != null) {
int upBitNbr = getThingConfig().getUpId() - 1;
int downBitNbr = getThingConfig().getDownId() - 1;
if (event.isSet(OutputState.STATE, upBitNbr)) {
if (!event.isSet(OutputState.STATE, downBitNbr)) {
updateState(channel.getUID(), UpDownType.UP);
}
} else if (event.isSet(OutputState.STATE, downBitNbr)) {
updateState(channel.getUID(), UpDownType.DOWN);
}
}
}
@Override
protected StateType getStateType(String channelId) {
return CHANNEL_SHUTTER_STATE.equals(channelId) ? OutputState.STATE : StateType.NONE;
}
@Override
protected Optional<SatelCommand> convertCommand(ChannelUID channel, Command command) {
ControlObjectCommand result = null;
if (CHANNEL_SHUTTER_STATE.equals(channel.getId())) {
final SatelBridgeHandler bridgeHandler = getBridgeHandler();
int cmdBytes = bridgeHandler.getIntegraType().hasExtPayload() ? 32 : 16;
if (command == UpDownType.UP) {
byte[] outputs = getObjectBitset(cmdBytes, getThingConfig().getUpId());
result = new ControlObjectCommand(OutputControl.ON, outputs, bridgeHandler.getUserCode(), scheduler);
} else if (command == UpDownType.DOWN) {
byte[] outputs = getObjectBitset(cmdBytes, getThingConfig().getDownId());
result = new ControlObjectCommand(OutputControl.ON, outputs, bridgeHandler.getUserCode(), scheduler);
} else if (command == StopMoveType.STOP) {
byte[] outputs = getObjectBitset(cmdBytes, getThingConfig().getUpId(), getThingConfig().getDownId());
result = new ControlObjectCommand(OutputControl.OFF, outputs, bridgeHandler.getUserCode(), scheduler);
}
}
return result == null ? Optional.empty() : Optional.of(result);
}
}

View File

@@ -0,0 +1,190 @@
/**
* 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.satel.internal.handler;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.satel.internal.command.IntegraStateCommand;
import org.openhab.binding.satel.internal.command.SatelCommand;
import org.openhab.binding.satel.internal.event.ConnectionStatusEvent;
import org.openhab.binding.satel.internal.event.IntegraStateEvent;
import org.openhab.binding.satel.internal.event.NewStatesEvent;
import org.openhab.binding.satel.internal.types.StateType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SatelStateThingHandler} is base thing handler class for all state holding things.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public abstract class SatelStateThingHandler extends SatelThingHandler {
private final Logger logger = LoggerFactory.getLogger(SatelStateThingHandler.class);
private final AtomicBoolean requiresRefresh = new AtomicBoolean(true);
public SatelStateThingHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("New command for {}: {}", channelUID, command);
if (command == RefreshType.REFRESH) {
this.requiresRefresh.set(true);
} else {
withBridgeHandlerPresent(bridgeHandler -> {
if (StringUtils.isEmpty(bridgeHandler.getUserCode())) {
logger.info("Cannot control devices without providing valid user code. Command has not been sent.");
} else {
convertCommand(channelUID, command)
.ifPresent(satelCommand -> bridgeHandler.sendCommand(satelCommand, true));
}
});
}
}
@Override
public void incomingEvent(ConnectionStatusEvent event) {
logger.trace("Handling incoming event: {}", event);
// we have just connected, change thing's status and force refreshing
if (event.isConnected()) {
updateStatus(ThingStatus.ONLINE);
requiresRefresh.set(true);
}
}
@Override
public void incomingEvent(NewStatesEvent event) {
logger.trace("Handling incoming event: {}", event);
// refresh all states that have changed
withBridgeHandlerPresent(bridgeHandler -> {
for (SatelCommand command : getRefreshCommands(event)) {
bridgeHandler.sendCommand(command, true);
}
});
}
@Override
public void incomingEvent(IntegraStateEvent event) {
logger.trace("Handling incoming event: {}", event);
// update thing's state unless it should accept commands only
if (getThingConfig().isCommandOnly()) {
return;
}
for (Channel channel : getThing().getChannels()) {
ChannelUID channelUID = channel.getUID();
if (isLinked(channel.getUID())) {
StateType stateType = getStateType(channelUID.getId());
if (stateType != StateType.NONE && event.hasDataForState(stateType)) {
int bitNbr = getStateBitNbr(stateType);
boolean invertState = getThingConfig().isStateInverted();
updateSwitch(channelUID, event.isSet(stateType, bitNbr) ^ invertState);
}
}
}
}
/**
* Returns bit number of given state type for this thing.
* This number addresses the bit in bit set sent to or received from alarm system.
* Usually this number is the device identifier.
*
* @param stateType state type
* @return bit number in state bit set
*/
protected int getStateBitNbr(StateType stateType) {
return getThingConfig().getId() - 1;
}
/**
* Converts openHAB command sent to a channel into Satel message.
*
* @param channel channel the command was sent to
* @param command sent command
* @return Satel message that reflects sent command
*/
protected abstract Optional<SatelCommand> convertCommand(ChannelUID channel, Command command);
/**
* Derived handlers must return appropriate state type for channels they support.
* If given channel is not supported by a handler, it should return {@linkplain StateType#NONE}.
*
* @param channelId channel identifier to get state type for
* @return object that represents state type
* @see #getChannel(StateType)
*/
protected abstract StateType getStateType(String channelId);
/**
* Returns channel for given state type. Usually channels have the same ID as state type name they represent.
*
* @param stateType state type to get channel for
* @return channel object
* @see #getStateType(String)
*/
protected @Nullable Channel getChannel(StateType stateType) {
final String channelId = stateType.toString().toLowerCase();
final Channel channel = getThing().getChannel(channelId);
if (channel == null) {
logger.debug("Missing channel for {}", stateType);
}
return channel;
}
/**
* Returns list of commands required to update thing state basing on event describing changes since last refresh.
*
* @param event list of state changes since last refresh
* @return collection of {@link IntegraStateCommand}
*/
protected Collection<SatelCommand> getRefreshCommands(NewStatesEvent event) {
final boolean hasExtPayload = getBridgeHandler().getIntegraType().hasExtPayload();
final Collection<SatelCommand> result = new LinkedList<>();
final boolean forceRefresh = requiresRefresh();
for (Channel channel : getThing().getChannels()) {
StateType stateType = getStateType(channel.getUID().getId());
if (stateType != StateType.NONE && isLinked(channel.getUID())) {
if (forceRefresh || event.isNew(stateType.getRefreshCommand())) {
result.add(new IntegraStateCommand(stateType, hasExtPayload));
}
}
}
return result;
}
/**
* Checks if this thing requires unconditional refresh of all its channels.
* Clears the flag afterwards.
*
* @return if <code>true</code> this thing requires full refresh
*/
protected boolean requiresRefresh() {
return requiresRefresh.getAndSet(false);
}
}

View File

@@ -0,0 +1,146 @@
/**
* 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.satel.internal.handler;
import static org.openhab.binding.satel.internal.SatelBindingConstants.*;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.command.ClearTroublesCommand;
import org.openhab.binding.satel.internal.command.IntegraStatusCommand;
import org.openhab.binding.satel.internal.command.SatelCommand;
import org.openhab.binding.satel.internal.command.SetClockCommand;
import org.openhab.binding.satel.internal.event.IntegraStatusEvent;
import org.openhab.binding.satel.internal.event.NewStatesEvent;
import org.openhab.binding.satel.internal.types.StateType;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SatelSystemHandler} is responsible for handling commands, which are
* sent to one of the system channels.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class SatelSystemHandler extends SatelStateThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_SYSTEM);
private static final Set<String> STATUS_CHANNELS = Stream
.of(CHANNEL_DATE_TIME, CHANNEL_SERVICE_MODE, CHANNEL_TROUBLES, CHANNEL_TROUBLES_MEMORY,
CHANNEL_ACU100_PRESENT, CHANNEL_INTRX_PRESENT, CHANNEL_GRADE23_SET)
.collect(Collectors.toSet());
private final Logger logger = LoggerFactory.getLogger(SatelSystemHandler.class);
public SatelSystemHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (CHANNEL_USER_CODE.equals(channelUID.getId()) && command instanceof StringType) {
withBridgeHandlerPresent(bridgeHandler -> {
bridgeHandler.setUserCode(command.toFullString());
});
} else {
super.handleCommand(channelUID, command);
}
}
@Override
public void incomingEvent(IntegraStatusEvent event) {
logger.trace("Handling incoming event: {}", event);
if (getThingConfig().isCommandOnly()) {
return;
}
updateState(CHANNEL_DATE_TIME,
event.getIntegraTime().map(dt -> (State) new DateTimeType(dt.atZone(getBridgeHandler().getZoneId())))
.orElse(UnDefType.UNDEF));
updateSwitch(CHANNEL_SERVICE_MODE, event.inServiceMode());
updateSwitch(CHANNEL_TROUBLES, event.troublesPresent());
updateSwitch(CHANNEL_TROUBLES_MEMORY, event.troublesMemory());
updateSwitch(CHANNEL_ACU100_PRESENT, event.isAcu100Present());
updateSwitch(CHANNEL_INTRX_PRESENT, event.isIntRxPresent());
updateSwitch(CHANNEL_GRADE23_SET, event.isGrade23Set());
}
@Override
protected StateType getStateType(String channelId) {
return StateType.NONE;
}
@Override
protected Optional<SatelCommand> convertCommand(ChannelUID channel, Command command) {
final SatelBridgeHandler bridgeHandler = getBridgeHandler();
switch (channel.getId()) {
case CHANNEL_TROUBLES:
case CHANNEL_TROUBLES_MEMORY:
if (command == OnOffType.ON) {
return Optional.empty();
} else {
return Optional.of(new ClearTroublesCommand(bridgeHandler.getUserCode()));
}
case CHANNEL_DATE_TIME:
DateTimeType dateTime = null;
if (command instanceof StringType) {
dateTime = DateTimeType.valueOf(command.toString());
} else if (command instanceof DateTimeType) {
dateTime = (DateTimeType) command;
}
if (dateTime != null) {
return Optional.of(new SetClockCommand(dateTime.getZonedDateTime()
.withZoneSameInstant(bridgeHandler.getZoneId()).toLocalDateTime(),
bridgeHandler.getUserCode()));
}
break;
default:
// do nothing for other types of status
break;
}
return Optional.empty();
}
@Override
protected Collection<SatelCommand> getRefreshCommands(NewStatesEvent event) {
Collection<SatelCommand> result = new LinkedList<>();
boolean anyStatusChannelLinked = getThing().getChannels().stream()
.filter(channel -> STATUS_CHANNELS.contains(channel.getUID().getId()))
.anyMatch(channel -> isLinked(channel.getUID().getId()));
boolean needRefresh = anyStatusChannelLinked
&& (requiresRefresh() || isLinked(CHANNEL_DATE_TIME) || event.isNew(IntegraStatusCommand.COMMAND_CODE));
if (needRefresh) {
result.add(new IntegraStatusCommand());
}
return result;
}
}

View File

@@ -0,0 +1,130 @@
/**
* 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.satel.internal.handler;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.satel.internal.config.SatelThingConfig;
import org.openhab.binding.satel.internal.event.SatelEventListener;
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.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.State;
/**
* The {@link SatelThingHandler} is base thing handler class for all non-bridge things.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public abstract class SatelThingHandler extends BaseThingHandler implements SatelEventListener {
private @Nullable SatelThingConfig thingConfig;
private @Nullable SatelBridgeHandler bridgeHandler;
public SatelThingHandler(Thing thing) {
super(thing);
}
@Override
public void dispose() {
final SatelBridgeHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler != null) {
bridgeHandler.removeEventListener(this);
}
}
@Override
public void initialize() {
thingConfig = getConfig().as(SatelThingConfig.class);
final Bridge bridge = getBridge();
if (bridge != null) {
final ThingHandler handler = bridge.getHandler();
if (handler != null && handler instanceof SatelBridgeHandler) {
this.bridgeHandler = (SatelBridgeHandler) handler;
this.bridgeHandler.addEventListener(this);
}
if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
}
}
protected SatelThingConfig getThingConfig() {
final SatelThingConfig thingConfig = this.thingConfig;
if (thingConfig != null) {
return thingConfig;
}
throw new IllegalStateException("Thing handler is not initialized yet for thing " + getThing().getUID());
}
protected void withBridgeHandlerPresent(Consumer<SatelBridgeHandler> action) {
final SatelBridgeHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler != null) {
action.accept(bridgeHandler);
}
}
protected SatelBridgeHandler getBridgeHandler() {
final SatelBridgeHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler != null) {
return bridgeHandler;
}
throw new IllegalStateException("Bridge handler is not set for thing " + getThing().getUID());
}
/**
* Updates switch channel with given state.
*
* @param channelID channel ID
* @param switchOn if <code>true</code> the channel is updated with ON state, with OFF state otherwise
*/
protected void updateSwitch(String channelID, boolean switchOn) {
ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelID);
updateSwitch(channelUID, switchOn);
}
/**
* Updates switch channel with given state.
*
* @param channelUID channel UID
* @param switchOn if <code>true</code> the channel is updated with ON state, with OFF state otherwise
*/
protected void updateSwitch(ChannelUID channelUID, boolean switchOn) {
State state = switchOn ? OnOffType.ON : OnOffType.OFF;
updateState(channelUID, state);
}
/**
* Creates bitset of given size with particular bits set to 1.
*
* @param size bitset size in bytes
* @param ids bits to set, first bit is 1
* @return bitset as array of bytes
*/
protected byte[] getObjectBitset(int size, int... ids) {
byte[] bitset = new byte[size];
for (int id : ids) {
int bitNbr = id - 1;
bitset[bitNbr / 8] |= (byte) (1 << (bitNbr % 8));
}
return bitset;
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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.satel.internal.handler;
import static org.openhab.binding.satel.internal.SatelBindingConstants.THING_TYPE_ZONE;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.command.ControlObjectCommand;
import org.openhab.binding.satel.internal.command.SatelCommand;
import org.openhab.binding.satel.internal.types.StateType;
import org.openhab.binding.satel.internal.types.ZoneControl;
import org.openhab.binding.satel.internal.types.ZoneState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
/**
* The {@link SatelZoneHandler} is responsible for handling commands, which are
* sent to one of the channels of a zone.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class SatelZoneHandler extends WirelessChannelsHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_ZONE);
public SatelZoneHandler(Thing thing) {
super(thing);
}
@Override
protected Optional<SatelCommand> convertCommand(ChannelUID channel, Command command) {
if (command instanceof OnOffType) {
final SatelBridgeHandler bridgeHandler = getBridgeHandler();
boolean switchOn = (command == OnOffType.ON);
StateType stateType = getStateType(channel.getId());
int size = bridgeHandler.getIntegraType().hasExtPayload() ? 32 : 16;
byte[] zones = getObjectBitset(size, getThingConfig().getId());
ZoneControl action = null;
switch ((ZoneState) stateType) {
case BYPASS:
action = switchOn ? ZoneControl.BYPASS : ZoneControl.UNBYPASS;
break;
case ISOLATE:
action = switchOn ? ZoneControl.ISOLATE : null;
break;
default:
// do nothing for other types of state
break;
}
if (action != null) {
return Optional.of(new ControlObjectCommand(action, zones, bridgeHandler.getUserCode(), scheduler));
}
}
return Optional.empty();
}
@Override
protected StateType getStateType(String channelId) {
StateType result = super.getStateType(channelId);
if (result == StateType.NONE) {
result = ZoneState.valueOf(channelId.toUpperCase());
}
return result;
}
}

View File

@@ -0,0 +1,128 @@
/**
* 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.satel.internal.handler;
import static org.openhab.binding.satel.internal.SatelBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.satel.internal.types.StateType;
import org.openhab.binding.satel.internal.types.TroubleState;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
/**
* The {@link WirelessChannelsHandler} is base thing handler class for things that can be wireless devices.
* It adds support for two additional channels:
* <ul>
* <li><i>device_lobatt</i> - low battery indication</li>
* <li><i>device_nocomm</i> - communication problems indication</li>
* </ul>
* adding them if the device is configured as a wireless device.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public abstract class WirelessChannelsHandler extends SatelStateThingHandler {
public WirelessChannelsHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
withBridgeHandlerPresent(bridgeHandler -> {
// add/remove channels depending on whether or not the device is wireless
final int wirelessDeviceId = getThingConfig().getId() - bridgeHandler.getIntegraType().getOnMainboard();
ThingBuilder thingBuilder = editThing();
if (isWirelessDevice() && wirelessDeviceId > 0) {
// if a wireless device, remove channels for wireless devices
if (getChannel(TroubleState.DEVICE_LOBATT) == null) {
thingBuilder.withChannel(ChannelBuilder.create(getChannelUID(TroubleState.DEVICE_LOBATT), "Switch")
.withType(CHANNEL_TYPE_LOBATT).build());
}
if (getChannel(TroubleState.DEVICE_NOCOMM) == null) {
thingBuilder.withChannel(ChannelBuilder.create(getChannelUID(TroubleState.DEVICE_NOCOMM), "Switch")
.withType(CHANNEL_TYPE_NOCOMM).build());
}
} else {
// if not a wireless device, remove channels for wireless devices
thingBuilder.withoutChannel(getChannelUID(TroubleState.DEVICE_LOBATT))
.withoutChannel(getChannelUID(TroubleState.DEVICE_NOCOMM));
}
updateThing(thingBuilder.build());
});
}
/**
* Defines the thing as a wireless or wired device.
*
* @return <code>true</code> if the thing is, or is configured as a wireless device
*/
protected boolean isWirelessDevice() {
return getThingConfig().isWireless();
}
@Override
protected int getStateBitNbr(StateType stateType) {
int bitNbr = getThingConfig().getId() - 1;
if (stateType instanceof TroubleState) {
// for wireless devices we need to correct bit number
switch ((TroubleState) stateType) {
case DEVICE_LOBATT1:
case DEVICE_NOCOMM1:
case OUTPUT_NOCOMM1:
bitNbr -= 120;
// pass through
case DEVICE_LOBATT:
case DEVICE_NOCOMM:
case OUTPUT_NOCOMM:
bitNbr -= getBridgeHandler().getIntegraType().getOnMainboard();
break;
default:
// other states are either not supported or don't need correction
break;
}
}
return bitNbr;
}
@Override
protected StateType getStateType(String channelId) {
String stateName = channelId.toUpperCase();
if (TroubleState.DEVICE_LOBATT.name().equals(stateName)
|| TroubleState.DEVICE_NOCOMM.name().equals(stateName)) {
if (getThingConfig().getId() - getBridgeHandler().getIntegraType().getOnMainboard() > 120) {
// last 120 ACU-100 devices in INTEGRA 256 PLUS
stateName += "1";
}
return TroubleState.valueOf(stateName);
} else {
return StateType.NONE;
}
}
/**
* Returns channel UID for given state type.
*
* @param stateType state type to get channel UID for
* @return channel UID object
*/
private ChannelUID getChannelUID(StateType stateType) {
String channelId = stateType.toString().toLowerCase();
return new ChannelUID(getThing().getUID(), channelId);
}
}

View File

@@ -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.satel.internal.protocol;
import java.security.GeneralSecurityException;
import java.security.Key;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Helper class for encrypting ETHM-1 messages.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class EncryptionHelper {
private Key key;
private Cipher encipher;
private Cipher decipher;
/**
* Creates new instance of encryption helper with given key.
*
* @param keyString key for integration encryption
* @throws GeneralSecurityException on JCE errors
*/
public EncryptionHelper(String keyString) throws GeneralSecurityException {
// we have to check if 192bit support is enabled
if (Cipher.getMaxAllowedKeyLength("AES") < 192) {
throw new GeneralSecurityException("JCE does not support 192-bit keys");
}
// build encryption/decryption key based on given password
byte passwordBytes[] = keyString.getBytes();
byte[] keyBytes = new byte[24];
for (int i = 0; i < 12; ++i) {
keyBytes[i] = keyBytes[i + 12] = (i < passwordBytes.length) ? passwordBytes[i] : 0x20;
}
// create objects for encryption/decryption
this.key = new SecretKeySpec(keyBytes, "AES");
this.encipher = Cipher.getInstance("AES/ECB/NoPadding");
this.encipher.init(Cipher.ENCRYPT_MODE, this.key);
this.decipher = Cipher.getInstance("AES/ECB/NoPadding");
this.decipher.init(Cipher.DECRYPT_MODE, this.key);
}
/**
* Decrypts given buffer of bytes in place.
*
* @param buffer
* bytes to decrypt
* @throws GeneralSecurityException
* on decryption errors
*/
public void decrypt(byte buffer[]) throws GeneralSecurityException {
byte[] cv = new byte[16];
byte[] c = new byte[16];
byte[] temp = new byte[16];
int count = buffer.length;
cv = this.encipher.doFinal(cv);
for (int index = 0; count > 0;) {
if (count > 15) {
count -= 16;
System.arraycopy(buffer, index, temp, 0, 16);
System.arraycopy(buffer, index, c, 0, 16);
c = this.decipher.doFinal(c);
for (int i = 0; i < 16; ++i) {
c[i] ^= cv[i];
cv[i] = temp[i];
}
System.arraycopy(c, 0, buffer, index, 16);
index += 16;
} else {
System.arraycopy(buffer, index, c, 0, count);
cv = this.encipher.doFinal(cv);
for (int i = 0; i < 16; ++i) {
c[i] ^= cv[i];
}
System.arraycopy(c, 0, buffer, index, count);
count = 0;
}
}
}
/**
* Encrypts given buffer of bytes in place.
*
* @param buffer bytes to encrypt
* @throws GeneralSecurityException on encryption errors
*/
public void encrypt(byte buffer[]) throws GeneralSecurityException {
byte[] cv = new byte[16];
byte[] p = new byte[16];
int count = buffer.length;
cv = this.encipher.doFinal(cv);
for (int index = 0; count > 0;) {
if (count > 15) {
count -= 16;
System.arraycopy(buffer, index, p, 0, 16);
for (int i = 0; i < 16; ++i) {
p[i] ^= cv[i];
}
p = this.encipher.doFinal(p);
System.arraycopy(p, 0, cv, 0, 16);
System.arraycopy(p, 0, buffer, index, 16);
index += 16;
} else {
System.arraycopy(buffer, index, p, 0, count);
cv = this.encipher.doFinal(cv);
for (int i = 0; i < 16; ++i) {
p[i] ^= cv[i];
}
System.arraycopy(p, 0, buffer, index, count);
count = 0;
}
}
}
}

View File

@@ -0,0 +1,259 @@
/**
* 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.satel.internal.protocol;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Random;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents Satel ETHM-1 module. Implements method required to connect and
* communicate with that module over TCP/IP protocol. The module must have
* integration protocol enable in DLOADX configuration options.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class Ethm1Module extends SatelModule {
private static final ByteArrayInputStream EMPTY_INPUT_STREAM = new ByteArrayInputStream(new byte[0]);
private final Logger logger = LoggerFactory.getLogger(Ethm1Module.class);
private final String host;
private final int port;
private final String encryptionKey;
/**
* Creates new instance with host, port, timeout and encryption key set to
* specified values.
*
* @param host host name or IP of ETHM-1 module
* @param port TCP port the module listens on
* @param timeout timeout value in milliseconds for connect/read/write operations
* @param encryptionKey encryption key for encrypted communication
* @param extPayloadSupport if <code>true</code>, the module supports extended command payload for reading
* INTEGRA 256 state
*/
public Ethm1Module(String host, int port, int timeout, String encryptionKey, boolean extPayloadSupport) {
super(timeout, extPayloadSupport);
this.host = host;
this.port = port;
this.encryptionKey = encryptionKey;
}
@Override
protected CommunicationChannel connect() throws ConnectionFailureException {
logger.info("Connecting to ETHM-1 module at {}:{}", this.host, this.port);
try {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(this.host, this.port), this.getTimeout());
logger.info("ETHM-1 module connected successfully");
if (StringUtils.isBlank(this.encryptionKey)) {
return new TCPCommunicationChannel(socket);
} else {
return new EncryptedCommunicationChannel(socket, this.encryptionKey);
}
} catch (SocketTimeoutException e) {
throw new ConnectionFailureException("Connection timeout", e);
} catch (IOException e) {
throw new ConnectionFailureException("IO error occurred while connecting socket", e);
}
}
private class TCPCommunicationChannel implements CommunicationChannel {
private Socket socket;
public TCPCommunicationChannel(Socket socket) {
this.socket = socket;
}
@Override
public InputStream getInputStream() throws IOException {
return this.socket.getInputStream();
}
@Override
public OutputStream getOutputStream() throws IOException {
return this.socket.getOutputStream();
}
@Override
public void disconnect() {
logger.info("Closing connection to ETHM-1 module");
try {
this.socket.close();
} catch (IOException e) {
logger.error("IO error occurred during closing socket", e);
}
}
}
private class EncryptedCommunicationChannel extends TCPCommunicationChannel {
private EncryptionHelper aesHelper;
private Random rand;
private byte idS;
private byte idR;
private int rollingCounter;
private InputStream inputStream;
private OutputStream outputStream;
public EncryptedCommunicationChannel(final Socket socket, String encryptionKey) throws IOException {
super(socket);
try {
this.aesHelper = new EncryptionHelper(encryptionKey);
} catch (Exception e) {
throw new IOException("General encryption failure", e);
}
this.rand = new Random();
this.idS = 0;
this.idR = 0;
this.rollingCounter = 0;
this.inputStream = new InputStream() {
private ByteArrayInputStream inputBuffer = EMPTY_INPUT_STREAM;
@Override
public int read() throws IOException {
if (inputBuffer.available() == 0) {
// read message and decrypt it
byte[] data = readMessage(socket.getInputStream());
// create new buffer
inputBuffer = new ByteArrayInputStream(data, 6, data.length - 6);
}
return inputBuffer.read();
}
};
this.outputStream = new OutputStream() {
private ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(256);
@Override
public void write(int b) throws IOException {
outputBuffer.write(b);
}
@Override
public void flush() throws IOException {
writeMessage(outputBuffer.toByteArray(), socket.getOutputStream());
outputBuffer.reset();
}
};
}
@Override
public InputStream getInputStream() throws IOException {
return this.inputStream;
}
@Override
public OutputStream getOutputStream() throws IOException {
return this.outputStream;
}
private synchronized byte[] readMessage(InputStream is) throws IOException {
logger.trace("Receiving data from ETHM-1");
// read number of bytes
int bytesCount = is.read();
logger.trace("Read count of bytes: {}", bytesCount);
if (bytesCount == -1) {
throw new IOException("End of input stream reached");
}
byte[] data = new byte[bytesCount];
// read encrypted data
int bytesRead = is.read(data);
if (bytesCount != bytesRead) {
throw new IOException(
String.format("Too few bytes read. Read: %d, expected: %d", bytesRead, bytesCount));
}
// decrypt data
if (logger.isTraceEnabled()) {
logger.trace("Decrypting data: {}", HexUtils.bytesToHex(data));
}
try {
this.aesHelper.decrypt(data);
} catch (Exception e) {
throw new IOException("Decryption exception", e);
}
if (logger.isDebugEnabled()) {
logger.debug("Decrypted data: {}", HexUtils.bytesToHex(data));
}
// validate message
this.idR = data[4];
if (this.idS != data[5]) {
throw new IOException(String.format("Invalid 'idS' value. Got: %d, expected: %d", data[5], this.idS));
}
return data;
}
private synchronized void writeMessage(byte[] message, OutputStream os) throws IOException {
// prepare data for encryption
int bytesCount = 6 + message.length;
if (bytesCount < 16) {
bytesCount = 16;
}
byte[] data = new byte[bytesCount];
int randomValue = this.rand.nextInt();
data[0] = (byte) (randomValue >> 8);
data[1] = (byte) (randomValue & 0xff);
data[2] = (byte) (this.rollingCounter >> 8);
data[3] = (byte) (this.rollingCounter & 0xff);
data[4] = this.idS = (byte) this.rand.nextInt();
data[5] = this.idR;
++this.rollingCounter;
System.arraycopy(message, 0, data, 6, message.length);
// encrypt data
if (logger.isDebugEnabled()) {
logger.debug("Encrypting data: {}", HexUtils.bytesToHex(data));
}
try {
this.aesHelper.encrypt(data);
} catch (Exception e) {
throw new IOException("Encryption exception", e);
}
if (logger.isTraceEnabled()) {
logger.trace("Encrypted data: {}", HexUtils.bytesToHex(data));
}
// write encrypted data to output stream
os.write(bytesCount);
os.write(data);
os.flush();
}
}
}

View File

@@ -0,0 +1,137 @@
/**
* 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.satel.internal.protocol;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.TooManyListenersException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortEvent;
import org.openhab.core.io.transport.serial.SerialPortEventListener;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents Satel INT-RS module. Implements methods required to connect and
* communicate with that module over serial protocol.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class IntRSModule extends SatelModule {
private final Logger logger = LoggerFactory.getLogger(IntRSModule.class);
private final String port;
private final SerialPortManager serialPortManager;
/**
* Creates new instance with port and timeout set to specified values.
*
* @param port serial port the module is connected to
* @param serialPortManager serial port manager object
* @param timeout timeout value in milliseconds for connect/read/write operations
* @param extPayloadSupport if <code>true</code>, the module supports extended command payload for reading
* INTEGRA 256 state
*/
public IntRSModule(String port, SerialPortManager serialPortManager, int timeout, boolean extPayloadSupport) {
super(timeout, extPayloadSupport);
this.port = port;
this.serialPortManager = serialPortManager;
}
@Override
protected CommunicationChannel connect() throws ConnectionFailureException {
logger.info("Connecting to INT-RS module at {}", this.port);
try {
SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(this.port);
if (portIdentifier == null) {
throw new ConnectionFailureException(String.format("Port %s does not exist", this.port));
}
SerialPort serialPort = portIdentifier.open("org.openhab.binding.satel", 2000);
serialPort.setSerialPortParams(19200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
serialPort.enableReceiveTimeout(this.getTimeout());
// RXTX serial port library causes high CPU load
// Start event listener, which will just sleep and slow down event
// loop
serialPort.addEventListener(new SerialPortEventListener() {
@Override
public void serialEvent(SerialPortEvent ev) {
try {
logger.trace("RXTX library CPU load workaround, sleep forever");
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
}
}
});
serialPort.notifyOnDataAvailable(true);
logger.info("INT-RS module connected successfuly");
return new SerialCommunicationChannel(serialPort);
} catch (PortInUseException e) {
throw new ConnectionFailureException(String.format("Port %s in use", this.port), e);
} catch (UnsupportedCommOperationException e) {
throw new ConnectionFailureException(String.format("Unsupported comm operation on port %s", this.port), e);
} catch (TooManyListenersException e) {
throw new ConnectionFailureException(String.format("Too many listeners on port %s", this.port), e);
}
}
private class SerialCommunicationChannel implements CommunicationChannel {
private SerialPort serialPort;
public SerialCommunicationChannel(SerialPort serialPort) {
this.serialPort = serialPort;
}
@Override
public InputStream getInputStream() throws IOException {
final InputStream stream = this.serialPort.getInputStream();
if (stream != null) {
return stream;
}
throw new IOException("Selected port doesn't support receiving data: " + this.serialPort.getName());
}
@Override
public OutputStream getOutputStream() throws IOException {
final OutputStream stream = this.serialPort.getOutputStream();
if (stream != null) {
return stream;
}
throw new IOException("Selected port doesn't support sending data: " + this.serialPort.getName());
}
@Override
public void disconnect() {
logger.debug("Closing connection to INT-RS module");
try {
this.serialPort.close();
logger.info("Connection to INT-RS module has been closed");
} catch (Exception e) {
logger.error("An error occurred during closing serial port", e);
}
}
}
}

View File

@@ -0,0 +1,196 @@
/**
* 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.satel.internal.protocol;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents a message sent to (a command) or received from (a response)
* {@link SatelModule}. It consists of one byte that specifies command type or
* response status and certain number of payload bytes. Number of payload byte
* depends on command type. The class allows to serialize a command to bytes and
* deserialize a response from given bytes. It also computes and validates
* message checksum.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public class SatelMessage {
private static final Logger LOGGER = LoggerFactory.getLogger(SatelMessage.class);
private byte command;
private byte[] payload;
protected static final byte[] EMPTY_PAYLOAD = new byte[0];
/**
* Creates new instance with specified command code and payload.
*
* @param command
* command code
* @param payload
* command payload
*/
public SatelMessage(byte command, byte[] payload) {
this.command = command;
this.payload = payload;
}
/**
* Creates new instance with specified command code and empty payload.
*
* @param command command code
*/
public SatelMessage(byte command) {
this(command, EMPTY_PAYLOAD);
}
/**
* Deserializes new message instance from specified byte buffer.
*
* @param buffer bytes to deserialize a message from
* @return deserialized message instance
*/
public static @Nullable SatelMessage fromBytes(byte[] buffer) {
// we need at least command and checksum
if (buffer.length < 3) {
LOGGER.error("Invalid message length: {}", buffer.length);
return null;
}
// check crc
int receivedCrc = 0xffff & ((buffer[buffer.length - 2] << 8) | (buffer[buffer.length - 1] & 0xff));
int expectedCrc = calculateChecksum(buffer, buffer.length - 2);
if (receivedCrc != expectedCrc) {
LOGGER.error("Invalid message checksum: received = {}, expected = {}", receivedCrc, expectedCrc);
return null;
}
SatelMessage message = new SatelMessage(buffer[0], new byte[buffer.length - 3]);
if (message.payload.length > 0) {
System.arraycopy(buffer, 1, message.payload, 0, buffer.length - 3);
}
return message;
}
/**
* Returns command byte.
*
* @return the command
*/
public byte getCommand() {
return this.command;
}
/**
* Returns the payload bytes.
*
* @return payload as byte array
*/
public byte[] getPayload() {
return this.payload;
}
/**
* Returns the message serialized as array of bytes with checksum calculated
* at last two bytes.
*
* @return the message as array of bytes
*/
public byte[] getBytes() {
byte buffer[] = new byte[this.payload.length + 3];
buffer[0] = this.command;
if (this.payload.length > 0) {
System.arraycopy(this.payload, 0, buffer, 1, this.payload.length);
}
int checksum = calculateChecksum(buffer, buffer.length - 2);
buffer[buffer.length - 2] = (byte) ((checksum >> 8) & 0xff);
buffer[buffer.length - 1] = (byte) (checksum & 0xff);
return buffer;
}
private String getPayloadAsHex() {
StringBuilder result = new StringBuilder();
for (int i = 0; i < this.payload.length; ++i) {
if (i > 0) {
result.append(" ");
}
result.append(String.format("%02X", this.payload[i]));
}
return result.toString();
}
/**
* Calculates a checksum for the specified buffer.
*
* @param buffer the buffer to calculate.
* @return the checksum value.
*/
private static int calculateChecksum(byte[] buffer, int length) {
int checkSum = 0x147a;
for (int i = 0; i < length; i++) {
checkSum = ((checkSum << 1) | ((checkSum >> 15) & 1));
checkSum ^= 0xffff;
checkSum += ((checkSum >> 8) & 0xff) + (buffer[i] & 0xff);
}
checkSum &= 0xffff;
LOGGER.trace("Calculated checksum = {}", String.format("%04X", checkSum));
return checkSum;
}
@Override
public String toString() {
return String.format("Message: command = %02X, payload = %s", this.command, getPayloadAsHex());
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!obj.getClass().equals(this.getClass())) {
return false;
}
SatelMessage other = (SatelMessage) obj;
if (other.command != this.command) {
return false;
}
if (!Arrays.equals(other.payload, this.payload)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + command;
result = prime * result + Arrays.hashCode(payload);
return result;
}
}

View File

@@ -0,0 +1,551 @@
/**
* 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.satel.internal.protocol;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.satel.internal.command.IntegraVersionCommand;
import org.openhab.binding.satel.internal.command.SatelCommand;
import org.openhab.binding.satel.internal.command.SatelCommand.State;
import org.openhab.binding.satel.internal.event.ConnectionStatusEvent;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.event.IntegraVersionEvent;
import org.openhab.binding.satel.internal.event.SatelEventListener;
import org.openhab.binding.satel.internal.types.IntegraType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class represents abstract communication module and is responsible for
* exchanging data between the binding and connected physical module.
* Communication happens by sending commands and receiving response from the
* module. Each command class must extend {@link SatelCommand}.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public abstract class SatelModule extends EventDispatcher implements SatelEventListener {
private final Logger logger = LoggerFactory.getLogger(SatelModule.class);
private static final byte FRAME_SYNC = (byte) 0xfe;
private static final byte FRAME_SYNC_ESC = (byte) 0xf0;
private static final byte[] FRAME_START = { FRAME_SYNC, FRAME_SYNC };
private static final byte[] FRAME_END = { FRAME_SYNC, (byte) 0x0d };
private final BlockingQueue<SatelCommand> sendQueue = new LinkedBlockingQueue<>();
private final int timeout;
private volatile IntegraType integraType;
private volatile String integraVersion;
private final boolean extPayloadSupport;
private @Nullable CommunicationChannel channel;
private @Nullable CommunicationWatchdog communicationWatchdog;
/*
* Helper interface for connecting and disconnecting to specific module
* type. Each module type should implement these methods to provide input
* and output streams and way to disconnect from the module.
*/
protected interface CommunicationChannel {
InputStream getInputStream() throws IOException;
OutputStream getOutputStream() throws IOException;
void disconnect();
}
/*
* Helper interface to handle communication timeouts.
*/
protected interface TimeoutTimer {
void start();
void stop();
}
/*
* Thrown on connection failures.
*/
protected static class ConnectionFailureException extends Exception {
private static final long serialVersionUID = 2L;
public ConnectionFailureException(String message) {
super(message);
}
public ConnectionFailureException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Creates new instance of the class.
*
* @param timeout timeout value in milliseconds for connect/read/write operations
* @param extPayloadSupport if <code>true</code>, the module supports extended command payload for reading
* INTEGRA256 state
*/
public SatelModule(int timeout, boolean extPayloadSupport) {
this.timeout = timeout;
this.integraType = IntegraType.UNKNOWN;
this.integraVersion = "";
this.extPayloadSupport = extPayloadSupport;
addEventListener(this);
}
/**
* Returns type of Integra connected to the module.
*
* @return Integra type
*/
public IntegraType getIntegraType() {
return this.integraType;
}
/**
* Returns firmware revision of Integra connected to the module.
*
* @return version of Integra firmware
*/
public String getIntegraVersion() {
return this.integraVersion;
}
/**
* Returns configured timeout value.
*
* @return timeout value as milliseconds
*/
public int getTimeout() {
return this.timeout;
}
public boolean isConnected() {
return this.channel != null;
}
/**
* Returns status of initialization.
*
* @return <code>true</code> if module is properly initialized and ready for sending commands
*/
public boolean isInitialized() {
return this.integraType != IntegraType.UNKNOWN;
}
/**
* Returns extended payload flag.
*
* @return <code>true</code> if the module supports extended (32-bit) payload for zones/outputs
*/
public boolean hasExtPayloadSupport() {
return this.extPayloadSupport;
}
protected abstract CommunicationChannel connect() throws ConnectionFailureException;
/**
* Starts communication.
*/
public synchronized void open() {
if (this.communicationWatchdog == null) {
this.communicationWatchdog = new CommunicationWatchdog();
} else {
logger.warn("Module is already opened.");
}
}
/**
* Stops communication by disconnecting from the module and stopping all background tasks.
*/
public void close() {
// first we clear watchdog field in the object
CommunicationWatchdog watchdog = null;
synchronized (this) {
if (this.communicationWatchdog != null) {
watchdog = this.communicationWatchdog;
this.communicationWatchdog = null;
}
}
// then, if watchdog exists, we close it
if (watchdog != null) {
watchdog.close();
}
}
/**
* Enqueues specified command in send queue if not already enqueued.
*
* @param cmd command to enqueue
* @return <code>true</code> if operation succeeded
*/
public boolean sendCommand(SatelCommand cmd) {
return this.sendCommand(cmd, false);
}
/**
* Enqueues specified command in send queue.
*
* @param cmd command to enqueue
* @param force if <code>true</code> enqueues unconditionally
* @return <code>true</code> if operation succeeded
*/
public boolean sendCommand(SatelCommand cmd, boolean force) {
try {
if (force || !this.sendQueue.contains(cmd)) {
this.sendQueue.put(cmd);
cmd.setState(State.ENQUEUED);
logger.trace("Command enqueued: {}", cmd);
} else {
logger.debug("Command already in the queue: {}", cmd);
}
return true;
} catch (InterruptedException e) {
return false;
}
}
@Override
public void incomingEvent(IntegraVersionEvent event) {
IntegraVersionEvent versionEvent = event;
this.integraType = IntegraType.valueOf(versionEvent.getType() & 0xFF);
this.integraVersion = versionEvent.getVersion();
logger.info("Connection to {} initialized. INTEGRA version: {}.", this.integraType.getName(),
this.integraVersion);
}
private @Nullable SatelMessage readMessage() throws InterruptedException {
final CommunicationChannel channel = this.channel;
if (channel == null) {
logger.error("Reading attempt on closed channel.");
return null;
}
try {
final InputStream is = channel.getInputStream();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
boolean inMessage = false;
int syncBytes = 0;
while (true) {
// if timed out, exit
int c = is.read();
if (c < 0) {
return null;
}
byte b = (byte) c;
if (b == FRAME_SYNC) {
if (inMessage) {
// syncBytes == 0 means special sequence or end of message
// otherwise we discard all received bytes
if (syncBytes != 0) {
logger.warn("Received frame sync bytes, discarding input: {}", baos.size());
// clear gathered bytes, we wait for new message
inMessage = false;
baos.reset();
}
}
++syncBytes;
} else {
if (inMessage) {
if (syncBytes == 0) {
// in sync, we have next message byte
baos.write(b);
} else if (syncBytes == 1) {
if (b == FRAME_SYNC_ESC) {
baos.write(FRAME_SYNC);
} else if (b == FRAME_END[1]) {
// end of message
break;
} else {
logger.warn("Received invalid byte {}, discarding input: {}", String.format("%02X", b),
baos.size());
// clear gathered bytes, we have new message
inMessage = false;
baos.reset();
}
} else {
logger.error("Sync bytes in message: {}", syncBytes);
}
} else if (syncBytes >= 2) {
// synced, we have first message byte
inMessage = true;
baos.write(b);
}
// otherwise we ignore all bytes until synced
syncBytes = 0;
}
// if meanwhile thread has been interrupted, exit the loop
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
// return read message
return SatelMessage.fromBytes(baos.toByteArray());
} catch (IOException e) {
if (!Thread.currentThread().isInterrupted()) {
logger.error("Unexpected exception occurred during reading a message", e);
}
}
return null;
}
private boolean writeMessage(SatelMessage message) {
final CommunicationChannel channel = this.channel;
if (channel == null) {
logger.error("Writing attempt on closed channel.");
return false;
}
try {
final OutputStream os = channel.getOutputStream();
os.write(FRAME_START);
for (byte b : message.getBytes()) {
os.write(b);
if (b == FRAME_SYNC) {
os.write(FRAME_SYNC_ESC);
}
}
os.write(FRAME_END);
os.flush();
return true;
} catch (IOException e) {
if (!Thread.currentThread().isInterrupted()) {
logger.error("Unexpected exception occurred during writing a message", e);
}
}
return false;
}
private synchronized void disconnect(@Nullable String reason) {
// remove all pending commands from the queue
// notifying about send failure
while (!this.sendQueue.isEmpty()) {
SatelCommand cmd = this.sendQueue.poll();
cmd.setState(State.FAILED);
}
final CommunicationChannel channel = this.channel;
if (channel != null) {
channel.disconnect();
this.channel = null;
// notify about connection status change
this.dispatchEvent(new ConnectionStatusEvent(false, reason));
}
}
private void communicationLoop(TimeoutTimer timeoutTimer) {
long reconnectionTime = 10 * 1000;
boolean receivedResponse = false;
SatelCommand command = null;
String disconnectReason = null;
try {
while (!Thread.currentThread().isInterrupted()) {
// connect, if not connected yet
if (this.channel == null) {
long connectStartTime = System.currentTimeMillis();
try {
synchronized (this) {
this.channel = connect();
}
} catch (ConnectionFailureException e) {
logger.debug("Connection failed", e);
// notify about connection failure
this.dispatchEvent(new ConnectionStatusEvent(false, e.getMessage()));
// try to reconnect after a while, if connection hasn't
// been established
Thread.sleep(reconnectionTime - System.currentTimeMillis() + connectStartTime);
continue;
}
}
// get next command and send it
command = this.sendQueue.take();
logger.debug("Sending message: {}", command.getRequest());
timeoutTimer.start();
boolean sent = this.writeMessage(command.getRequest());
timeoutTimer.stop();
if (!sent) {
break;
}
command.setState(State.SENT);
SatelMessage response;
do {
// command sent, wait for response
logger.trace("Waiting for response");
timeoutTimer.start();
response = this.readMessage();
timeoutTimer.stop();
if (response == null) {
break;
}
logger.debug("Got response: {}", response);
if (!receivedResponse) {
receivedResponse = true;
// notify about connection success after first
// response from the module
this.dispatchEvent(new ConnectionStatusEvent(true));
}
if (command.matches(response)) {
break;
}
logger.info("Ignoring response, it does not match command {}: {}",
String.format("%02X", command.getRequest().getCommand()), response);
} while (!Thread.currentThread().isInterrupted());
if (response == null) {
break;
}
if (command.handleResponse(this, response)) {
command.setState(State.SUCCEEDED);
} else {
command.setState(State.FAILED);
}
command = null;
}
} catch (InterruptedException e) {
// exit thread
} catch (Exception e) {
// unexpected error, log and exit thread
logger.info("Unhandled exception occurred in communication loop, disconnecting.", e);
disconnectReason = "Unhandled exception: " + e.toString();
} finally {
// stop counting if thread interrupted
timeoutTimer.stop();
}
// either send or receive failed
if (command != null) {
command.setState(State.FAILED);
}
disconnect(disconnectReason);
}
/*
* Respawns communication thread in case on any error and interrupts it in
* case read/write operations take too long.
*/
private class CommunicationWatchdog extends Timer implements TimeoutTimer {
private @Nullable Thread thread;
private volatile long lastActivity;
public CommunicationWatchdog() {
this.thread = null;
this.lastActivity = 0;
this.schedule(new TimerTask() {
@Override
public void run() {
CommunicationWatchdog.this.checkThread();
}
}, 0, 1000);
}
@Override
public void start() {
this.lastActivity = System.currentTimeMillis();
}
@Override
public void stop() {
this.lastActivity = 0;
}
public synchronized void close() {
// cancel timer first to prevent reconnect
this.cancel();
// then stop communication thread
final Thread thread = this.thread;
if (thread != null) {
thread.interrupt();
try {
thread.join();
} catch (InterruptedException e) {
// ignore
}
}
}
private void startCommunication() {
final Thread thread = this.thread;
if (thread != null && thread.isAlive()) {
logger.error("Start communication canceled: communication thread is still alive");
return;
}
// start new thread
this.thread = new Thread(new Runnable() {
@Override
public void run() {
logger.debug("Communication thread started");
SatelModule.this.communicationLoop(CommunicationWatchdog.this);
logger.debug("Communication thread stopped");
}
});
this.thread.start();
// if module is not initialized yet, send version command
if (!SatelModule.this.isInitialized()) {
SatelModule.this.sendCommand(new IntegraVersionCommand());
}
}
private void checkThread() {
final Thread thread = this.thread;
logger.trace("Checking communication thread: {}, {}", thread != null,
Boolean.toString(thread != null && thread.isAlive()));
if (thread != null && thread.isAlive()) {
long timePassed = (this.lastActivity == 0) ? 0 : System.currentTimeMillis() - this.lastActivity;
if (timePassed > SatelModule.this.timeout) {
logger.error("Send/receive timeout, disconnecting module.");
stop();
thread.interrupt();
try {
// wait for the thread to terminate
thread.join(100);
} catch (InterruptedException e) {
// ignore
}
SatelModule.this.disconnect("Send/receive timeout");
}
} else {
startCommunication();
}
}
}
}

View File

@@ -0,0 +1,47 @@
/**
* 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.satel.internal.types;
import java.util.BitSet;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface for all types of control.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public interface ControlType {
/**
* Returns Satel command to control state for this kind of object type.
*
* @return command identifier
*/
byte getControlCommand();
/**
* Returns object type for this kind of control.
*
* @return Integra object type
*/
ObjectType getObjectType();
/**
* Returns set of states that may change for this control type.
*
* @return command identifier
*/
BitSet getControlledStates();
}

View File

@@ -0,0 +1,47 @@
/**
* 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.satel.internal.types;
import java.util.BitSet;
/**
* Available door control types.
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum DoorControl implements ControlType {
OPEN(0x8A, DoorState.OPENED);
private byte controlCommand;
private BitSet stateBits;
DoorControl(int controlCommand, DoorState... controlledStates) {
this.controlCommand = (byte) controlCommand;
this.stateBits = StateType.getStatesBitSet(controlledStates);
}
@Override
public byte getControlCommand() {
return controlCommand;
}
@Override
public ObjectType getObjectType() {
return ObjectType.DOOR;
}
@Override
public BitSet getControlledStates() {
return stateBits;
}
}

View File

@@ -0,0 +1,54 @@
/**
* 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.satel.internal.types;
/**
* Available door states.
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum DoorState implements StateType {
OPENED(0x18),
OPENED_LONG(0x19);
private byte refreshCommand;
DoorState(int refreshCommand) {
this.refreshCommand = (byte) refreshCommand;
}
@Override
public byte getRefreshCommand() {
return refreshCommand;
}
@Override
public int getPayloadLength(boolean extendedCmd) {
return 8;
}
@Override
public ObjectType getObjectType() {
return ObjectType.DOOR;
}
@Override
public int getStartByte() {
return 0;
}
@Override
public int getBytesCount(boolean extendedCmd) {
return getPayloadLength(extendedCmd);
}
}

View File

@@ -0,0 +1,101 @@
/**
* 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.satel.internal.types;
/**
* Available Integra types.
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum IntegraType {
UNKNOWN(-1, "Unknown", 0, 0, 0),
I24(0, "Integra 24", 4, 24, 4),
I32(1, "Integra 32", 16, 32, 8),
I64(2, "Integra 64", 32, 64, 16),
I128(3, "Integra 128", 32, 128, 16),
I128_SIM300(4, "Integra 128-WRL SIM300", 32, 128, 8),
I128_LEON(132, "Integra 128-WRL LEON", 32, 128, 8),
I64_PLUS(66, "Integra 64 Plus", 32, 64, 16),
I128_PLUS(67, "Integra 128 Plus", 32, 128, 16),
I256_PLUS(72, "Integra 256 Plus", 32, 256, 16, true);
private int code;
private String name;
private int partitions;
private int zones;
private int onMainboard;
private boolean extPayload;
IntegraType(int code, String name, int partitions, int zones, int onMainboard) {
this(code, name, partitions, zones, onMainboard, false);
}
IntegraType(int code, String name, int partitions, int zones, int onMainboard, boolean extPayload) {
this.code = code;
this.name = name;
this.partitions = partitions;
this.zones = zones;
this.onMainboard = onMainboard;
this.extPayload = extPayload;
}
/**
* @return name of Integra type
*/
public String getName() {
return this.name;
}
/**
* @return max number of partitions
*/
public int getPartitions() {
return partitions;
}
/**
* @return max number of zones
*/
public int getZones() {
return zones;
}
/**
* @return wired zones/outputs available on mainboard
*/
public int getOnMainboard() {
return onMainboard;
}
/**
* @return <code>true</code> if this Integra requires extended message payload
*/
public boolean hasExtPayload() {
return this.extPayload;
}
/**
* Returns Integra type for given code.
*
* @param code code to get type for
* @return Integra type object
*/
public static IntegraType valueOf(int code) {
for (IntegraType val : IntegraType.values()) {
if (val.code == code) {
return val;
}
}
return UNKNOWN;
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.satel.internal.types;
/**
* Available object types:
* <ul>
* <li>ZONE - various kinds of devices connected to inputs, like reed switches, PIRs, etc</li>
* <li>PARTITION - group of zones and outputs</li>
* <li>OUTPUT - outputs to various devices like sirens, relays, etc.</li>
* <li>DOORS - inputs connected to reed switches mounted on doors</li>
* <li>TROUBLE - indication of a particular trouble</li>
* <li>TROUBLE_MEMORY - particular trouble state latched</li>
* </ul>
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum ObjectType {
ZONE,
PARTITION,
OUTPUT,
DOOR,
TROUBLE,
TROUBLE_MEMORY
}

View File

@@ -0,0 +1,54 @@
/**
* 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.satel.internal.types;
import java.util.BitSet;
/**
* Available output control types:
* <ul>
* <li>ON - sets an output</li>
* <li>OFF - resets an output</li>
* <li>TOGGLE - inverts output state</li>
* </ul>
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum OutputControl implements ControlType {
ON(0x88),
OFF(0x89),
TOGGLE(0x91);
private byte controlCommand;
private BitSet stateBits;
OutputControl(int controlCommand) {
this.controlCommand = (byte) controlCommand;
this.stateBits = StateType.getStatesBitSet(OutputState.STATE);
}
@Override
public byte getControlCommand() {
return controlCommand;
}
@Override
public ObjectType getObjectType() {
return ObjectType.OUTPUT;
}
@Override
public BitSet getControlledStates() {
return stateBits;
}
}

View File

@@ -0,0 +1,53 @@
/**
* 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.satel.internal.types;
/**
* Available output states.
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum OutputState implements StateType {
STATE(0x17);
private byte refreshCommand;
OutputState(int refreshCommand) {
this.refreshCommand = (byte) refreshCommand;
}
@Override
public byte getRefreshCommand() {
return refreshCommand;
}
@Override
public int getPayloadLength(boolean extendedCmd) {
return extendedCmd ? 32 : 16;
}
@Override
public ObjectType getObjectType() {
return ObjectType.OUTPUT;
}
@Override
public int getStartByte() {
return 0;
}
@Override
public int getBytesCount(boolean extendedCmd) {
return getPayloadLength(extendedCmd);
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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.satel.internal.types;
import java.util.BitSet;
/**
* Available partition control types.
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum PartitionControl implements ControlType {
ARM_MODE_0(0x80),
ARM_MODE_1(0x81),
ARM_MODE_2(0x82),
ARM_MODE_3(0x83),
DISARM(0x84),
CLEAR_ALARM(0x85),
FORCE_ARM_MODE_0(0xa0),
FORCE_ARM_MODE_1(0xa1),
FORCE_ARM_MODE_2(0xa2),
FORCE_ARM_MODE_3(0xa3);
private byte controlCommand;
private BitSet stateBits;
PartitionControl(int controlCommand) {
this.controlCommand = (byte) controlCommand;
// for simplicity we just add all partition states here
this.stateBits = StateType.getStatesBitSet(PartitionState.values());
}
@Override
public byte getControlCommand() {
return controlCommand;
}
@Override
public ObjectType getObjectType() {
return ObjectType.PARTITION;
}
@Override
public BitSet getControlledStates() {
return stateBits;
}
}

View File

@@ -0,0 +1,70 @@
/**
* 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.satel.internal.types;
/**
* Available partition states.
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum PartitionState implements StateType {
ARMED(0x09),
REALLY_ARMED(0x0a),
ARMED_MODE_2(0x0b),
ARMED_MODE_3(0x0c),
FIRST_CODE_ENTERED(0x0d),
ENTRY_TIME(0x0e),
EXIT_TIME_GT_10(0x0f),
EXIT_TIME_LT_10(0x10),
TEMPORARY_BLOCKED(0x11),
BLOCKED_FOR_GUARD(0x12),
ALARM(0x13),
FIRE_ALARM(0x14),
ALARM_MEMORY(0x15),
FIRE_ALARM_MEMORY(0x16),
VIOLATED_ZONES(0x25),
VERIFIED_ALARMS(0x27),
ARMED_MODE_1(0x2a),
WARNING_ALARMS(0x2b);
private byte refreshCommand;
PartitionState(int refreshCommand) {
this.refreshCommand = (byte) refreshCommand;
}
@Override
public byte getRefreshCommand() {
return refreshCommand;
}
@Override
public int getPayloadLength(boolean extendedCmd) {
return 4;
}
@Override
public ObjectType getObjectType() {
return ObjectType.PARTITION;
}
@Override
public int getStartByte() {
return 0;
}
@Override
public int getBytesCount(boolean extendedCmd) {
return getPayloadLength(extendedCmd);
}
}

View File

@@ -0,0 +1,108 @@
/**
* 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.satel.internal.types;
import java.util.BitSet;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Base of all kinds of Integra state.
*
* @author Krzysztof Goworek - Initial contribution
*/
@NonNullByDefault
public interface StateType {
/**
* Returns Satel command to get current state for this state type.
*
* @return command identifier
*/
byte getRefreshCommand();
/**
* Returns number of payload bytes in refresh command.
*
* @param extendedCmd if <code>true</code> return number of bytes for extended command
* @return payload length
*/
int getPayloadLength(boolean extendedCmd);
/**
* Returns object type for this kind of state.
*
* @return Integra object type
*/
ObjectType getObjectType();
/**
* Returns state's first byte in the response buffer.
*
* @return start byte in the response
*/
int getStartByte();
/**
* Returns number of state bytes in the response buffer.
*
* @param extendedCmd if <code>true</code> return number of bytes for extended command
* @return bytes count in the response
*/
int getBytesCount(boolean extendedCmd);
/**
* Builds bit set based on list of state types. Each bit is addressed by refresh command.
*
* @param states list of states
* @return built bit set
*/
public static BitSet getStatesBitSet(StateType... states) {
BitSet stateBits = new BitSet();
for (StateType state : states) {
stateBits.set(state.getRefreshCommand());
}
return stateBits;
}
/**
* Marker instance for lack of state type.
*/
public static final StateType NONE = new StateType() {
@Override
public byte getRefreshCommand() {
throw new UnsupportedOperationException("Illegal use of NONE state type");
}
@Override
public int getPayloadLength(boolean extendedCmd) {
throw new UnsupportedOperationException("Illegal use of NONE state type");
}
@Override
public ObjectType getObjectType() {
throw new UnsupportedOperationException("Illegal use of NONE state type");
}
@Override
public int getStartByte() {
throw new UnsupportedOperationException("Illegal use of NONE state type");
}
@Override
public int getBytesCount(boolean extendedCmd) {
throw new UnsupportedOperationException("Illegal use of NONE state type");
}
};
}

View File

@@ -0,0 +1,103 @@
/**
* 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.satel.internal.types;
/**
* Supported memory of troubles.
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum TroubleMemoryState implements StateType {
// part 1
TECHNICAL_ZONE(0x20, 47, 0, 16),
EXPANDER_AC(0x20, 47, 16, 8),
EXPANDER_BATT(0x20, 47, 24, 8),
EXPANDER_NOBATT(0x20, 47, 32, 8),
SYSTEM(0x20, 47, 40, 3),
PTSA_AC(0x20, 47, 43, 1),
PTSA_BATT(0x20, 47, 44, 1),
PTSA_NOBATT(0x20, 47, 45, 1),
ETHM1(0x20, 47, 46, 1),
// part 2
PROXIMITY_A(0x21, 39, 0, 8),
PROXIMITY_B(0x21, 39, 8, 8),
EXPANDER_OVERLOAD(0x21, 39, 16, 8),
JAMMED_ACU100(0x21, 39, 24, 2),
LCD_RESTART(0x21, 39, 26, 1),
EXPANDER_RESTART(0x21, 39, 27, 8),
// part 3
DEVICE_LOBATT(0x22, 60, 15, 15),
DEVICE_NOCOMM(0x22, 60, 30, 15),
OUTPUT_NOCOMM(0x22, 60, 45, 15),
// part 4
EXPANDER_NOCOMM(0x23, 30, 0, 8),
EXPANDER_SWITCHEROOED(0x23, 30, 8, 8),
KEYPAD_NOCOMM(0x23, 30, 16, 1),
KEYPAD_SWITCHEROOED(0x23, 30, 17, 1),
ETHM1_NOLAN(0x23, 30, 18, 1),
EXPANDER_TAMPER(0x23, 30, 19, 8),
KEYPAD_TAMPER(0x23, 30, 27, 1),
KEYPAD_INIT(0x23, 30, 28, 1),
AUXILIARY_STM(0x23, 30, 29, 1),
// part 5
LONG_VIOLATION(0x24, 48, 0, 16),
NO_VIOLATION(0x24, 48, 16, 16),
ZONE_TAMPER(0x24, 48, 32, 16),
// part 7 of troubles
TECHNICAL_ZONE1(0x2d, 47, 16, 16),
// part 6
DEVICE_LOBATT1(0x2e, 45, 0, 15),
DEVICE_NOCOMM1(0x2e, 45, 15, 15),
OUTPUT_NOCOMM1(0x2e, 45, 30, 15),
// part 7
LONG_VIOLATION1(0x2f, 48, 0, 16),
NO_VIOLATION1(0x2f, 48, 16, 16),
ZONE_TAMPER1(0x2f, 48, 32, 16),;
private byte refreshCommand;
private int payloadLength;
private int startByte;
private int bytesCount;
TroubleMemoryState(int refreshCommand, int payloadLength, int startByte, int bytesCount) {
this.refreshCommand = (byte) refreshCommand;
this.payloadLength = payloadLength;
this.startByte = startByte;
this.bytesCount = bytesCount;
}
@Override
public byte getRefreshCommand() {
return refreshCommand;
}
@Override
public int getPayloadLength(boolean extendedCmd) {
return payloadLength;
}
@Override
public ObjectType getObjectType() {
return ObjectType.TROUBLE_MEMORY;
}
@Override
public int getStartByte() {
return startByte;
}
@Override
public int getBytesCount(boolean isExtended) {
return bytesCount;
}
}

View File

@@ -0,0 +1,96 @@
/**
* 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.satel.internal.types;
/**
* Supported troubles.
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum TroubleState implements StateType {
// part 1
TECHNICAL_ZONE(0x1b, 47, 0, 16),
EXPANDER_AC(0x1b, 47, 16, 8),
EXPANDER_BATT(0x1b, 47, 24, 8),
EXPANDER_NOBATT(0x1b, 47, 32, 8),
SYSTEM(0x1b, 47, 40, 3),
PTSA_AC(0x1b, 47, 43, 1),
PTSA_BATT(0x1b, 47, 44, 1),
PTSA_NOBATT(0x1b, 47, 45, 1),
ETHM1(0x1b, 47, 46, 1),
// part 2
PROXIMITY_A(0x1c, 26, 0, 8),
PROXIMITY_B(0x1c, 26, 8, 8),
EXPANDER_OVERLOAD(0x1c, 26, 16, 8),
JAMMED_ACU100(0x1c, 26, 24, 2),
// part 3
DEVICE_LOBATT(0x1d, 60, 15, 15),
DEVICE_NOCOMM(0x1d, 60, 30, 15),
OUTPUT_NOCOMM(0x1d, 60, 45, 15),
// part 4
EXPANDER_NOCOMM(0x1e, 30, 0, 8),
EXPANDER_SWITCHEROOED(0x1e, 30, 8, 8),
KEYPAD_NOCOMM(0x1e, 30, 16, 1),
KEYPAD_SWITCHEROOED(0x1e, 30, 17, 1),
ETHM1_NOLAN(0x1e, 30, 18, 1),
EXPANDER_TAMPER(0x1e, 30, 19, 8),
KEYPAD_TAMPER(0x1e, 30, 27, 1),
KEYPAD_INIT(0x1e, 30, 28, 1),
AUXILIARY_STM(0x1e, 30, 29, 1),
// part 5
MASTER_KEYFOB(0x1f, 31, 0, 1),
USER_KEYFOB(0x1f, 31, 1, 30),
// part 6
DEVICE_LOBATT1(0x2c, 45, 0, 15),
DEVICE_NOCOMM1(0x2c, 45, 15, 15),
OUTPUT_NOCOMM1(0x2c, 45, 30, 15),
// part 7
TECHNICAL_ZONE1(0x2d, 47, 0, 16),;
private byte refreshCommand;
private int payloadLength;
private int startByte;
private int bytesCount;
TroubleState(int refreshCommand, int payloadLength, int startByte, int bytesCount) {
this.refreshCommand = (byte) refreshCommand;
this.payloadLength = payloadLength;
this.startByte = startByte;
this.bytesCount = bytesCount;
}
@Override
public byte getRefreshCommand() {
return refreshCommand;
}
@Override
public int getPayloadLength(boolean extendedCmd) {
return payloadLength;
}
@Override
public ObjectType getObjectType() {
return ObjectType.TROUBLE;
}
@Override
public int getStartByte() {
return startByte;
}
@Override
public int getBytesCount(boolean isExtended) {
return bytesCount;
}
}

View File

@@ -0,0 +1,49 @@
/**
* 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.satel.internal.types;
import java.util.BitSet;
/**
* Available zone control types.
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum ZoneControl implements ControlType {
BYPASS(0x86, ZoneState.BYPASS),
UNBYPASS(0x87, ZoneState.BYPASS),
ISOLATE(0x90, ZoneState.ISOLATE);
private byte controlCommand;
private BitSet stateBits;
ZoneControl(int controlCommand, ZoneState... controlledStates) {
this.controlCommand = (byte) controlCommand;
this.stateBits = StateType.getStatesBitSet(controlledStates);
}
@Override
public byte getControlCommand() {
return controlCommand;
}
@Override
public ObjectType getObjectType() {
return ObjectType.ZONE;
}
@Override
public BitSet getControlledStates() {
return stateBits;
}
}

View File

@@ -0,0 +1,64 @@
/**
* 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.satel.internal.types;
/**
* Available zone states.
*
* @author Krzysztof Goworek - Initial contribution
*/
public enum ZoneState implements StateType {
VIOLATION(0x00),
TAMPER(0x01),
ALARM(0x02),
TAMPER_ALARM(0x03),
ALARM_MEMORY(0x04),
TAMPER_ALARM_MEMORY(0x05),
BYPASS(0x06),
NO_VIOLATION_TROUBLE(0x07),
LONG_VIOLATION_TROUBLE(0x08),
ISOLATE(0x26),
MASKED(0x28),
MASKED_MEMORY(0x29);
private byte refreshCommand;
ZoneState(int refreshCommand) {
this.refreshCommand = (byte) refreshCommand;
}
@Override
public byte getRefreshCommand() {
return refreshCommand;
}
@Override
public int getPayloadLength(boolean extendedCmd) {
return extendedCmd ? 32 : 16;
}
@Override
public ObjectType getObjectType() {
return ObjectType.ZONE;
}
@Override
public int getStartByte() {
return 0;
}
@Override
public int getBytesCount(boolean extendedCmd) {
return getPayloadLength(extendedCmd);
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="satel" 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>Satel Binding</name>
<description>This is the binding for Satel INTEGRA alarm systems using either ETHM-1 or INT-RS communication module</description>
<author>Krzysztof Goworek</author>
</binding:binding>

View File

@@ -0,0 +1,19 @@
# config status
config-status.hostEmpty = Host name or IP of ETHM-1 must be provided.
config-status.portEmpty = Serial port of INT-RS must be provided.
# actions
actionReadEventLabel = Read event
actionReadEventDesc = Reads a single record from the event log.
actionInputIndexLabel = Event index
actionInputIndexDesc = Index of the event to read
actionOutputIndexLabel = Current Index
actionOutputIndexDesc = Index of this record in the event log.
actionOutputPrevIndexLabel = Previous Index
actionOutputPrevIndexDesc = Index of the previous record in the event log. Use this value to iterate over the log.
actionOutputTimestampLabel = Date and Time
actionOutputTimestampDesc = Date and time when the event happened.
actionOutputDescriptionLabel = Description
actionOutputDescriptionDesc = Textual description of the event.
actionOutputDetailsLabel = Details
actionOutputDetailsDesc = Additional details about the event, like names of partitions, zones, users, etc.

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="satel"
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="atd-100">
<supported-bridge-type-refs>
<bridge-type-ref id="ethm-1"/>
<bridge-type-ref id="int-rs"/>
</supported-bridge-type-refs>
<label>ATD-100</label>
<description><![CDATA[Wireless temperature detector designed for operation as part of the ABAX two-way wireless system.]]></description>
<category>Sensor</category>
<channels>
<channel typeId="zoneTemperature" id="temperature"/>
<channel typeId="system.low-battery" id="device_lobatt"/>
<channel typeId="device_nocomm" id="device_nocomm"/>
</channels>
<config-description>
<parameter name="id" type="integer" min="1" max="256" required="true">
<label>Zone Number</label>
<description>Zone number in the alarm system monitored by this detector</description>
</parameter>
<parameter name="refresh" type="integer" unit="min" min="1">
<label>Refresh Interval</label>
<description>Polling interval in minutes.</description>
<default>15</default>
<unitLabel>Minutes</unitLabel>
</parameter>
</config-description>
</thing-type>
<channel-type id="zoneTemperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Current temperature in the zone</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="satel"
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">
<channel-type id="device_nocomm">
<item-type>Switch</item-type>
<label>No Communication</label>
<description>Indicates communication troubles with the wireless device</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="satel"
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">
<bridge-type id="ethm-1">
<label>ETHM-1</label>
<description>Ethernet communication module</description>
<config-description>
<parameter name="host" type="text" required="true">
<label>Host or IP</label>
<description>Host name or IP address of ETHM-1 module.</description>
<context>network-address</context>
</parameter>
<parameter name="port" type="integer" min="1" max="65535">
<label>Port Number</label>
<description>TCP port for the integration protocol.</description>
<default>7094</default>
</parameter>
<parameter name="timeout" type="integer" unit="ms">
<label>Timeout</label>
<description>Timeout value in milliseconds for connect, read and write operations.</description>
<default>5000</default>
<unitLabel>Milliseconds</unitLabel>
</parameter>
<parameter name="refresh" type="integer" unit="ms">
<label>Refresh Interval</label>
<description>Polling interval in milliseconds.</description>
<default>5000</default>
<unitLabel>Milliseconds</unitLabel>
</parameter>
<parameter name="userCode" type="text" pattern="[0-9]{4,8}">
<label>User Code</label>
<description>Security code of the user in behalf of all operations will be executed. If empty, only read operations
are allowed.</description>
<context>password</context>
</parameter>
<parameter name="encryptionKey" type="text">
<label>Encryption Key</label>
<description>Encryption key used to encrypt data sent and received, if empty communication is not encrypted.</description>
<context>password</context>
</parameter>
<parameter name="encoding" type="text">
<label>Encoding</label>
<description>Encoding used for all the texts received from the module.</description>
<default>windows-1250</default>
</parameter>
<parameter name="extCommands" type="boolean">
<label>Extended Commands Support</label>
<description>Check this option to enable extended commands supported only by ETHM-1 Plus and newer versions of
ETHM-1. Turn off in case of communication timeouts.</description>
<default>true</default>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="satel"
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="event-log">
<supported-bridge-type-refs>
<bridge-type-ref id="ethm-1"/>
<bridge-type-ref id="int-rs"/>
</supported-bridge-type-refs>
<label>Event Log</label>
<description><![CDATA[A virtual thing that allows reading records from the alarm system event log.<br>
Event log must read sequentially starting from the most recent record.]]></description>
<channels>
<channel typeId="index" id="index"/>
<channel typeId="prev_index" id="prev_index"/>
<channel typeId="timestamp" id="timestamp"/>
<channel typeId="description" id="description"/>
<channel typeId="details" id="details"/>
</channels>
</thing-type>
<channel-type id="index">
<item-type>Number</item-type>
<label>Current Index</label>
<description>Index of the current record in the event log. Send '-1' to get most recent record.</description>
</channel-type>
<channel-type id="prev_index">
<item-type>Number</item-type>
<label>Previous Index</label>
<description>Index of the previous record in the event log. Use this value to iterate over the log.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="timestamp">
<item-type>DateTime</item-type>
<label>Date and Time</label>
<description>Date and time when the event happened.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="description">
<item-type>String</item-type>
<label>Description</label>
<description>Textual description of the event.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="details">
<item-type>String</item-type>
<label>Details</label>
<description>Additional details about the event, like names of partitions, zones, users, etc.</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="satel"
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">
<bridge-type id="int-rs">
<label>INT-RS</label>
<description>RS-232 communication module</description>
<config-description>
<parameter name="port" type="text" required="true">
<label>Serial Port</label>
<description>Serial port connected to the module.</description>
<context>serial-port</context>
<limitToOptions>false</limitToOptions>
</parameter>
<parameter name="timeout" type="integer" unit="ms">
<label>Timeout</label>
<description>Timeout value in milliseconds for connect, read and write operations.</description>
<default>5000</default>
<unitLabel>Milliseconds</unitLabel>
</parameter>
<parameter name="refresh" type="integer" unit="ms">
<label>Refresh Interval</label>
<description>Current state polling interval, in milliseconds.</description>
<default>5000</default>
<unitLabel>Milliseconds</unitLabel>
</parameter>
<parameter name="userCode" type="text" pattern="[0-9]{4,8}">
<label>User Code</label>
<description>Security code of the user in behalf of all operations will be executed.</description>
<context>password</context>
</parameter>
<parameter name="encoding" type="text">
<label>Encoding</label>
<description>Encoding for all the texts received from the module.</description>
<default>windows-1250</default>
</parameter>
<parameter name="extCommands" type="boolean">
<label>Extended Commands Support</label>
<description>Check this option to enable extended commands supported only by version 2.xx of INT-RS. Turn off in
case of communication timeouts.</description>
<default>true</default>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="satel"
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="output">
<supported-bridge-type-refs>
<bridge-type-ref id="ethm-1"/>
<bridge-type-ref id="int-rs"/>
</supported-bridge-type-refs>
<label>Output</label>
<description><![CDATA[An output from the alarm system.<br>The system supports the following outputs:<ul>
<li>hardwired - on the control panel electronics board and in expanders. The number of available hardwired outputs is determined by the control panel during identification procedure.</li>
<li>wireless - in the INTEGRA 128-WRL control panel and/or when the ACU-120, ACU-270, ACU-100 or ACU-250 controller is connected. The number of available wireless outputs depends on the number of wireless devices registered in the system and is determined during the procedure of adding wireless devices.</li>
<li>virtual - the outputs which do not exist physically, but can be used e.g. for execution of logical functions.</li></ul>]]>
</description>
<channels>
<channel typeId="output_state" id="state"/>
</channels>
<config-description>
<parameter name="id" type="integer" min="1" max="256" required="true">
<label>Output Number</label>
<description>Number that identifies the output in the alarm system</description>
</parameter>
<parameter name="invertState" type="boolean" required="false">
<label>Invert State</label>
<description>Changes active (ON) state to 0</description>
</parameter>
<parameter name="commandOnly" type="boolean" required="false">
<label>Command Only</label>
<description>Accepts commands only, does not update state of the thing</description>
</parameter>
<parameter name="wireless" type="boolean" required="false">
<label>Wireless Output</label>
<description>This output controls a wireless device like ASP-100 R, ASW-100 E, etc.</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="output_state">
<item-type>Switch</item-type>
<label>Output State</label>
<description>Represents state of the output</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="satel"
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="partition">
<supported-bridge-type-refs>
<bridge-type-ref id="ethm-1"/>
<bridge-type-ref id="int-rs"/>
</supported-bridge-type-refs>
<label>Partition</label>
<description><![CDATA[The partition is a separated area within the premises protected by the security alarm system.<br>
The subdivision into partitions enables arming/disarming the system only in part of the protected area, as well as limiting
access to some portion of the premises to selected users.]]>
</description>
<channels>
<channel typeId="armed" id="armed"/>
<channel typeId="really_armed" id="really_armed"/>
<channel typeId="armed_mode_1" id="armed_mode_1"/>
<channel typeId="armed_mode_2" id="armed_mode_2"/>
<channel typeId="armed_mode_3" id="armed_mode_3"/>
<channel typeId="first_code_entered" id="first_code_entered"/>
<channel typeId="entry_time" id="entry_time"/>
<channel typeId="exit_time_gt_10" id="exit_time_gt_10"/>
<channel typeId="exit_time_lt_10" id="exit_time_lt_10"/>
<channel typeId="temporary_blocked" id="temporary_blocked"/>
<channel typeId="blocked_for_guard" id="blocked_for_guard"/>
<channel typeId="alarm" id="alarm"/>
<channel typeId="alarm_memory" id="alarm_memory"/>
<channel typeId="fire_alarm" id="fire_alarm"/>
<channel typeId="fire_alarm_memory" id="fire_alarm_memory"/>
<channel typeId="verified_alarms" id="verified_alarms"/>
<channel typeId="warning_alarms" id="warning_alarms"/>
<channel typeId="violated_zones" id="violated_zones"/>
</channels>
<config-description>
<parameter name="id" type="integer" min="1" max="32" required="true">
<label>Partition Number</label>
<description></description>
</parameter>
<parameter name="forceArming" type="boolean" required="false">
<label>Force Arming</label>
<description>Forces arming the partition regardless of ongoing troubles and violations.</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="armed">
<item-type>Switch</item-type>
<label>Armed</label>
<description>Active when the partion is armed in any mode or arming is in progress</description>
</channel-type>
<channel-type id="really_armed">
<item-type>Switch</item-type>
<label>Really Armed</label>
<description>Active when the partion is armed in any mode</description>
</channel-type>
<channel-type id="armed_mode_1" advanced="true">
<item-type>Switch</item-type>
<label>Armed in Mode 1</label>
<description>Active when the partion is armed in mode 1</description>
</channel-type>
<channel-type id="armed_mode_2" advanced="true">
<item-type>Switch</item-type>
<label>Armed in Mode 2</label>
<description>Active when the partion is armed in mode 2</description>
</channel-type>
<channel-type id="armed_mode_3" advanced="true">
<item-type>Switch</item-type>
<label>Armed in Mode 3</label>
<description>Active when the partion is armed in mode 3</description>
</channel-type>
<channel-type id="first_code_entered" advanced="true">
<item-type>Switch</item-type>
<label>First Code Entered</label>
<description>Active when first code of required two codes has been entered</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="entry_time" advanced="true">
<item-type>Switch</item-type>
<label>Entry Time</label>
<description>Active in entry time</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="exit_time_gt_10" advanced="true">
<item-type>Switch</item-type>
<label>Exit Time &gt; 10s</label>
<description>Active when arming is in progress and time to exit is greater than ten seconds</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="exit_time_lt_10" advanced="true">
<item-type>Switch</item-type>
<label>Exit Time &lt; 10s</label>
<description>Active when arming is in progress and time to exit is less than ten seconds</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="temporary_blocked" advanced="true">
<item-type>Switch</item-type>
<label>Temporary Blocked</label>
<description>Active when the partition is blocked after arming</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="blocked_for_guard" advanced="true">
<item-type>Switch</item-type>
<label>Blocked for Guard</label>
<description>Active when the partition is blocked for guard round</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="alarm">
<item-type>Switch</item-type>
<label>Alarm</label>
<description>Active when there is ongoing alarm in the patition</description>
</channel-type>
<channel-type id="alarm_memory">
<item-type>Switch</item-type>
<label>Alarm Memory</label>
<description>Active when there had been an alarm in the patition</description>
</channel-type>
<channel-type id="fire_alarm">
<item-type>Switch</item-type>
<label>Fire Alarm</label>
<description>Active when there is ongoing fire alarm in the patition</description>
</channel-type>
<channel-type id="fire_alarm_memory">
<item-type>Switch</item-type>
<label>Fire Alarm Memory</label>
<description>Active when there had been a fire alarm in the patition</description>
</channel-type>
<channel-type id="verified_alarms" advanced="true">
<item-type>Switch</item-type>
<label>Verified Alarms</label>
<description>Active when two zones triggered an alarm during verification time</description>
</channel-type>
<channel-type id="warning_alarms" advanced="true">
<item-type>Switch</item-type>
<label>Warning Alarms</label>
<description>Active when there are warning alarms in the partition</description>
</channel-type>
<channel-type id="violated_zones" advanced="true">
<item-type>Switch</item-type>
<label>Violated Zones</label>
<description>Active when there are violated zones in the partition</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="satel"
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="shutter">
<supported-bridge-type-refs>
<bridge-type-ref id="ethm-1"/>
<bridge-type-ref id="int-rs"/>
</supported-bridge-type-refs>
<label>Roller Shutter</label>
<description>A roller shutter connected to two subsequent outputs.</description>
<channels>
<channel typeId="shutter_state" id="shutter_state"/>
</channels>
<config-description>
<parameter name="upId" type="integer" min="1" max="256" required="true">
<label>Up Output</label>
<description>Output number for "up" direction.</description>
</parameter>
<parameter name="downId" type="integer" min="1" max="256" required="true">
<label>Down Output</label>
<description>Output number for "down" direction.</description>
</parameter>
<parameter name="commandOnly" type="boolean" required="false">
<label>Command Only</label>
<description>Accepts commands only, does not update state of the thing</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="shutter_state">
<item-type>Rollershutter</item-type>
<label>Shutter State</label>
<description>Represents state of the roller shutter</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="satel"
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="system">
<supported-bridge-type-refs>
<bridge-type-ref id="ethm-1"/>
<bridge-type-ref id="int-rs"/>
</supported-bridge-type-refs>
<label>Alarm System</label>
<description>A virtual thing describing general status of the alarm system.</description>
<channels>
<channel typeId="date_time" id="date_time"/>
<channel typeId="troubles" id="troubles"/>
<channel typeId="troubles_memory" id="troubles_memory"/>
<channel typeId="service_mode" id="service_mode"/>
<channel typeId="acu100_present" id="acu100_present"/>
<channel typeId="intrx_present" id="intrx_present"/>
<channel typeId="grade23_set" id="grade23_set"/>
<channel typeId="user_code" id="user_code"/>
</channels>
</thing-type>
<channel-type id="date_time">
<item-type>DateTime</item-type>
<label>Date and Time</label>
<description>Current date and time in the alarm system</description>
</channel-type>
<channel-type id="troubles">
<item-type>Switch</item-type>
<label>Troubles</label>
<description>Active when the system has troubles (trouble led is blinking on a panel)</description>
</channel-type>
<channel-type id="troubles_memory" advanced="true">
<item-type>Switch</item-type>
<label>Troubles Memory</label>
<description>Memorized state of system troubles</description>
</channel-type>
<channel-type id="service_mode">
<item-type>Switch</item-type>
<label>Service Mode</label>
<description>Active when the system is in service mode</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="acu100_present" advanced="true">
<item-type>Switch</item-type>
<label>ACU-100 Present</label>
<description>Active when there is an ACU-100 module installed in the system</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="intrx_present" advanced="true">
<item-type>Switch</item-type>
<label>INT-RX Present</label>
<description>Active when there is an INT-RX module installed in the system</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="grade23_set" advanced="true">
<item-type>Switch</item-type>
<label>Grade2/Grade3 Set</label>
<description>Active when Grade2/Grade3 option is set in the system</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="user_code">
<item-type>String</item-type>
<label>User Code</label>
<description>Accepts string commands that override configured user code. Send empty string to revert user code to the
one in the binding configuration.</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="satel"
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="zone">
<supported-bridge-type-refs>
<bridge-type-ref id="ethm-1"/>
<bridge-type-ref id="int-rs"/>
</supported-bridge-type-refs>
<label>Zone</label>
<description><![CDATA[A separated part of the protected area that can supervised by a detector connected to the alarm system.<br>The system supports the following zones:<ul>
<li>hardwired - on the control panel electronics board, in keypads and expanders. The number of available hardwired zones is determined by the control panel during identification procedure.</li>
<li>wireless - in the INTEGRA 128-WRL control panel and/or when the ACU-120, ACU-270, ACU-100 or ACU-250 controller is connected. The number of available wireless zones depends on the number of wireless devices registered in the system and is determined during the procedure of adding the devices.</li>
<li>virtual - zones which physically do not exist, but have been programmed as FOLLOW OUTPUT or are controlled by means of a keyfob.</li></ul>]]></description>
<channels>
<channel typeId="violation" id="violation"/>
<channel typeId="tamper" id="tamper"/>
<channel typeId="alarm" id="alarm"/>
<channel typeId="tamper_alarm" id="tamper_alarm"/>
<channel typeId="alarm_memory" id="alarm_memory"/>
<channel typeId="tamper_alarm_memory" id="tamper_alarm_memory"/>
<channel typeId="bypass" id="bypass"/>
<channel typeId="no_violation_trouble" id="no_violation_trouble"/>
<channel typeId="long_violation_trouble" id="long_violation_trouble"/>
<channel typeId="isolate" id="isolate"/>
<channel typeId="masked" id="masked"/>
<channel typeId="masked_memory" id="masked_memory"/>
</channels>
<config-description>
<parameter name="id" type="integer" min="1" max="256" required="true">
<label>Zone Number</label>
<description>Number that identifies the zone in the alarm system</description>
</parameter>
<parameter name="invertState" type="boolean" required="false">
<label>Invert State</label>
<description>Changes active (ON) state to 0</description>
</parameter>
<parameter name="wireless" type="boolean" required="false">
<label>Wireless Zone</label>
<description>This zone is monitored by a wireless detector like APD-100, AFD-100, etc</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="violation">
<item-type>Switch</item-type>
<label>Violation</label>
<description>Active when violation is detected in the zone</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="tamper">
<item-type>Switch</item-type>
<label>Tamper</label>
<description>Active when detector in the zone is tampered</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="alarm">
<item-type>Switch</item-type>
<label>Alarm</label>
<description>Active when violation is detected in the zone and zone is armed</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="tamper_alarm">
<item-type>Switch</item-type>
<label>Tamper Alarm</label>
<description>Active when detector in the zone is tampered and zone is armed</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="alarm_memory">
<item-type>Switch</item-type>
<label>Alarm Memory</label>
<description>Memorized alarm state for the zone</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="tamper_alarm_memory">
<item-type>Switch</item-type>
<label>Tamper Alarm Memory</label>
<description>Memorized tamper alarm state for the zone</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="bypass">
<item-type>Switch</item-type>
<label>Bypass</label>
<description>Active when zone is bypassed</description>
</channel-type>
<channel-type id="no_violation_trouble" advanced="true">
<item-type>Switch</item-type>
<label>No Violation Trouble</label>
<description>Active when no violation has been detected in the zone for configured time</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="long_violation_trouble" advanced="true">
<item-type>Switch</item-type>
<label>Long Violation Trouble</label>
<description>Active when violation in the zone lasts longer than configured time</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="isolate" advanced="true">
<item-type>Switch</item-type>
<label>Isolate</label>
<description>Active when zone is isolated</description>
</channel-type>
<channel-type id="masked" advanced="true">
<item-type>Switch</item-type>
<label>Masked</label>
<description>Active when zone is masked</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="masked_memory" advanced="true">
<item-type>Switch</item-type>
<label>Masked Memory</label>
<description>Memorized masked state for the zone</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>