2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.nikobus.internal.handler;
15 import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.CHANNEL_OUTPUT_PREFIX;
17 import java.util.EnumSet;
18 import java.util.HashMap;
19 import java.util.HashSet;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.openhab.binding.nikobus.internal.protocol.NikobusCommand;
25 import org.openhab.binding.nikobus.internal.protocol.SwitchModuleCommandFactory;
26 import org.openhab.binding.nikobus.internal.protocol.SwitchModuleGroup;
27 import org.openhab.core.thing.Channel;
28 import org.openhab.core.thing.ChannelUID;
29 import org.openhab.core.thing.Thing;
30 import org.openhab.core.thing.ThingStatus;
31 import org.openhab.core.thing.ThingStatusDetail;
32 import org.openhab.core.types.Command;
33 import org.openhab.core.types.RefreshType;
34 import org.openhab.core.types.State;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
39 * The {@link NikobusSwitchModuleHandler} is responsible for communication between Nikobus modules and binding.
41 * @author Boris Krivonog - Initial contribution
44 abstract class NikobusModuleHandler extends NikobusBaseThingHandler {
45 private final EnumSet<SwitchModuleGroup> pendingRefresh = EnumSet.noneOf(SwitchModuleGroup.class);
46 private final Logger logger = LoggerFactory.getLogger(NikobusModuleHandler.class);
47 private final Map<String, Integer> cachedStates = new HashMap<>();
49 protected NikobusModuleHandler(Thing thing) {
54 public void initialize() {
57 if (thing.getStatus() != ThingStatus.OFFLINE) {
58 // Fetch all linked channels to get initial values.
59 thing.getChannels().forEach(channel -> refreshChannel(channel.getUID()));
64 public void dispose() {
67 synchronized (cachedStates) {
71 synchronized (pendingRefresh) {
72 pendingRefresh.clear();
77 public void handleCommand(ChannelUID channelUID, Command command) {
78 logger.debug("handleCommand '{}' for channel '{}'", command, channelUID.getId());
80 if (command instanceof RefreshType) {
81 refreshChannel(channelUID);
83 processWrite(channelUID, command);
87 private void refreshChannel(ChannelUID channelUID) {
88 logger.debug("Refreshing channel '{}'", channelUID.getId());
90 if (!isLinked(channelUID)) {
91 logger.debug("Refreshing channel '{}' skipped since it is not linked", channelUID.getId());
95 updateGroup(SwitchModuleGroup.mapFromChannel(channelUID));
98 public void refreshModule() {
99 Set<SwitchModuleGroup> groups = new HashSet<>();
100 for (Channel channel : thing.getChannels()) {
101 ChannelUID channelUID = channel.getUID();
102 if (isLinked(channelUID)) {
103 groups.add(SwitchModuleGroup.mapFromChannel(channelUID));
107 if (groups.isEmpty()) {
108 logger.debug("Nothing to refresh for '{}'", thing.getUID());
112 logger.debug("Refreshing {} - {}", thing.getUID(), groups);
114 for (SwitchModuleGroup group : groups) {
119 public void requestStatus(SwitchModuleGroup group) {
123 private void updateGroup(SwitchModuleGroup group) {
124 synchronized (pendingRefresh) {
125 if (pendingRefresh.contains(group)) {
126 logger.debug("Refresh already scheduled for group {} of module '{}'", group, getAddress());
130 pendingRefresh.add(group);
133 logger.debug("Refreshing group {} of switch module '{}'", group, getAddress());
135 NikobusPcLinkHandler pcLink = getPcLink();
136 if (pcLink != null) {
137 NikobusCommand command = SwitchModuleCommandFactory.createReadCommand(getAddress(), group,
138 result -> processStatusUpdate(result, group));
139 pcLink.sendCommand(command);
143 private void processStatusUpdate(NikobusCommand.Result result, SwitchModuleGroup group) {
145 String responsePayload = result.get();
147 logger.debug("processStatusUpdate '{}' for group {} in module '{}'", responsePayload, group, getAddress());
149 if (thing.getStatus() != ThingStatus.ONLINE) {
150 updateStatus(ThingStatus.ONLINE);
153 // Update channel's statuses based on response.
154 for (int i = 0; i < group.getCount(); i++) {
155 String channelId = CHANNEL_OUTPUT_PREFIX + (i + group.getOffset());
156 String responseDigits = responsePayload.substring(9 + (i * 2), 11 + (i * 2));
158 int value = Integer.parseInt(responseDigits, 16);
160 updateStateAndCacheValue(channelId, value);
162 } catch (Exception e) {
163 logger.warn("Processing response for '{}'-{} failed with {}", getAddress(), group, e.getMessage(), e);
164 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
166 synchronized (pendingRefresh) {
167 pendingRefresh.remove(group);
172 private void updateStateAndCacheValue(String channelId, int value) {
173 if (value < 0x00 || value > 0xff) {
174 throw new IllegalArgumentException("Invalid range. 0x00 - 0xff expected but got value " + value);
177 logger.debug("setting channel '{}' to {}", channelId, value);
179 Integer previousValue;
180 synchronized (cachedStates) {
181 previousValue = cachedStates.put(channelId, value);
184 if (previousValue == null || previousValue.intValue() != value) {
185 updateState(channelId, stateFromValue(channelId, value));
189 private void processWrite(ChannelUID channelUID, Command command) {
190 StringBuilder commandPayload = new StringBuilder();
191 SwitchModuleGroup group = SwitchModuleGroup.mapFromChannel(channelUID);
193 for (int i = group.getOffset(); i < group.getOffset() + group.getCount(); i++) {
194 String channelId = CHANNEL_OUTPUT_PREFIX + i;
197 if (channelId.equals(channelUID.getId())) {
198 digits = valueFromCommand(channelId, command);
199 updateStateAndCacheValue(channelId, digits.intValue());
201 synchronized (cachedStates) {
202 digits = cachedStates.get(channelId);
206 if (digits == null) {
207 commandPayload.append("00");
208 logger.warn("no cached value found for '{}' in module '{}'", channelId, getAddress());
210 commandPayload.append(String.format("%02X", digits.intValue()));
214 NikobusPcLinkHandler pcLink = getPcLink();
215 if (pcLink != null) {
216 pcLink.sendCommand(SwitchModuleCommandFactory.createWriteCommand(getAddress(), group,
217 commandPayload.toString(), this::processWriteCommandResponse));
221 private void processWriteCommandResponse(NikobusCommand.Result result) {
223 String responsePayload = result.get();
225 logger.debug("processWriteCommandResponse '{}'", responsePayload);
227 if (thing.getStatus() != ThingStatus.ONLINE) {
228 updateStatus(ThingStatus.ONLINE);
230 } catch (Exception e) {
231 logger.warn("Processing write confirmation failed with {}", e.getMessage());
232 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
236 protected abstract int valueFromCommand(String channelId, Command command);
238 protected abstract State stateFromValue(String channelId, int value);