2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.nikobus.internal.handler;
15 import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.CHANNEL_OUTPUT_PREFIX;
17 import java.util.ArrayList;
18 import java.util.EnumSet;
19 import java.util.HashMap;
20 import java.util.HashSet;
21 import java.util.List;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.openhab.binding.nikobus.internal.protocol.NikobusCommand;
27 import org.openhab.binding.nikobus.internal.protocol.SwitchModuleCommandFactory;
28 import org.openhab.binding.nikobus.internal.protocol.SwitchModuleGroup;
29 import org.openhab.core.thing.ChannelUID;
30 import org.openhab.core.thing.Thing;
31 import org.openhab.core.thing.ThingStatus;
32 import org.openhab.core.thing.ThingStatusDetail;
33 import org.openhab.core.types.Command;
34 import org.openhab.core.types.RefreshType;
35 import org.openhab.core.types.State;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
40 * The {@link NikobusSwitchModuleHandler} is responsible for communication between Nikobus modules and binding.
42 * @author Boris Krivonog - Initial contribution
45 abstract class NikobusModuleHandler extends NikobusBaseThingHandler {
46 private final EnumSet<SwitchModuleGroup> pendingRefresh = EnumSet.noneOf(SwitchModuleGroup.class);
47 private final Logger logger = LoggerFactory.getLogger(NikobusModuleHandler.class);
48 private final Map<String, Integer> cachedStates = new HashMap<>();
49 private final List<ChannelUID> linkedChannels = new ArrayList<>();
51 protected NikobusModuleHandler(Thing thing) {
56 public void dispose() {
59 synchronized (cachedStates) {
63 synchronized (pendingRefresh) {
64 pendingRefresh.clear();
69 public void handleCommand(ChannelUID channelUID, Command command) {
70 if (command instanceof RefreshType) {
71 refreshChannel(channelUID);
73 processWrite(channelUID, command);
77 private void refreshChannel(ChannelUID channelUID) {
78 logger.debug("Refreshing channel '{}'", channelUID.getId());
80 if (!isLinked(channelUID)) {
81 logger.debug("Refreshing channel '{}' skipped since it is not linked", channelUID.getId());
85 updateGroup(SwitchModuleGroup.mapFromChannel(channelUID));
89 public void channelLinked(ChannelUID channelUID) {
90 synchronized (linkedChannels) {
91 linkedChannels.add(channelUID);
93 super.channelLinked(channelUID);
97 public void channelUnlinked(ChannelUID channelUID) {
98 synchronized (linkedChannels) {
99 linkedChannels.remove(channelUID);
101 super.channelUnlinked(channelUID);
104 public void refreshModule() {
105 Set<SwitchModuleGroup> groups = new HashSet<>();
106 synchronized (linkedChannels) {
107 for (ChannelUID channelUID : linkedChannels) {
108 groups.add(SwitchModuleGroup.mapFromChannel(channelUID));
112 if (groups.isEmpty()) {
113 logger.debug("Nothing to refresh for '{}'", thing.getUID());
117 logger.debug("Refreshing {} - {}", thing.getUID(), groups);
119 for (SwitchModuleGroup group : groups) {
124 public void requestStatus(SwitchModuleGroup group) {
128 private void updateGroup(SwitchModuleGroup group) {
129 synchronized (pendingRefresh) {
130 if (pendingRefresh.contains(group)) {
131 logger.debug("Refresh already scheduled for group {} of module '{}'", group, getAddress());
135 pendingRefresh.add(group);
138 logger.debug("Refreshing group {} of switch module '{}'", group, getAddress());
140 NikobusPcLinkHandler pcLink = getPcLink();
141 if (pcLink != null) {
142 NikobusCommand command = SwitchModuleCommandFactory.createReadCommand(getAddress(), group,
143 result -> processStatusUpdate(result, group));
144 pcLink.sendCommand(command);
148 private void processStatusUpdate(NikobusCommand.Result result, SwitchModuleGroup group) {
150 String responsePayload = result.get();
152 logger.debug("processStatusUpdate '{}' for group {} in module '{}'", responsePayload, group, getAddress());
154 if (thing.getStatus() != ThingStatus.ONLINE) {
155 updateStatus(ThingStatus.ONLINE);
158 // Update channel's statuses based on response.
159 for (int i = 0; i < group.getCount(); i++) {
160 String channelId = CHANNEL_OUTPUT_PREFIX + (i + group.getOffset());
161 String responseDigits = responsePayload.substring(9 + (i * 2), 11 + (i * 2));
163 int value = Integer.parseInt(responseDigits, 16);
165 updateStateAndCacheValue(channelId, value);
167 } catch (Exception e) {
168 logger.warn("Processing response for '{}'-{} failed with {}", getAddress(), group, e.getMessage(), e);
169 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
171 synchronized (pendingRefresh) {
172 pendingRefresh.remove(group);
177 private void updateStateAndCacheValue(String channelId, int value) {
178 if (value < 0x00 || value > 0xff) {
179 throw new IllegalArgumentException("Invalid range. 0x00 - 0xff expected but got value " + value);
182 logger.debug("setting channel '{}' to {}", channelId, value);
184 synchronized (cachedStates) {
185 cachedStates.put(channelId, value);
188 updateState(channelId, stateFromValue(value));
191 @SuppressWarnings({ "unused", "null" })
192 private void processWrite(ChannelUID channelUID, Command command) {
193 StringBuilder commandPayload = new StringBuilder();
194 SwitchModuleGroup group = SwitchModuleGroup.mapFromChannel(channelUID);
196 for (int i = group.getOffset(); i < group.getOffset() + group.getCount(); i++) {
197 String channelId = CHANNEL_OUTPUT_PREFIX + i;
200 if (channelId.equals(channelUID.getId())) {
201 digits = valueFromCommand(command);
202 updateStateAndCacheValue(channelId, digits.intValue());
204 synchronized (cachedStates) {
205 digits = cachedStates.get(channelId);
209 if (digits == null) {
210 commandPayload.append("00");
211 logger.warn("no cached value found for '{}' in module '{}'", channelId, getAddress());
213 commandPayload.append(String.format("%02X", digits.intValue()));
217 NikobusPcLinkHandler pcLink = getPcLink();
218 if (pcLink != null) {
219 pcLink.sendCommand(SwitchModuleCommandFactory.createWriteCommand(getAddress(), group,
220 commandPayload.toString(), this::processWriteCommandResponse));
224 private void processWriteCommandResponse(NikobusCommand.Result result) {
226 String responsePayload = result.get();
228 logger.debug("processWriteCommandResponse '{}'", responsePayload);
230 if (thing.getStatus() != ThingStatus.ONLINE) {
231 updateStatus(ThingStatus.ONLINE);
233 } catch (Exception e) {
234 logger.warn("Processing write confirmation failed with {}", e.getMessage());
235 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
239 protected abstract int valueFromCommand(Command command);
241 protected abstract State stateFromValue(int value);